Repository: kyleshay/DIM Branch: master Commit: 8064466d14fe Files: 1630 Total size: 354.9 MB Directory structure: gitextract_c6q93roc/ ├── .dockerignore ├── .editorconfig ├── .git-blame-ignore-revs ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── actions/ │ │ └── setup-pnpm/ │ │ └── action.yml │ ├── copilot-instructions.md │ ├── dependabot.yml │ ├── pull_request_template.md │ ├── scripts/ │ │ ├── discord_changelog.py │ │ └── i18n_discord.py │ └── workflows/ │ ├── auto-merge.yml │ ├── changelog-updater.yml │ ├── copilot-setup-steps.yml │ ├── deploy-beta.yml │ ├── deploy-prod.yml │ ├── i18n-bot-download.yml │ ├── i18n-bot-upload.yml │ ├── i18n-update.yml │ ├── lint-workflows.yml │ ├── notify-discord-changelog.yml │ ├── notify-discord-i18n.yml │ ├── pr-cleanup.yml │ ├── pr-reports.yml │ └── pr-validation.yml ├── .gitignore ├── .gitmodules ├── .husky/ │ └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .svgo.yml ├── .vscode/ │ ├── css.json │ ├── dim.code-snippets │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── LICENSE.md ├── README.md ├── babel.config.cjs ├── build/ │ ├── deploy-prod.sh │ ├── purge-cloudflare.sh │ ├── rsync-deploy.sh │ ├── test-changelog.js │ └── update-changelog.js ├── config/ │ ├── .well-known/ │ │ ├── android-config.beta.json │ │ ├── android-config.json │ │ └── apple-config.json │ ├── content-security-policy.ts │ ├── cspell/ │ │ ├── bungie-dict.txt │ │ ├── dim-dict.txt │ │ ├── dim-username-dict.txt │ │ └── programming-dict.txt │ ├── dim_travis.rsa.enc │ ├── dim_travis.rsa.pub │ ├── feature-flags.ts │ ├── i18n.json │ ├── manifest-webapp.ts │ ├── notify-webpack-plugin.ts │ └── webpack.ts ├── crowdin.yml ├── cspell.json ├── docker-compose.yml ├── docs/ │ ├── CHANGELOG.md │ ├── CODE_OF_CONDUCT.md │ ├── COMMUNITY_CURATIONS.md │ ├── CONTRIBUTING.md │ ├── Docker.md │ ├── OLD_CHANGELOG/ │ │ ├── OLD_CHANGELOG_3.X.X.md │ │ ├── OLD_CHANGELOG_4.X.X.md │ │ ├── OLD_CHANGELOG_5.X.X.md │ │ └── OLD_CHANGELOG_6.X.X.md │ ├── TRANSLATIONS.md │ └── clean-changelog.rb ├── eslint.config.js ├── i18next-scanner.config.cjs ├── icons/ │ ├── build_icons.cjs │ └── splash.json ├── jest.config.js ├── package.json ├── src/ │ ├── 404.html │ ├── @types/ │ │ └── i18next.d.ts │ ├── Index.tsx │ ├── StorageTest.tsx │ ├── __mocks__/ │ │ └── fileMock.js │ ├── app/ │ │ ├── App.m.scss │ │ ├── App.m.scss.d.ts │ │ ├── App.tsx │ │ ├── Root.tsx │ │ ├── _variables.scss │ │ ├── accounts/ │ │ │ ├── Account.m.scss │ │ │ ├── Account.m.scss.d.ts │ │ │ ├── Account.tsx │ │ │ ├── MenuAccounts.m.scss │ │ │ ├── MenuAccounts.m.scss.d.ts │ │ │ ├── MenuAccounts.tsx │ │ │ ├── SelectAccount.m.scss │ │ │ ├── SelectAccount.m.scss.d.ts │ │ │ ├── SelectAccount.tsx │ │ │ ├── actions.ts │ │ │ ├── bungie-account.ts │ │ │ ├── destiny-account.test.ts │ │ │ ├── destiny-account.ts │ │ │ ├── observers.ts │ │ │ ├── platforms.ts │ │ │ ├── reducer.ts │ │ │ └── selectors.ts │ │ ├── armory/ │ │ │ ├── AllWishlistRolls.m.scss │ │ │ ├── AllWishlistRolls.m.scss.d.ts │ │ │ ├── AllWishlistRolls.tsx │ │ │ ├── Armory.m.scss │ │ │ ├── Armory.m.scss.d.ts │ │ │ ├── Armory.tsx │ │ │ ├── ArmoryPage.tsx │ │ │ ├── ArmorySheet.m.scss │ │ │ ├── ArmorySheet.m.scss.d.ts │ │ │ ├── ArmorySheet.tsx │ │ │ ├── ItemGrid.tsx │ │ │ ├── LazyArmory.ts │ │ │ ├── Links.m.scss │ │ │ ├── Links.m.scss.d.ts │ │ │ ├── Links.tsx │ │ │ ├── WishListEntry.m.scss │ │ │ ├── WishListEntry.m.scss.d.ts │ │ │ ├── WishListEntry.tsx │ │ │ ├── crafting-utils.ts │ │ │ ├── trait-to-enhanced-trait.d.ts │ │ │ ├── wishlist-collapser.test.ts │ │ │ └── wishlist-collapser.ts │ │ ├── bungie-api/ │ │ │ ├── README.md │ │ │ ├── __snapshots__/ │ │ │ │ └── http-client.test.ts.snap │ │ │ ├── authenticated-fetch.ts │ │ │ ├── bungie-api-utils.ts │ │ │ ├── bungie-core-api.ts │ │ │ ├── bungie-service-helper.ts │ │ │ ├── destiny1-api.ts │ │ │ ├── destiny2-api.ts │ │ │ ├── error-toaster.tsx │ │ │ ├── http-client.test.ts │ │ │ ├── http-client.ts │ │ │ ├── oauth-tokens.ts │ │ │ ├── oauth.ts │ │ │ ├── rate-limit-config.ts │ │ │ └── rate-limiter.ts │ │ ├── character-tile/ │ │ │ ├── CharacterHeaderXP.m.scss │ │ │ ├── CharacterHeaderXP.m.scss.d.ts │ │ │ ├── CharacterHeaderXP.tsx │ │ │ ├── CharacterTile.m.scss │ │ │ ├── CharacterTile.m.scss.d.ts │ │ │ ├── CharacterTile.tsx │ │ │ ├── CharacterTileButton.m.scss │ │ │ ├── CharacterTileButton.m.scss.d.ts │ │ │ ├── CharacterTileButton.tsx │ │ │ ├── StoreHeading.m.scss │ │ │ ├── StoreHeading.m.scss.d.ts │ │ │ ├── StoreHeading.tsx │ │ │ ├── StoreIcon.m.scss │ │ │ ├── StoreIcon.m.scss.d.ts │ │ │ └── StoreIcon.tsx │ │ ├── clarity/ │ │ │ ├── about.ts │ │ │ ├── actions.ts │ │ │ ├── descriptions/ │ │ │ │ ├── ClarityDescriptions.tsx │ │ │ │ ├── Description.m.scss │ │ │ │ ├── Description.m.scss.d.ts │ │ │ │ ├── character-stats.ts │ │ │ │ ├── descriptionInterface.ts │ │ │ │ └── loadDescriptions.ts │ │ │ ├── reducer.ts │ │ │ └── selectors.ts │ │ ├── compare/ │ │ │ ├── Compare.m.scss │ │ │ ├── Compare.m.scss.d.ts │ │ │ ├── Compare.tsx │ │ │ ├── CompareButtons.m.scss │ │ │ ├── CompareButtons.m.scss.d.ts │ │ │ ├── CompareColumns.m.scss │ │ │ ├── CompareColumns.m.scss.d.ts │ │ │ ├── CompareColumns.tsx │ │ │ ├── CompareContainer.tsx │ │ │ ├── CompareItem.m.scss │ │ │ ├── CompareItem.m.scss.d.ts │ │ │ ├── CompareItem.tsx │ │ │ ├── CompareStat.m.scss │ │ │ ├── CompareStat.m.scss.d.ts │ │ │ ├── CompareStat.tsx │ │ │ ├── CompareSuggestions.tsx │ │ │ ├── actions.ts │ │ │ ├── compare-buttons.tsx │ │ │ ├── compare-utils.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ ├── css-variables.ts │ │ ├── debug/ │ │ │ ├── Debug.m.scss │ │ │ ├── Debug.m.scss.d.ts │ │ │ └── Debug.tsx │ │ ├── destiny1/ │ │ │ ├── activities/ │ │ │ │ ├── Activities.m.scss │ │ │ │ ├── Activities.m.scss.d.ts │ │ │ │ └── Activities.tsx │ │ │ ├── d1-bucket-categories.ts │ │ │ ├── d1-buckets.ts │ │ │ ├── d1-definitions.ts │ │ │ ├── d1-factions.ts │ │ │ ├── d1-manifest-types.ts │ │ │ ├── loadout-builder/ │ │ │ │ ├── D1LoadoutBuilder.m.scss │ │ │ │ ├── D1LoadoutBuilder.m.scss.d.ts │ │ │ │ ├── D1LoadoutBuilder.tsx │ │ │ │ ├── ExcludeItemsDropTarget.tsx │ │ │ │ ├── GeneratedSet.m.scss │ │ │ │ ├── GeneratedSet.m.scss.d.ts │ │ │ │ ├── GeneratedSet.tsx │ │ │ │ ├── LoadoutBuilderDropTarget.m.scss │ │ │ │ ├── LoadoutBuilderDropTarget.m.scss.d.ts │ │ │ │ ├── LoadoutBuilderDropTarget.tsx │ │ │ │ ├── LoadoutBuilderItem.m.scss │ │ │ │ ├── LoadoutBuilderItem.m.scss.d.ts │ │ │ │ ├── LoadoutBuilderItem.tsx │ │ │ │ ├── LoadoutBuilderLockPerk.m.scss │ │ │ │ ├── LoadoutBuilderLockPerk.m.scss.d.ts │ │ │ │ ├── LoadoutBuilderLockPerk.tsx │ │ │ │ ├── LoadoutBuilderLocksDialog.m.scss │ │ │ │ ├── LoadoutBuilderLocksDialog.m.scss.d.ts │ │ │ │ ├── LoadoutBuilderLocksDialog.tsx │ │ │ │ ├── calculate.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── loadout-drawer/ │ │ │ │ ├── Buttons.m.scss │ │ │ │ ├── Buttons.m.scss.d.ts │ │ │ │ ├── Buttons.tsx │ │ │ │ ├── D1LoadoutDrawer.m.scss │ │ │ │ ├── D1LoadoutDrawer.m.scss.d.ts │ │ │ │ ├── D1LoadoutDrawer.tsx │ │ │ │ ├── LoadoutDrawerBucket.m.scss │ │ │ │ ├── LoadoutDrawerBucket.m.scss.d.ts │ │ │ │ ├── LoadoutDrawerBucket.tsx │ │ │ │ ├── LoadoutDrawerContents.m.scss │ │ │ │ ├── LoadoutDrawerContents.m.scss.d.ts │ │ │ │ ├── LoadoutDrawerContents.tsx │ │ │ │ ├── LoadoutDrawerItem.m.scss │ │ │ │ ├── LoadoutDrawerItem.m.scss.d.ts │ │ │ │ ├── LoadoutDrawerItem.tsx │ │ │ │ ├── LoadoutDrawerOptions.m.scss │ │ │ │ ├── LoadoutDrawerOptions.m.scss.d.ts │ │ │ │ └── LoadoutDrawerOptions.tsx │ │ │ ├── record-books/ │ │ │ │ ├── RecordBooks.m.scss │ │ │ │ ├── RecordBooks.m.scss.d.ts │ │ │ │ └── RecordBooks.tsx │ │ │ └── vendors/ │ │ │ ├── D1Vendor.tsx │ │ │ ├── D1VendorItem.m.scss │ │ │ ├── D1VendorItem.m.scss.d.ts │ │ │ ├── D1VendorItem.tsx │ │ │ ├── D1VendorItems.tsx │ │ │ ├── D1Vendors.m.scss │ │ │ ├── D1Vendors.m.scss.d.ts │ │ │ ├── D1Vendors.tsx │ │ │ └── vendor.service.ts │ │ ├── destiny2/ │ │ │ ├── d2-bucket-categories.ts │ │ │ ├── d2-buckets.ts │ │ │ ├── d2-definitions.test.ts │ │ │ ├── d2-definitions.ts │ │ │ └── definitions.ts │ │ ├── developer/ │ │ │ └── Developer.tsx │ │ ├── dim-api/ │ │ │ ├── actions.ts │ │ │ ├── api-permission-prompt.m.scss │ │ │ ├── api-permission-prompt.m.scss.d.ts │ │ │ ├── api-permission-prompt.tsx │ │ │ ├── api-types.ts │ │ │ ├── basic-actions.ts │ │ │ ├── dim-api-helper.ts │ │ │ ├── dim-api.ts │ │ │ ├── import.ts │ │ │ ├── reducer.test.ts │ │ │ ├── reducer.ts │ │ │ ├── register-app.ts │ │ │ └── selectors.ts │ │ ├── dim-ui/ │ │ │ ├── AlertIcon.m.scss │ │ │ ├── AlertIcon.m.scss.d.ts │ │ │ ├── AlertIcon.tsx │ │ │ ├── AnimatedNumber.tsx │ │ │ ├── AutoRefresh.tsx │ │ │ ├── BungieImage.tsx │ │ │ ├── CharacterSelect.m.scss │ │ │ ├── CharacterSelect.m.scss.d.ts │ │ │ ├── CharacterSelect.tsx │ │ │ ├── CheckButton.m.scss │ │ │ ├── CheckButton.m.scss.d.ts │ │ │ ├── CheckButton.tsx │ │ │ ├── ClassIcon.tsx │ │ │ ├── ClickOutside.tsx │ │ │ ├── ClickOutsideRoot.tsx │ │ │ ├── ClosableContainer.m.scss │ │ │ ├── ClosableContainer.m.scss.d.ts │ │ │ ├── ClosableContainer.tsx │ │ │ ├── CollapsibleTitle.m.scss │ │ │ ├── CollapsibleTitle.m.scss.d.ts │ │ │ ├── CollapsibleTitle.tsx │ │ │ ├── ConfirmButton.m.scss │ │ │ ├── ConfirmButton.m.scss.d.ts │ │ │ ├── ConfirmButton.tsx │ │ │ ├── Countdown.tsx │ │ │ ├── CustomStatTotal.m.scss │ │ │ ├── CustomStatTotal.m.scss.d.ts │ │ │ ├── CustomStatTotal.tsx │ │ │ ├── CustomStatWeights.m.scss │ │ │ ├── CustomStatWeights.m.scss.d.ts │ │ │ ├── CustomStatWeights.tsx │ │ │ ├── DestinyTooltipText.m.scss │ │ │ ├── DestinyTooltipText.m.scss.d.ts │ │ │ ├── DestinyTooltipText.tsx │ │ │ ├── DiamondProgress.m.scss │ │ │ ├── DiamondProgress.m.scss.d.ts │ │ │ ├── DiamondProgress.tsx │ │ │ ├── Dropdown.m.scss │ │ │ ├── Dropdown.m.scss.d.ts │ │ │ ├── Dropdown.tsx │ │ │ ├── ElementIcon.m.scss │ │ │ ├── ElementIcon.m.scss.d.ts │ │ │ ├── ElementIcon.tsx │ │ │ ├── EnergyIncrements.m.scss │ │ │ ├── EnergyIncrements.m.scss.d.ts │ │ │ ├── EnergyIncrements.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── ExpandableTextBlock.m.scss │ │ │ ├── ExpandableTextBlock.m.scss.d.ts │ │ │ ├── ExpandableTextBlock.tsx │ │ │ ├── ExternalLink.tsx │ │ │ ├── FileUpload.m.scss │ │ │ ├── FileUpload.m.scss.d.ts │ │ │ ├── FileUpload.tsx │ │ │ ├── FilterPills.m.scss │ │ │ ├── FilterPills.m.scss.d.ts │ │ │ ├── FilterPills.tsx │ │ │ ├── FractionalPowerLevel.m.scss │ │ │ ├── FractionalPowerLevel.m.scss.d.ts │ │ │ ├── FractionalPowerLevel.tsx │ │ │ ├── HelpLink.m.scss │ │ │ ├── HelpLink.m.scss.d.ts │ │ │ ├── HelpLink.tsx │ │ │ ├── ItemCategoryIcon.m.scss │ │ │ ├── ItemCategoryIcon.m.scss.d.ts │ │ │ ├── ItemCategoryIcon.tsx │ │ │ ├── ItemPop.m.scss │ │ │ ├── ItemPop.m.scss.d.ts │ │ │ ├── KeyHelp.m.scss │ │ │ ├── KeyHelp.m.scss.d.ts │ │ │ ├── KeyHelp.tsx │ │ │ ├── Loading.m.scss │ │ │ ├── Loading.m.scss.d.ts │ │ │ ├── Loading.tsx │ │ │ ├── PageLoading.m.scss │ │ │ ├── PageLoading.m.scss.d.ts │ │ │ ├── PageLoading.tsx │ │ │ ├── PageWithMenu.m.scss │ │ │ ├── PageWithMenu.m.scss.d.ts │ │ │ ├── PageWithMenu.tsx │ │ │ ├── PressTip.m.scss │ │ │ ├── PressTip.m.scss.d.ts │ │ │ ├── PressTip.tsx │ │ │ ├── README.md │ │ │ ├── RadioButtons.m.scss │ │ │ ├── RadioButtons.m.scss.d.ts │ │ │ ├── RadioButtons.tsx │ │ │ ├── Select.m.scss │ │ │ ├── Select.m.scss.d.ts │ │ │ ├── Select.tsx │ │ │ ├── SetFilterButton.m.scss │ │ │ ├── SetFilterButton.m.scss.d.ts │ │ │ ├── SetFilterButton.tsx │ │ │ ├── Sheet.m.scss │ │ │ ├── Sheet.m.scss.d.ts │ │ │ ├── Sheet.tsx │ │ │ ├── SheetHorizontalScrollContainer.m.scss │ │ │ ├── SheetHorizontalScrollContainer.m.scss.d.ts │ │ │ ├── SheetHorizontalScrollContainer.tsx │ │ │ ├── ShowPageLoading.tsx │ │ │ ├── SpecialtyModSlotIcon.m.scss │ │ │ ├── SpecialtyModSlotIcon.m.scss.d.ts │ │ │ ├── SpecialtyModSlotIcon.tsx │ │ │ ├── StaticPage.m.scss │ │ │ ├── StaticPage.m.scss.d.ts │ │ │ ├── StaticPage.tsx │ │ │ ├── Switch.m.scss │ │ │ ├── Switch.m.scss.d.ts │ │ │ ├── Switch.tsx │ │ │ ├── TileGrid.m.scss │ │ │ ├── TileGrid.m.scss.d.ts │ │ │ ├── TileGrid.tsx │ │ │ ├── UserGuideLink.tsx │ │ │ ├── VirtualList.m.scss │ │ │ ├── VirtualList.m.scss.d.ts │ │ │ ├── VirtualList.tsx │ │ │ ├── WeaponGroupingIcon.m.scss │ │ │ ├── WeaponGroupingIcon.m.scss.d.ts │ │ │ ├── WeaponGroupingIcon.tsx │ │ │ ├── _tooltip-mixins.scss │ │ │ ├── common.m.scss │ │ │ ├── destiny-symbols/ │ │ │ │ ├── ColorDestinySymbols.m.scss │ │ │ │ ├── ColorDestinySymbols.m.scss.d.ts │ │ │ │ ├── ColorDestinySymbols.tsx │ │ │ │ ├── RichDestinyText.tsx │ │ │ │ ├── SymbolsPicker.m.scss │ │ │ │ ├── SymbolsPicker.m.scss.d.ts │ │ │ │ ├── SymbolsPicker.tsx │ │ │ │ ├── destiny-symbols.test.ts │ │ │ │ ├── destiny-symbols.ts │ │ │ │ └── rich-destiny-text.ts │ │ │ ├── dim-button.scss │ │ │ ├── scroll.ts │ │ │ ├── sheets-open.ts │ │ │ ├── svgs/ │ │ │ │ ├── BucketIcon.tsx │ │ │ │ └── itemCategory.ts │ │ │ ├── table-columns.test.ts │ │ │ ├── table-columns.ts │ │ │ ├── text-complete/ │ │ │ │ ├── text-complete.m.scss │ │ │ │ ├── text-complete.m.scss.d.ts │ │ │ │ └── text-complete.ts │ │ │ ├── useBulkNote.m.scss │ │ │ ├── useBulkNote.m.scss.d.ts │ │ │ ├── useBulkNote.tsx │ │ │ ├── useConfirm.tsx │ │ │ ├── useDialog.m.scss │ │ │ ├── useDialog.m.scss.d.ts │ │ │ ├── useDialog.tsx │ │ │ ├── useFixOverscrollBehavior.ts │ │ │ ├── usePopper.ts │ │ │ ├── usePrompt.m.scss │ │ │ ├── usePrompt.m.scss.d.ts │ │ │ └── usePrompt.tsx │ │ ├── farming/ │ │ │ ├── Farming.m.scss │ │ │ ├── Farming.m.scss.d.ts │ │ │ ├── Farming.tsx │ │ │ ├── actions.ts │ │ │ ├── basic-actions.ts │ │ │ ├── reducer.ts │ │ │ └── selectors.ts │ │ ├── gear-power/ │ │ │ ├── GearPower.m.scss │ │ │ ├── GearPower.m.scss.d.ts │ │ │ ├── GearPower.tsx │ │ │ └── gear-power.ts │ │ ├── google.ts │ │ ├── hotkeys/ │ │ │ ├── GlobalHotkeys.tsx │ │ │ ├── HotkeysCheatSheet.m.scss │ │ │ ├── HotkeysCheatSheet.m.scss.d.ts │ │ │ ├── HotkeysCheatSheet.tsx │ │ │ ├── hotkeys.test.ts │ │ │ ├── hotkeys.ts │ │ │ └── useHotkey.ts │ │ ├── i18n.ts │ │ ├── i18next-t.ts │ │ ├── infuse/ │ │ │ ├── InfusionFinder.m.scss │ │ │ ├── InfusionFinder.m.scss.d.ts │ │ │ ├── InfusionFinder.tsx │ │ │ └── infuse.ts │ │ ├── inventory/ │ │ │ ├── ArtifactXP.m.scss │ │ │ ├── ArtifactXP.m.scss.d.ts │ │ │ ├── ArtifactXP.tsx │ │ │ ├── BadgeInfo.m.scss │ │ │ ├── BadgeInfo.m.scss.d.ts │ │ │ ├── BadgeInfo.tsx │ │ │ ├── ConnectedInventoryItem.tsx │ │ │ ├── DragPerformanceFix.m.scss │ │ │ ├── DragPerformanceFix.m.scss.d.ts │ │ │ ├── DragPerformanceFix.tsx │ │ │ ├── DraggableInventoryItem.m.scss │ │ │ ├── DraggableInventoryItem.m.scss.d.ts │ │ │ ├── DraggableInventoryItem.tsx │ │ │ ├── InventoryItem.m.scss │ │ │ ├── InventoryItem.m.scss.d.ts │ │ │ ├── InventoryItem.tsx │ │ │ ├── ItemDragPreview.tsx │ │ │ ├── ItemIcon.m.scss │ │ │ ├── ItemIcon.m.scss.d.ts │ │ │ ├── ItemIcon.tsx │ │ │ ├── ItemIconPlaceholder.m.scss │ │ │ ├── ItemIconPlaceholder.m.scss.d.ts │ │ │ ├── ItemIconPlaceholder.tsx │ │ │ ├── ItemPopupTrigger.tsx │ │ │ ├── ItemPowerSet.m.scss │ │ │ ├── ItemPowerSet.m.scss.d.ts │ │ │ ├── ItemPowerSet.tsx │ │ │ ├── MoveNotifications.m.scss │ │ │ ├── MoveNotifications.m.scss.d.ts │ │ │ ├── MoveNotifications.tsx │ │ │ ├── NewItemIndicator.m.scss │ │ │ ├── NewItemIndicator.m.scss.d.ts │ │ │ ├── NewItemIndicator.tsx │ │ │ ├── PullFromPostmaster.m.scss │ │ │ ├── PullFromPostmaster.m.scss.d.ts │ │ │ ├── PullFromPostmaster.tsx │ │ │ ├── RatingIcon.m.scss │ │ │ ├── RatingIcon.m.scss.d.ts │ │ │ ├── RatingIcon.tsx │ │ │ ├── SyncTagLock.tsx │ │ │ ├── TagIcon.tsx │ │ │ ├── __snapshots__/ │ │ │ │ └── d2-stores.test.ts.snap │ │ │ ├── actions.ts │ │ │ ├── advanced-write-actions.ts │ │ │ ├── bulk-actions.tsx │ │ │ ├── cross-tab.ts │ │ │ ├── d1-stores.ts │ │ │ ├── d2-stores.test.ts │ │ │ ├── d2-stores.ts │ │ │ ├── dim-item-info.ts │ │ │ ├── drag-events.ts │ │ │ ├── inventory-buckets.ts │ │ │ ├── item-move-service.ts │ │ │ ├── item-types.ts │ │ │ ├── locate-item.ts │ │ │ ├── manual-moves.ts │ │ │ ├── move-item.ts │ │ │ ├── note-hashtags.ts │ │ │ ├── notes-hashtags.test.ts │ │ │ ├── observers.ts │ │ │ ├── reducer.ts │ │ │ ├── rewards.ts │ │ │ ├── selectors.ts │ │ │ ├── spreadsheets.ts │ │ │ ├── store/ │ │ │ │ ├── armor-quality.ts │ │ │ │ ├── catalyst.ts │ │ │ │ ├── character-utils.ts │ │ │ │ ├── crafted.ts │ │ │ │ ├── d1-item-factory.ts │ │ │ │ ├── d1-store-factory.ts │ │ │ │ ├── d2-item-factory.ts │ │ │ │ ├── d2-store-factory.ts │ │ │ │ ├── deepsight.ts │ │ │ │ ├── energy.ts │ │ │ │ ├── enhanced-info.d.ts │ │ │ │ ├── exotic-class-item.ts │ │ │ │ ├── exotic-to-catalyst-record.d.ts │ │ │ │ ├── hooks.ts │ │ │ │ ├── item-index.ts │ │ │ │ ├── masterwork.ts │ │ │ │ ├── missing-sources.d.ts │ │ │ │ ├── objectives.ts │ │ │ │ ├── override-sockets.ts │ │ │ │ ├── patterns.ts │ │ │ │ ├── season-d2ai.d.ts │ │ │ │ ├── season.ts │ │ │ │ ├── selectors.ts │ │ │ │ ├── sockets.ts │ │ │ │ ├── stats-conditional.ts │ │ │ │ ├── stats-custom.ts │ │ │ │ ├── stats.ts │ │ │ │ └── well-rested.ts │ │ │ ├── store-types.ts │ │ │ ├── stores-helpers.ts │ │ │ └── subclass.ts │ │ ├── inventory-page/ │ │ │ ├── CategoryStrip.m.scss │ │ │ ├── CategoryStrip.m.scss.d.ts │ │ │ ├── CategoryStrip.tsx │ │ │ ├── D1Reputation.m.scss │ │ │ ├── D1Reputation.m.scss.d.ts │ │ │ ├── D1Reputation.tsx │ │ │ ├── D1ReputationSection.tsx │ │ │ ├── DesktopStores.m.scss │ │ │ ├── DesktopStores.m.scss.d.ts │ │ │ ├── DesktopStores.tsx │ │ │ ├── HeaderShadowDiv.m.scss │ │ │ ├── HeaderShadowDiv.m.scss.d.ts │ │ │ ├── HeaderShadowDiv.tsx │ │ │ ├── Inventory.tsx │ │ │ ├── InventoryCollapsibleTitle.m.scss │ │ │ ├── InventoryCollapsibleTitle.m.scss.d.ts │ │ │ ├── InventoryCollapsibleTitle.tsx │ │ │ ├── PhoneStores.m.scss │ │ │ ├── PhoneStores.m.scss.d.ts │ │ │ ├── PhoneStores.tsx │ │ │ ├── PhoneStoresHeader.m.scss │ │ │ ├── PhoneStoresHeader.m.scss.d.ts │ │ │ ├── PhoneStoresHeader.tsx │ │ │ ├── StoreBucket.m.scss │ │ │ ├── StoreBucket.m.scss.d.ts │ │ │ ├── StoreBucket.scss │ │ │ ├── StoreBucket.tsx │ │ │ ├── StoreBucketDropTarget.m.scss │ │ │ ├── StoreBucketDropTarget.m.scss.d.ts │ │ │ ├── StoreBucketDropTarget.tsx │ │ │ ├── StoreBuckets.m.scss │ │ │ ├── StoreBuckets.m.scss.d.ts │ │ │ ├── StoreBuckets.tsx │ │ │ ├── StoreInventoryItem.tsx │ │ │ ├── Stores.scss │ │ │ └── Stores.tsx │ │ ├── issue-awareness-banner/ │ │ │ ├── Game2Give.m.scss │ │ │ ├── Game2Give.m.scss.d.ts │ │ │ ├── Game2Give.tsx │ │ │ ├── IssueAwarenessBanner.tsx │ │ │ └── useGame2GiveData.tsx │ │ ├── item-actions/ │ │ │ ├── ActionButton.m.scss │ │ │ ├── ActionButton.m.scss.d.ts │ │ │ ├── ActionButton.tsx │ │ │ ├── ActionButtons.m.scss │ │ │ ├── ActionButtons.m.scss.d.ts │ │ │ ├── ActionButtons.tsx │ │ │ ├── ItemAccessoryButtons.tsx │ │ │ ├── ItemActionsDropdown.m.scss │ │ │ ├── ItemActionsDropdown.m.scss.d.ts │ │ │ ├── ItemActionsDropdown.tsx │ │ │ ├── ItemMoveLocations.m.scss │ │ │ ├── ItemMoveLocations.m.scss.d.ts │ │ │ ├── ItemMoveLocations.tsx │ │ │ ├── LockButton.m.scss │ │ │ ├── LockButton.m.scss.d.ts │ │ │ └── LockButton.tsx │ │ ├── item-feed/ │ │ │ ├── Highlights.m.scss │ │ │ ├── Highlights.m.scss.d.ts │ │ │ ├── Highlights.tsx │ │ │ ├── ItemFeed.m.scss │ │ │ ├── ItemFeed.m.scss.d.ts │ │ │ ├── ItemFeed.tsx │ │ │ ├── ItemFeedPage.m.scss │ │ │ ├── ItemFeedPage.m.scss.d.ts │ │ │ ├── ItemFeedPage.tsx │ │ │ ├── ItemFeedSidebar.m.scss │ │ │ ├── ItemFeedSidebar.m.scss.d.ts │ │ │ ├── ItemFeedSidebar.tsx │ │ │ ├── TagButtons.m.scss │ │ │ ├── TagButtons.m.scss.d.ts │ │ │ └── TagButtons.tsx │ │ ├── item-picker/ │ │ │ ├── ItemPicker.m.scss │ │ │ ├── ItemPicker.m.scss.d.ts │ │ │ ├── ItemPicker.tsx │ │ │ ├── ItemPickerContainer.tsx │ │ │ └── item-picker.ts │ │ ├── item-popup/ │ │ │ ├── AmmoIcon.m.scss │ │ │ ├── AmmoIcon.m.scss.d.ts │ │ │ ├── AmmoIcon.tsx │ │ │ ├── ApplyPerkSelection.m.scss │ │ │ ├── ApplyPerkSelection.m.scss.d.ts │ │ │ ├── ApplyPerkSelection.tsx │ │ │ ├── ArchetypeSocket.m.scss │ │ │ ├── ArchetypeSocket.m.scss.d.ts │ │ │ ├── ArchetypeSocket.tsx │ │ │ ├── BreakerType.m.scss │ │ │ ├── BreakerType.m.scss.d.ts │ │ │ ├── BreakerType.tsx │ │ │ ├── DeepSightHarmonizerIcon.m.scss │ │ │ ├── DeepSightHarmonizerIcon.m.scss.d.ts │ │ │ ├── DeepsightHarmonizerIcon.tsx │ │ │ ├── DesktopItemActions.m.scss │ │ │ ├── DesktopItemActions.m.scss.d.ts │ │ │ ├── DesktopItemActions.tsx │ │ │ ├── EmblemPreview.m.scss │ │ │ ├── EmblemPreview.m.scss.d.ts │ │ │ ├── EmblemPreview.tsx │ │ │ ├── EmoteSockets.m.scss │ │ │ ├── EmoteSockets.m.scss.d.ts │ │ │ ├── EmoteSockets.tsx │ │ │ ├── EnergyMeter.m.scss │ │ │ ├── EnergyMeter.m.scss.d.ts │ │ │ ├── EnergyMeter.tsx │ │ │ ├── ItemDescription.m.scss │ │ │ ├── ItemDescription.m.scss.d.ts │ │ │ ├── ItemDescription.tsx │ │ │ ├── ItemDetails.m.scss │ │ │ ├── ItemDetails.m.scss.d.ts │ │ │ ├── ItemDetails.tsx │ │ │ ├── ItemExpiration.tsx │ │ │ ├── ItemMoveAmount.m.scss │ │ │ ├── ItemMoveAmount.m.scss.d.ts │ │ │ ├── ItemMoveAmount.tsx │ │ │ ├── ItemPerks.m.scss │ │ │ ├── ItemPerks.m.scss.d.ts │ │ │ ├── ItemPerks.tsx │ │ │ ├── ItemPerksList.m.scss │ │ │ ├── ItemPerksList.m.scss.d.ts │ │ │ ├── ItemPerksList.tsx │ │ │ ├── ItemPopup.m.scss │ │ │ ├── ItemPopup.m.scss.d.ts │ │ │ ├── ItemPopup.tsx │ │ │ ├── ItemPopupBody.scss │ │ │ ├── ItemPopupContainer.tsx │ │ │ ├── ItemPopupHeader.m.scss │ │ │ ├── ItemPopupHeader.m.scss.d.ts │ │ │ ├── ItemPopupHeader.tsx │ │ │ ├── ItemPopupTabs.m.scss │ │ │ ├── ItemPopupTabs.m.scss.d.ts │ │ │ ├── ItemPopupTabs.tsx │ │ │ ├── ItemSockets.m.scss │ │ │ ├── ItemSockets.m.scss.d.ts │ │ │ ├── ItemSockets.tsx │ │ │ ├── ItemSocketsGeneral.m.scss │ │ │ ├── ItemSocketsGeneral.m.scss.d.ts │ │ │ ├── ItemSocketsGeneral.tsx │ │ │ ├── ItemSocketsWeapons.m.scss │ │ │ ├── ItemSocketsWeapons.m.scss.d.ts │ │ │ ├── ItemSocketsWeapons.tsx │ │ │ ├── ItemStat.m.scss │ │ │ ├── ItemStat.m.scss.d.ts │ │ │ ├── ItemStat.tsx │ │ │ ├── ItemStats.m.scss │ │ │ ├── ItemStats.m.scss.d.ts │ │ │ ├── ItemStats.tsx │ │ │ ├── ItemTagHotkeys.tsx │ │ │ ├── ItemTagSelector.m.scss │ │ │ ├── ItemTagSelector.m.scss.d.ts │ │ │ ├── ItemTagSelector.tsx │ │ │ ├── ItemTalentGrid.m.scss │ │ │ ├── ItemTalentGrid.m.scss.d.ts │ │ │ ├── ItemTalentGrid.tsx │ │ │ ├── KillTracker.tsx │ │ │ ├── MetricCategories.m.scss │ │ │ ├── MetricCategories.m.scss.d.ts │ │ │ ├── MetricCategories.tsx │ │ │ ├── NotesArea.m.scss │ │ │ ├── NotesArea.m.scss.d.ts │ │ │ ├── NotesArea.tsx │ │ │ ├── Plug.m.scss │ │ │ ├── Plug.m.scss.d.ts │ │ │ ├── Plug.tsx │ │ │ ├── PlugTooltip.m.scss │ │ │ ├── PlugTooltip.m.scss.d.ts │ │ │ ├── PlugTooltip.tsx │ │ │ ├── RecoilStat.tsx │ │ │ ├── SetBonus.m.scss │ │ │ ├── SetBonus.m.scss.d.ts │ │ │ ├── SetBonus.tsx │ │ │ ├── Socket.m.scss │ │ │ ├── Socket.m.scss.d.ts │ │ │ ├── Socket.tsx │ │ │ ├── SocketDetails.m.scss │ │ │ ├── SocketDetails.m.scss.d.ts │ │ │ ├── SocketDetails.tsx │ │ │ ├── SocketDetailsSelectedPlug.m.scss │ │ │ ├── SocketDetailsSelectedPlug.m.scss.d.ts │ │ │ ├── SocketDetailsSelectedPlug.tsx │ │ │ ├── WeaponCatalystInfo.m.scss │ │ │ ├── WeaponCatalystInfo.m.scss.d.ts │ │ │ ├── WeaponCatalystInfo.tsx │ │ │ ├── WeaponCraftedInfo.m.scss │ │ │ ├── WeaponCraftedInfo.m.scss.d.ts │ │ │ ├── WeaponCraftedInfo.tsx │ │ │ ├── WeaponDeepsightInfo.m.scss │ │ │ ├── WeaponDeepsightInfo.m.scss.d.ts │ │ │ ├── WeaponDeepsightInfo.tsx │ │ │ ├── item-popup-actions.test.ts │ │ │ ├── item-popup-actions.ts │ │ │ ├── item-popup.ts │ │ │ └── sidecar-popper-modifier.ts │ │ ├── item-triage/ │ │ │ ├── ItemTriage.m.scss │ │ │ ├── ItemTriage.m.scss.d.ts │ │ │ ├── ItemTriage.tsx │ │ │ ├── TriageFactors.m.scss │ │ │ ├── TriageFactors.m.scss.d.ts │ │ │ ├── triage-factors.tsx │ │ │ ├── triage-utils.test.ts │ │ │ └── triage-utils.ts │ │ ├── loadout/ │ │ │ ├── LoadoutView.m.scss │ │ │ ├── LoadoutView.m.scss.d.ts │ │ │ ├── LoadoutView.tsx │ │ │ ├── Loadouts.m.scss │ │ │ ├── Loadouts.m.scss.d.ts │ │ │ ├── Loadouts.tsx │ │ │ ├── LoadoutsRow.m.scss │ │ │ ├── LoadoutsRow.tsx │ │ │ ├── ModPicker.tsx │ │ │ ├── SubclassPlugDrawer.tsx │ │ │ ├── actions.ts │ │ │ ├── armor-upgrade-utils.ts │ │ │ ├── fashion/ │ │ │ │ ├── FashionDrawer.m.scss │ │ │ │ ├── FashionDrawer.m.scss.d.ts │ │ │ │ └── FashionDrawer.tsx │ │ │ ├── ingame/ │ │ │ │ ├── EditInGameLoadout.m.scss │ │ │ │ ├── EditInGameLoadout.m.scss.d.ts │ │ │ │ ├── EditInGameLoadout.tsx │ │ │ │ ├── EditInGameLoadoutIdentifiers.m.scss │ │ │ │ ├── EditInGameLoadoutIdentifiers.m.scss.d.ts │ │ │ │ ├── EditInGameLoadoutIdentifiers.tsx │ │ │ │ ├── InGameLoadoutDetailsSheet.m.scss │ │ │ │ ├── InGameLoadoutDetailsSheet.m.scss.d.ts │ │ │ │ ├── InGameLoadoutDetailsSheet.tsx │ │ │ │ ├── InGameLoadoutIcon.m.scss │ │ │ │ ├── InGameLoadoutIcon.m.scss.d.ts │ │ │ │ ├── InGameLoadoutIcon.tsx │ │ │ │ ├── InGameLoadoutIdentifiersSelectButton.m.scss │ │ │ │ ├── InGameLoadoutIdentifiersSelectButton.m.scss.d.ts │ │ │ │ ├── InGameLoadoutIdentifiersSelectButton.tsx │ │ │ │ ├── InGameLoadoutStrip.m.scss │ │ │ │ ├── InGameLoadoutStrip.m.scss.d.ts │ │ │ │ ├── InGameLoadoutStrip.tsx │ │ │ │ ├── RadioButton.m.scss │ │ │ │ ├── RadioButton.m.scss.d.ts │ │ │ │ ├── RadioButton.tsx │ │ │ │ ├── SelectInGameLoadoutIdentifiers.m.scss │ │ │ │ ├── SelectInGameLoadoutIdentifiers.m.scss.d.ts │ │ │ │ ├── SelectInGameLoadoutIdentifiers.tsx │ │ │ │ ├── actions.ts │ │ │ │ ├── ingame-loadout-apply.ts │ │ │ │ ├── ingame-loadout-utils.ts │ │ │ │ ├── reducer.ts │ │ │ │ └── selectors.ts │ │ │ ├── known-values.ts │ │ │ ├── loadout-edit/ │ │ │ │ ├── LoadoutEdit.m.scss │ │ │ │ ├── LoadoutEdit.m.scss.d.ts │ │ │ │ ├── LoadoutEdit.tsx │ │ │ │ ├── LoadoutEditBucket.m.scss │ │ │ │ ├── LoadoutEditBucket.m.scss.d.ts │ │ │ │ ├── LoadoutEditBucket.tsx │ │ │ │ ├── LoadoutEditSection.m.scss │ │ │ │ ├── LoadoutEditSection.m.scss.d.ts │ │ │ │ ├── LoadoutEditSection.tsx │ │ │ │ ├── LoadoutEditSubclass.m.scss │ │ │ │ ├── LoadoutEditSubclass.m.scss.d.ts │ │ │ │ ├── LoadoutEditSubclass.tsx │ │ │ │ └── useEquipDropTargets.tsx │ │ │ ├── loadout-item-utils.ts │ │ │ ├── loadout-menu/ │ │ │ │ ├── LoadoutPopup.m.scss │ │ │ │ ├── LoadoutPopup.m.scss.d.ts │ │ │ │ ├── LoadoutPopup.tsx │ │ │ │ ├── LoadoutPopupRandomize.m.scss │ │ │ │ ├── LoadoutPopupRandomize.m.scss.d.ts │ │ │ │ ├── LoadoutPopupRandomize.tsx │ │ │ │ ├── MaxlightButton.m.scss │ │ │ │ ├── MaxlightButton.m.scss.d.ts │ │ │ │ └── MaxlightButton.tsx │ │ │ ├── loadout-share/ │ │ │ │ ├── LoadoutImportSheet.m.scss │ │ │ │ ├── LoadoutImportSheet.m.scss.d.ts │ │ │ │ ├── LoadoutImportSheet.tsx │ │ │ │ ├── LoadoutShareSheet.m.scss │ │ │ │ ├── LoadoutShareSheet.m.scss.d.ts │ │ │ │ ├── LoadoutShareSheet.tsx │ │ │ │ ├── loadout-import.test.ts │ │ │ │ └── loadout-import.ts │ │ │ ├── loadout-type-converters.ts │ │ │ ├── loadout-types.ts │ │ │ ├── loadout-ui/ │ │ │ │ ├── BucketPlaceholder.m.scss │ │ │ │ ├── BucketPlaceholder.m.scss.d.ts │ │ │ │ ├── BucketPlaceholder.tsx │ │ │ │ ├── EmptySubclass.tsx │ │ │ │ ├── FashionMods.m.scss │ │ │ │ ├── FashionMods.m.scss.d.ts │ │ │ │ ├── FashionMods.tsx │ │ │ │ ├── LoadoutItemCategorySection.m.scss │ │ │ │ ├── LoadoutItemCategorySection.m.scss.d.ts │ │ │ │ ├── LoadoutItemCategorySection.tsx │ │ │ │ ├── LoadoutMods.m.scss │ │ │ │ ├── LoadoutMods.m.scss.d.ts │ │ │ │ ├── LoadoutMods.tsx │ │ │ │ ├── LoadoutParametersDisplay.m.scss │ │ │ │ ├── LoadoutParametersDisplay.m.scss.d.ts │ │ │ │ ├── LoadoutParametersDisplay.tsx │ │ │ │ ├── LoadoutSubclassSection.m.scss │ │ │ │ ├── LoadoutSubclassSection.m.scss.d.ts │ │ │ │ ├── LoadoutSubclassSection.tsx │ │ │ │ ├── OptimizerButton.tsx │ │ │ │ ├── PlugDef.tsx │ │ │ │ ├── Sockets.m.scss │ │ │ │ ├── Sockets.m.scss.d.ts │ │ │ │ ├── Sockets.tsx │ │ │ │ ├── menu-hooks.m.scss │ │ │ │ ├── menu-hooks.m.scss.d.ts │ │ │ │ └── menu-hooks.tsx │ │ │ ├── loadouts-selector.ts │ │ │ ├── mod-assignment-drawer/ │ │ │ │ ├── ModAssignmentDrawer.m.scss │ │ │ │ ├── ModAssignmentDrawer.m.scss.d.ts │ │ │ │ ├── ModAssignmentDrawer.tsx │ │ │ │ └── selectors.ts │ │ │ ├── mod-assignment-utils.test.ts │ │ │ ├── mod-assignment-utils.ts │ │ │ ├── mod-permutations.ts │ │ │ ├── mod-utils.ts │ │ │ ├── mutually-exclusive-mods.d.ts │ │ │ ├── plug-drawer/ │ │ │ │ ├── Footer.m.scss │ │ │ │ ├── Footer.m.scss.d.ts │ │ │ │ ├── Footer.tsx │ │ │ │ ├── PlugDrawer.tsx │ │ │ │ ├── PlugSection.m.scss │ │ │ │ ├── PlugSection.m.scss.d.ts │ │ │ │ ├── PlugSection.tsx │ │ │ │ ├── PlugStackableIcon.m.scss │ │ │ │ ├── PlugStackableIcon.m.scss.d.ts │ │ │ │ ├── PlugStackableIcon.tsx │ │ │ │ ├── SelectablePlug.m.scss │ │ │ │ ├── SelectablePlug.m.scss.d.ts │ │ │ │ ├── SelectablePlug.tsx │ │ │ │ └── types.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ ├── spreadsheets.ts │ │ │ └── stats.ts │ │ ├── loadout-analyzer/ │ │ │ ├── analysis.test.ts │ │ │ ├── analysis.ts │ │ │ ├── finding-display.ts │ │ │ ├── hooks.tsx │ │ │ ├── store.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── loadout-builder/ │ │ │ ├── LoadoutBucketDropTarget.m.scss │ │ │ ├── LoadoutBucketDropTarget.m.scss.d.ts │ │ │ ├── LoadoutBucketDropTarget.tsx │ │ │ ├── LoadoutBuilder.m.scss │ │ │ ├── LoadoutBuilder.m.scss.d.ts │ │ │ ├── LoadoutBuilder.tsx │ │ │ ├── LoadoutBuilderContainer.tsx │ │ │ ├── LoadoutBuilderItem.tsx │ │ │ ├── NoBuildsFoundExplainer.m.scss │ │ │ ├── NoBuildsFoundExplainer.m.scss.d.ts │ │ │ ├── NoBuildsFoundExplainer.tsx │ │ │ ├── README.md │ │ │ ├── example-search.ts │ │ │ ├── filter/ │ │ │ │ ├── EnergyOptions.m.scss │ │ │ │ ├── EnergyOptions.m.scss.d.ts │ │ │ │ ├── EnergyOptions.tsx │ │ │ │ ├── ExoticArmorChoice.m.scss │ │ │ │ ├── ExoticArmorChoice.m.scss.d.ts │ │ │ │ ├── ExoticArmorChoice.tsx │ │ │ │ ├── ExoticPicker.m.scss │ │ │ │ ├── ExoticPicker.m.scss.d.ts │ │ │ │ ├── ExoticPicker.tsx │ │ │ │ ├── ExoticTile.m.scss │ │ │ │ ├── ExoticTile.m.scss.d.ts │ │ │ │ ├── ExoticTile.tsx │ │ │ │ ├── LoadoutOptimizerExotic.m.scss │ │ │ │ ├── LoadoutOptimizerExotic.m.scss.d.ts │ │ │ │ ├── LoadoutOptimizerExotic.tsx │ │ │ │ ├── LoadoutOptimizerMenuItems.m.scss │ │ │ │ ├── LoadoutOptimizerMenuItems.m.scss.d.ts │ │ │ │ ├── LoadoutOptimizerMenuItems.tsx │ │ │ │ ├── LoadoutOptimizerSetBonus.m.scss │ │ │ │ ├── LoadoutOptimizerSetBonus.m.scss.d.ts │ │ │ │ ├── LoadoutOptimizerSetBonus.tsx │ │ │ │ ├── LockedItem.tsx │ │ │ │ ├── NewFeaturedGearFilter.m.scss │ │ │ │ ├── NewFeaturedGearFilter.m.scss.d.ts │ │ │ │ ├── NewFeaturedGearFilter.tsx │ │ │ │ ├── TierlessStatConstraintEditor.m.scss │ │ │ │ ├── TierlessStatConstraintEditor.m.scss.d.ts │ │ │ │ └── TierlessStatConstraintEditor.tsx │ │ │ ├── generated-sets/ │ │ │ │ ├── CompareLoadoutsDrawer.m.scss │ │ │ │ ├── CompareLoadoutsDrawer.m.scss.d.ts │ │ │ │ ├── CompareLoadoutsDrawer.tsx │ │ │ │ ├── GeneratedSet.m.scss │ │ │ │ ├── GeneratedSet.m.scss.d.ts │ │ │ │ ├── GeneratedSet.tsx │ │ │ │ ├── GeneratedSetButtons.m.scss │ │ │ │ ├── GeneratedSetButtons.m.scss.d.ts │ │ │ │ ├── GeneratedSetButtons.tsx │ │ │ │ ├── GeneratedSetItem.m.scss │ │ │ │ ├── GeneratedSetItem.m.scss.d.ts │ │ │ │ ├── GeneratedSetItem.tsx │ │ │ │ ├── GeneratedSets.tsx │ │ │ │ ├── SetStats.m.scss │ │ │ │ ├── SetStats.m.scss.d.ts │ │ │ │ ├── SetStats.tsx │ │ │ │ └── utils.ts │ │ │ ├── item-filter.test.ts │ │ │ ├── item-filter.ts │ │ │ ├── loadout-builder-reducer.ts │ │ │ ├── loadout-builder-vendors.ts │ │ │ ├── loadout-params.test.ts │ │ │ ├── loadout-params.ts │ │ │ ├── process/ │ │ │ │ ├── mappers.test.ts │ │ │ │ ├── mappers.ts │ │ │ │ ├── process-wrapper.ts │ │ │ │ └── useProcess.ts │ │ │ ├── process-worker/ │ │ │ │ ├── ProcessWorker.ts │ │ │ │ ├── __snapshots__/ │ │ │ │ │ ├── auto-stat-mod-utils.test.ts.snap │ │ │ │ │ └── process-utils.test.ts.snap │ │ │ │ ├── auto-stat-mod-utils.test.ts │ │ │ │ ├── auto-stat-mod-utils.ts │ │ │ │ ├── process-utils.test.ts │ │ │ │ ├── process-utils.ts │ │ │ │ ├── process.ts │ │ │ │ ├── set-tracker.test.ts │ │ │ │ ├── set-tracker.ts │ │ │ │ ├── tsconfig.json │ │ │ │ └── types.ts │ │ │ ├── types.ts │ │ │ ├── updated-loadout.ts │ │ │ ├── useEquippedHashes.ts │ │ │ └── utils.ts │ │ ├── loadout-drawer/ │ │ │ ├── LoadoutDrawer.m.scss │ │ │ ├── LoadoutDrawer.m.scss.d.ts │ │ │ ├── LoadoutDrawer.tsx │ │ │ ├── LoadoutDrawerContainer.tsx │ │ │ ├── LoadoutDrawerDropTarget.m.scss │ │ │ ├── LoadoutDrawerDropTarget.m.scss.d.ts │ │ │ ├── LoadoutDrawerDropTarget.tsx │ │ │ ├── LoadoutDrawerFooter.m.scss │ │ │ ├── LoadoutDrawerFooter.m.scss.d.ts │ │ │ ├── LoadoutDrawerFooter.tsx │ │ │ ├── LoadoutDrawerHeader.m.scss │ │ │ ├── LoadoutDrawerHeader.m.scss.d.ts │ │ │ ├── LoadoutDrawerHeader.tsx │ │ │ ├── auto-loadouts.ts │ │ │ ├── loadout-apply-state.ts │ │ │ ├── loadout-apply.ts │ │ │ ├── loadout-drawer-reducer.test.ts │ │ │ ├── loadout-drawer-reducer.ts │ │ │ ├── loadout-events.ts │ │ │ ├── loadout-item-conversion.ts │ │ │ ├── loadout-utils.ts │ │ │ └── postmaster.ts │ │ ├── login/ │ │ │ ├── Login.m.scss │ │ │ ├── Login.m.scss.d.ts │ │ │ └── Login.tsx │ │ ├── main.scss │ │ ├── manifest/ │ │ │ ├── actions.ts │ │ │ ├── d1-manifest-service.ts │ │ │ ├── manifest-service-json.ts │ │ │ ├── reducer.ts │ │ │ └── selectors.ts │ │ ├── material-counts/ │ │ │ ├── MaterialCounts.m.scss │ │ │ ├── MaterialCounts.m.scss.d.ts │ │ │ ├── MaterialCounts.tsx │ │ │ ├── MaterialCountsWrappers.m.scss │ │ │ ├── MaterialCountsWrappers.m.scss.d.ts │ │ │ └── MaterialCountsWrappers.tsx │ │ ├── notifications/ │ │ │ ├── Notification.m.scss │ │ │ ├── Notification.m.scss.d.ts │ │ │ ├── Notification.tsx │ │ │ ├── NotificationButton.m.scss │ │ │ ├── NotificationButton.m.scss.d.ts │ │ │ ├── NotificationButton.tsx │ │ │ ├── NotificationsContainer.m.scss │ │ │ ├── NotificationsContainer.m.scss.d.ts │ │ │ ├── NotificationsContainer.tsx │ │ │ └── notifications.ts │ │ ├── organizer/ │ │ │ ├── Columns.m.scss │ │ │ ├── Columns.m.scss.d.ts │ │ │ ├── Columns.tsx │ │ │ ├── CustomStatColumns.tsx │ │ │ ├── DropDown.m.scss │ │ │ ├── DropDown.m.scss.d.ts │ │ │ ├── DropDown.tsx │ │ │ ├── EnabledColumnsSelector.tsx │ │ │ ├── ItemActions.m.scss │ │ │ ├── ItemActions.m.scss.d.ts │ │ │ ├── ItemActions.tsx │ │ │ ├── ItemTable.m.scss │ │ │ ├── ItemTable.m.scss.d.ts │ │ │ ├── ItemTable.tsx │ │ │ ├── ItemTypeSelector.m.scss │ │ │ ├── ItemTypeSelector.m.scss.d.ts │ │ │ ├── ItemTypeSelector.tsx │ │ │ ├── Organizer.m.scss │ │ │ ├── Organizer.m.scss.d.ts │ │ │ ├── Organizer.tsx │ │ │ └── table-types.ts │ │ ├── progress/ │ │ │ ├── ActivityModifier.m.scss │ │ │ ├── ActivityModifier.m.scss.d.ts │ │ │ ├── ActivityModifier.tsx │ │ │ ├── BountyGuide.m.scss │ │ │ ├── BountyGuide.m.scss.d.ts │ │ │ ├── BountyGuide.tsx │ │ │ ├── Event.m.scss │ │ │ ├── Event.m.scss.d.ts │ │ │ ├── Event.tsx │ │ │ ├── FactionIcon.m.scss │ │ │ ├── FactionIcon.m.scss.d.ts │ │ │ ├── FactionIcon.tsx │ │ │ ├── Milestones.m.scss │ │ │ ├── Milestones.m.scss.d.ts │ │ │ ├── Milestones.tsx │ │ │ ├── Objective.m.scss │ │ │ ├── Objective.m.scss.d.ts │ │ │ ├── Objective.tsx │ │ │ ├── Pathfinder.m.scss │ │ │ ├── Pathfinder.m.scss.d.ts │ │ │ ├── Pathfinder.tsx │ │ │ ├── Progress.m.scss │ │ │ ├── Progress.m.scss.d.ts │ │ │ ├── Progress.tsx │ │ │ ├── Pursuit.tsx │ │ │ ├── PursuitGrid.m.scss │ │ │ ├── PursuitGrid.m.scss.d.ts │ │ │ ├── PursuitGrid.tsx │ │ │ ├── PursuitItem.m.scss │ │ │ ├── PursuitItem.m.scss.d.ts │ │ │ ├── PursuitItem.tsx │ │ │ ├── Pursuits.tsx │ │ │ ├── Raid.tsx │ │ │ ├── RaidDisplay.m.scss │ │ │ ├── RaidDisplay.m.scss.d.ts │ │ │ ├── RaidDisplay.tsx │ │ │ ├── Raids.tsx │ │ │ ├── Ranks.tsx │ │ │ ├── ReputationRank.m.scss │ │ │ ├── ReputationRank.m.scss.d.ts │ │ │ ├── ReputationRank.tsx │ │ │ ├── Reward.m.scss │ │ │ ├── Reward.m.scss.d.ts │ │ │ ├── Reward.tsx │ │ │ ├── SeasonalChallenges.tsx │ │ │ ├── SeasonalRank.m.scss │ │ │ ├── SeasonalRank.m.scss.d.ts │ │ │ ├── SeasonalRank.tsx │ │ │ ├── TrackedTriumphs.m.scss │ │ │ ├── TrackedTriumphs.m.scss.d.ts │ │ │ ├── TrackedTriumphs.tsx │ │ │ ├── WellRestedPerkIcon.tsx │ │ │ ├── engrams.ts │ │ │ ├── milestone-items.ts │ │ │ ├── milestone.scss │ │ │ ├── selectors.ts │ │ │ └── xp.ts │ │ ├── records/ │ │ │ ├── Collectible.tsx │ │ │ ├── CollectiblesGrid.m.scss │ │ │ ├── CollectiblesGrid.m.scss.d.ts │ │ │ ├── CollectiblesGrid.tsx │ │ │ ├── Craftable.tsx │ │ │ ├── Metric.m.scss │ │ │ ├── Metric.m.scss.d.ts │ │ │ ├── Metric.tsx │ │ │ ├── MetricBanner.m.scss │ │ │ ├── MetricBanner.m.scss.d.ts │ │ │ ├── MetricBanner.tsx │ │ │ ├── Metrics.m.scss │ │ │ ├── Metrics.m.scss.d.ts │ │ │ ├── Metrics.tsx │ │ │ ├── PresentationNode.m.scss │ │ │ ├── PresentationNode.m.scss.d.ts │ │ │ ├── PresentationNode.tsx │ │ │ ├── PresentationNodeLeaf.tsx │ │ │ ├── PresentationNodeRoot.m.scss │ │ │ ├── PresentationNodeRoot.m.scss.d.ts │ │ │ ├── PresentationNodeRoot.tsx │ │ │ ├── PresentationNodeSearchResults.m.scss │ │ │ ├── PresentationNodeSearchResults.m.scss.d.ts │ │ │ ├── PresentationNodeSearchResults.tsx │ │ │ ├── Record.m.scss │ │ │ ├── Record.m.scss.d.ts │ │ │ ├── Record.tsx │ │ │ ├── Records.m.scss │ │ │ ├── Records.m.scss.d.ts │ │ │ ├── Records.tsx │ │ │ ├── SetCard.m.scss │ │ │ ├── SetCard.m.scss.d.ts │ │ │ ├── SetCard.tsx │ │ │ ├── _set-card.scss │ │ │ ├── catalysts.ts │ │ │ ├── collectible-matching.ts │ │ │ ├── extra-collectibles.d.ts │ │ │ ├── plugset-helpers.ts │ │ │ ├── presentation-nodes.ts │ │ │ ├── selectors.ts │ │ │ └── universal-ornaments/ │ │ │ ├── UniversalOrnaments.m.scss │ │ │ ├── UniversalOrnaments.m.scss.d.ts │ │ │ ├── UniversalOrnaments.tsx │ │ │ └── universal-ornaments.ts │ │ ├── register-service-worker.test.ts │ │ ├── register-service-worker.ts │ │ ├── routes.ts │ │ ├── safari-touch-fix.ts │ │ ├── search/ │ │ │ ├── FilterHelp.m.scss │ │ │ ├── FilterHelp.m.scss.d.ts │ │ │ ├── FilterHelp.tsx │ │ │ ├── HighlightedText.tsx │ │ │ ├── MainSearchBarActions.m.scss │ │ │ ├── MainSearchBarActions.m.scss.d.ts │ │ │ ├── MainSearchBarActions.tsx │ │ │ ├── MainSearchBarMenu.tsx │ │ │ ├── SearchBar.m.scss │ │ │ ├── SearchBar.m.scss.d.ts │ │ │ ├── SearchBar.tsx │ │ │ ├── SearchFilter.tsx │ │ │ ├── SearchHistory.m.scss │ │ │ ├── SearchHistory.m.scss.d.ts │ │ │ ├── SearchHistory.tsx │ │ │ ├── SearchInput.tsx │ │ │ ├── SearchResults.m.scss │ │ │ ├── SearchResults.m.scss.d.ts │ │ │ ├── SearchResults.tsx │ │ │ ├── __snapshots__/ │ │ │ │ ├── autocomplete.test.ts.snap │ │ │ │ ├── query-parser.test.ts.snap │ │ │ │ ├── search-config.test.ts.snap │ │ │ │ └── search-filter.test.ts.snap │ │ │ ├── armory-search.ts │ │ │ ├── autocomplete.test.ts │ │ │ ├── autocomplete.ts │ │ │ ├── d1-known-values.ts │ │ │ ├── d2-known-values.ts │ │ │ ├── filter-description.tsx │ │ │ ├── filter-types.ts │ │ │ ├── items/ │ │ │ │ ├── item-filter-types.ts │ │ │ │ ├── item-search-filter.ts │ │ │ │ └── search-filters/ │ │ │ │ ├── advanced.ts │ │ │ │ ├── d1-filters.ts │ │ │ │ ├── d2-sources.ts │ │ │ │ ├── data/ │ │ │ │ │ └── d2/ │ │ │ │ │ └── artifact-breaker-weapon-types.d.ts │ │ │ │ ├── dupes-deprecated.ts │ │ │ │ ├── dupes.ts │ │ │ │ ├── freeform.ts │ │ │ │ ├── item-infos.ts │ │ │ │ ├── known-values.ts │ │ │ │ ├── loadouts.ts │ │ │ │ ├── perks-set.ts │ │ │ │ ├── range-numeric.ts │ │ │ │ ├── range-overload.ts │ │ │ │ ├── simple.ts │ │ │ │ ├── sockets.ts │ │ │ │ ├── stats.ts │ │ │ │ ├── stores.ts │ │ │ │ └── wishlist.ts │ │ │ ├── loadouts/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── loadout-search-filter.test.ts.snap │ │ │ │ ├── loadout-filter-types.ts │ │ │ │ ├── loadout-search-filter.test.ts │ │ │ │ ├── loadout-search-filter.ts │ │ │ │ └── search-filters/ │ │ │ │ ├── freeform.ts │ │ │ │ ├── range-overload.ts │ │ │ │ └── simple.ts │ │ │ ├── plug-search.ts │ │ │ ├── power-levels.ts │ │ │ ├── query-parser.test.ts │ │ │ ├── query-parser.ts │ │ │ ├── search-config.test.ts │ │ │ ├── search-config.ts │ │ │ ├── search-filter-values.test.ts │ │ │ ├── search-filter-values.ts │ │ │ ├── search-filter.scss │ │ │ ├── search-filter.test.ts │ │ │ ├── search-filter.ts │ │ │ ├── specialty-modslots.ts │ │ │ ├── suggestions-generation.ts │ │ │ └── text-utils.ts │ │ ├── settings/ │ │ │ ├── CharacterOrderEditor.m.scss │ │ │ ├── CharacterOrderEditor.m.scss.d.ts │ │ │ ├── CharacterOrderEditor.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── CustomStatsSettings.m.scss │ │ │ ├── CustomStatsSettings.m.scss.d.ts │ │ │ ├── CustomStatsSettings.tsx │ │ │ ├── LanguageSetting.tsx │ │ │ ├── Select.m.scss │ │ │ ├── Select.m.scss.d.ts │ │ │ ├── Select.tsx │ │ │ ├── SettingsPage.m.scss │ │ │ ├── SettingsPage.m.scss.d.ts │ │ │ ├── SettingsPage.tsx │ │ │ ├── SortOrderEditor.m.scss │ │ │ ├── SortOrderEditor.m.scss.d.ts │ │ │ ├── SortOrderEditor.tsx │ │ │ ├── Spreadsheets.m.scss │ │ │ ├── Spreadsheets.m.scss.d.ts │ │ │ ├── Spreadsheets.tsx │ │ │ ├── Troubleshooting.tsx │ │ │ ├── WishListSettings.m.scss │ │ │ ├── WishListSettings.m.scss.d.ts │ │ │ ├── WishListSettings.tsx │ │ │ ├── actions.ts │ │ │ ├── character-sort.ts │ │ │ ├── hooks.ts │ │ │ ├── initial-settings.ts │ │ │ ├── item-sort.ts │ │ │ ├── settings.ts │ │ │ ├── vault-grouping.test.ts │ │ │ └── vault-grouping.ts │ │ ├── shell/ │ │ │ ├── About.m.scss │ │ │ ├── About.m.scss.d.ts │ │ │ ├── About.tsx │ │ │ ├── AppInstallBanner.m.scss │ │ │ ├── AppInstallBanner.m.scss.d.ts │ │ │ ├── AppInstallBanner.tsx │ │ │ ├── DefaultAccount.tsx │ │ │ ├── Destiny.m.scss │ │ │ ├── Destiny.m.scss.d.ts │ │ │ ├── Destiny.tsx │ │ │ ├── ErrorPanel.m.scss │ │ │ ├── ErrorPanel.m.scss.d.ts │ │ │ ├── ErrorPanel.tsx │ │ │ ├── GATracker.tsx │ │ │ ├── Header.m.scss │ │ │ ├── Header.m.scss.d.ts │ │ │ ├── Header.tsx │ │ │ ├── HeaderWarningBanner.m.scss │ │ │ ├── HeaderWarningBanner.m.scss.d.ts │ │ │ ├── HeaderWarningBanner.tsx │ │ │ ├── LocationSwitcher.tsx │ │ │ ├── MenuBadge.m.scss │ │ │ ├── MenuBadge.m.scss.d.ts │ │ │ ├── MenuBadge.tsx │ │ │ ├── PostmasterWarningBanner.tsx │ │ │ ├── Privacy.m.scss │ │ │ ├── Privacy.m.scss.d.ts │ │ │ ├── Privacy.tsx │ │ │ ├── RefreshButton.m.scss │ │ │ ├── RefreshButton.m.scss.d.ts │ │ │ ├── RefreshButton.tsx │ │ │ ├── ScrollToTop.tsx │ │ │ ├── SneakyUpdates.tsx │ │ │ ├── actions.ts │ │ │ ├── alerts.ts │ │ │ ├── app-install.ts │ │ │ ├── formatters.ts │ │ │ ├── icons/ │ │ │ │ ├── AppIcon.scss │ │ │ │ ├── AppIcon.tsx │ │ │ │ ├── Library.js │ │ │ │ ├── custom/ │ │ │ │ │ ├── Artifice.ts │ │ │ │ │ ├── Engram.ts │ │ │ │ │ ├── Enhanced.ts │ │ │ │ │ ├── Epic.ts │ │ │ │ │ ├── FeaturedBanner.ts │ │ │ │ │ ├── Hunter.ts │ │ │ │ │ ├── HunterProportional.ts │ │ │ │ │ ├── MasterworkHammer.ts │ │ │ │ │ ├── Power.ts │ │ │ │ │ ├── PowerAlt.ts │ │ │ │ │ ├── Shaped.ts │ │ │ │ │ ├── StatBarsIcon.ts │ │ │ │ │ ├── Titan.ts │ │ │ │ │ ├── TitanProportional.ts │ │ │ │ │ ├── TunedStatIcon.ts │ │ │ │ │ ├── Warlock.ts │ │ │ │ │ ├── WarlockProportional.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── font-awesome-icon-variables.scss │ │ │ │ ├── font-awesome.scss │ │ │ │ └── index.ts │ │ │ ├── item-comparators.ts │ │ │ ├── links.ts │ │ │ ├── loading-tracker.ts │ │ │ ├── reducer.ts │ │ │ ├── refresh-events.ts │ │ │ └── selectors.ts │ │ ├── storage/ │ │ │ ├── DimApiSettings.m.scss │ │ │ ├── DimApiSettings.m.scss.d.ts │ │ │ ├── DimApiSettings.tsx │ │ │ ├── DimApiWarningBanner.tsx │ │ │ ├── ImportExport.tsx │ │ │ ├── LocalStorageInfo.m.scss │ │ │ ├── LocalStorageInfo.m.scss.d.ts │ │ │ ├── LocalStorageInfo.tsx │ │ │ ├── export-data.ts │ │ │ ├── human-bytes.ts │ │ │ └── idb-keyval.ts │ │ ├── store/ │ │ │ ├── observerMiddleware.ts │ │ │ ├── reducers.ts │ │ │ ├── store.ts │ │ │ ├── thunk-dispatch.ts │ │ │ └── types.ts │ │ ├── store-stats/ │ │ │ ├── AccountCurrencies.m.scss │ │ │ ├── AccountCurrencies.m.scss.d.ts │ │ │ ├── AccountCurrencies.tsx │ │ │ ├── CharacterStats.m.scss │ │ │ ├── CharacterStats.m.scss.d.ts │ │ │ ├── CharacterStats.tsx │ │ │ ├── ClarityCharacterStat.m.scss │ │ │ ├── ClarityCharacterStat.m.scss.d.ts │ │ │ ├── ClarityCharacterStat.tsx │ │ │ ├── D1CharacterStats.m.scss │ │ │ ├── D1CharacterStats.m.scss.d.ts │ │ │ ├── D1CharacterStats.tsx │ │ │ ├── StatTooltip.m.scss │ │ │ ├── StatTooltip.m.scss.d.ts │ │ │ ├── StatTooltip.tsx │ │ │ ├── StoreStats.m.scss │ │ │ ├── StoreStats.m.scss.d.ts │ │ │ ├── StoreStats.tsx │ │ │ ├── VaultCapacity.m.scss │ │ │ ├── VaultCapacity.m.scss.d.ts │ │ │ └── VaultCapacity.tsx │ │ ├── stream-deck/ │ │ │ ├── OpenOnStreamDeckButton/ │ │ │ │ ├── OpenOnStreamDeckButton.m.scss │ │ │ │ ├── OpenOnStreamDeckButton.m.scss.d.ts │ │ │ │ └── OpenOnStreamDeckButton.tsx │ │ │ ├── StreamDeckButton/ │ │ │ │ ├── StreamDeckButton.m.scss │ │ │ │ ├── StreamDeckButton.m.scss.d.ts │ │ │ │ └── StreamDeckButton.tsx │ │ │ ├── StreamDeckSettings/ │ │ │ │ ├── StreamDeckSettings.m.scss │ │ │ │ ├── StreamDeckSettings.m.scss.d.ts │ │ │ │ └── StreamDeckSettings.tsx │ │ │ ├── actions.ts │ │ │ ├── async-module.ts │ │ │ ├── interfaces.ts │ │ │ ├── msg-handlers.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ ├── stream-deck.ts │ │ │ ├── useStreamDeckSelection.ts │ │ │ └── util/ │ │ │ ├── authorization.ts │ │ │ ├── packager.ts │ │ │ └── version.ts │ │ ├── strip-sockets/ │ │ │ ├── StripSockets.m.scss │ │ │ ├── StripSockets.m.scss.d.ts │ │ │ ├── StripSockets.tsx │ │ │ ├── strip-sockets-actions.ts │ │ │ └── strip-sockets.ts │ │ ├── themes/ │ │ │ ├── _theme-classic.scss │ │ │ ├── _theme-dimdark.scss │ │ │ ├── _theme-europa.scss │ │ │ ├── _theme-neomuna.scss │ │ │ ├── _theme-pyramid.scss │ │ │ ├── _theme-throneworld.scss │ │ │ ├── _theme-vexnet.scss │ │ │ └── _theme.scss │ │ ├── utils/ │ │ │ ├── __snapshots__/ │ │ │ │ └── csv.test.ts.snap │ │ │ ├── action-queue.ts │ │ │ ├── app-badge.ts │ │ │ ├── browsers.ts │ │ │ ├── cancel.ts │ │ │ ├── collections.test.ts │ │ │ ├── collections.ts │ │ │ ├── comparators.ts │ │ │ ├── csv.test.ts │ │ │ ├── csv.ts │ │ │ ├── dim-error.ts │ │ │ ├── download.ts │ │ │ ├── empty.ts │ │ │ ├── errors.ts │ │ │ ├── functions.ts │ │ │ ├── hooks.ts │ │ │ ├── intl.test.ts │ │ │ ├── intl.ts │ │ │ ├── item-utils.ts │ │ │ ├── log.ts │ │ │ ├── measure-memory.ts │ │ │ ├── media-queries.ts │ │ │ ├── memoize.test.ts │ │ │ ├── memoize.ts │ │ │ ├── observable.ts │ │ │ ├── parallel-cores.ts │ │ │ ├── perk-utils.ts │ │ │ ├── plug-descriptions.ts │ │ │ ├── promises.test.ts │ │ │ ├── promises.ts │ │ │ ├── react.ts │ │ │ ├── seasons.ts │ │ │ ├── selectors.ts │ │ │ ├── sentry.ts │ │ │ ├── socket-utils.ts │ │ │ ├── stats-set.test.ts │ │ │ ├── stats-set.ts │ │ │ ├── stats.ts │ │ │ ├── system-info.ts │ │ │ ├── temp-container.scss │ │ │ ├── temp-container.ts │ │ │ ├── textarea-caret.ts │ │ │ ├── time.test.ts │ │ │ ├── time.ts │ │ │ ├── undo-redo-history.test.ts │ │ │ ├── undo-redo-history.ts │ │ │ ├── useWhatChanged.ts │ │ │ └── util-types.ts │ │ ├── vendors/ │ │ │ ├── Cost.m.scss │ │ │ ├── Cost.m.scss.d.ts │ │ │ ├── Cost.tsx │ │ │ ├── Vendor.m.scss │ │ │ ├── Vendor.m.scss.d.ts │ │ │ ├── Vendor.tsx │ │ │ ├── VendorItem.m.scss │ │ │ ├── VendorItem.m.scss.d.ts │ │ │ ├── VendorItemComponent.tsx │ │ │ ├── VendorItems.m.scss │ │ │ ├── VendorItems.m.scss.d.ts │ │ │ ├── VendorItems.tsx │ │ │ ├── Vendors.m.scss │ │ │ ├── Vendors.m.scss.d.ts │ │ │ ├── Vendors.tsx │ │ │ ├── VendorsMenu.m.scss │ │ │ ├── VendorsMenu.m.scss.d.ts │ │ │ ├── VendorsMenu.tsx │ │ │ ├── actions.ts │ │ │ ├── d2-vendors.test.ts │ │ │ ├── d2-vendors.ts │ │ │ ├── focusing-item-outputs.d.ts │ │ │ ├── hooks.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ ├── single-vendor/ │ │ │ │ ├── ArtifactUnlocks.m.scss │ │ │ │ ├── ArtifactUnlocks.m.scss.d.ts │ │ │ │ ├── ArtifactUnlocks.tsx │ │ │ │ ├── SingleVendor.m.scss │ │ │ │ ├── SingleVendor.m.scss.d.ts │ │ │ │ ├── SingleVendor.tsx │ │ │ │ ├── SingleVendorPage.m.scss │ │ │ │ ├── SingleVendorPage.m.scss.d.ts │ │ │ │ ├── SingleVendorPage.tsx │ │ │ │ ├── SingleVendorSheet.m.scss │ │ │ │ ├── SingleVendorSheet.m.scss.d.ts │ │ │ │ ├── SingleVendorSheet.tsx │ │ │ │ ├── SingleVendorSheetContainer.tsx │ │ │ │ └── single-vendor-sheet.ts │ │ │ ├── specialVendorStrings.d.ts │ │ │ └── vendor-item.ts │ │ ├── whats-new/ │ │ │ ├── BungieAlerts.m.scss │ │ │ ├── BungieAlerts.m.scss.d.ts │ │ │ ├── BungieAlerts.tsx │ │ │ ├── ChangeLog.scss │ │ │ ├── ChangeLog.tsx │ │ │ ├── WhatsNew.tsx │ │ │ ├── WhatsNewLink.m.scss │ │ │ ├── WhatsNewLink.m.scss.d.ts │ │ │ ├── WhatsNewLink.tsx │ │ │ └── versions.ts │ │ └── wishlists/ │ │ ├── WishListPerkThumb.m.scss │ │ ├── WishListPerkThumb.m.scss.d.ts │ │ ├── WishListPerkThumb.tsx │ │ ├── actions.ts │ │ ├── observers.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ ├── wishlist-fetch.ts │ │ ├── wishlist-file.test.ts │ │ ├── wishlist-file.ts │ │ └── wishlists.ts │ ├── authReturn.ts │ ├── backup.html │ ├── backup.ts │ ├── browsercheck-utils.js │ ├── browsercheck.js │ ├── browsercheck.test.ts │ ├── browserconfig.xml │ ├── build-browsercheck-utils.js │ ├── bungie-api-ts.d.ts │ ├── data/ │ │ ├── d1/ │ │ │ ├── manifests/ │ │ │ │ ├── d1-manifest-de.json │ │ │ │ ├── d1-manifest-de.json.br │ │ │ │ ├── d1-manifest-en.json │ │ │ │ ├── d1-manifest-en.json.br │ │ │ │ ├── d1-manifest-es.json │ │ │ │ ├── d1-manifest-es.json.br │ │ │ │ ├── d1-manifest-fr.json │ │ │ │ ├── d1-manifest-fr.json.br │ │ │ │ ├── d1-manifest-it.json │ │ │ │ ├── d1-manifest-it.json.br │ │ │ │ ├── d1-manifest-ja.json │ │ │ │ ├── d1-manifest-ja.json.br │ │ │ │ ├── d1-manifest-pt-br.json │ │ │ │ └── d1-manifest-pt-br.json.br │ │ │ └── missing_sources.json │ │ ├── d2/ │ │ │ ├── README.md │ │ │ ├── artifact-breaker-weapon-types.json │ │ │ ├── bad-vendors.json │ │ │ ├── bright-engram-bonus.json │ │ │ ├── bright-engrams.json │ │ │ ├── catalyst-triumph-icons.json │ │ │ ├── craftable-hashes.json │ │ │ ├── crafting-enhanced-intrinsics.ts │ │ │ ├── crafting-mementos.json │ │ │ ├── d2-event-info-v2.ts │ │ │ ├── d2-event-info.ts │ │ │ ├── d2-season-info.ts │ │ │ ├── d2-trials-objectives.json │ │ │ ├── deprecated-mods.json │ │ │ ├── dummy-catalyst-mapping.json │ │ │ ├── empty-plug-hashes.ts │ │ │ ├── energy-mods-change.json │ │ │ ├── energy-mods.json │ │ │ ├── engram-rarity-icons.json │ │ │ ├── events.json │ │ │ ├── exotic-synergy.json │ │ │ ├── exotic-to-catalyst-record.json │ │ │ ├── exotics-with-catalysts.ts │ │ │ ├── extended-breaker.json │ │ │ ├── extended-foundry.json │ │ │ ├── extended-ich.json │ │ │ ├── focusing-item-outputs.json │ │ │ ├── generated-enums.ts │ │ │ ├── ghost-perks.json │ │ │ ├── item-def-workaround-replacements.json │ │ │ ├── legacy-triumphs.json │ │ │ ├── lightcap-to-season.json │ │ │ ├── masterworks-with-cond-stats.json │ │ │ ├── missing-faction-tokens.json │ │ │ ├── missing-source-info.ts │ │ │ ├── mods-with-bad-descriptions.json │ │ │ ├── mutually-exclusive-mods.json │ │ │ ├── objective-richTexts.ts │ │ │ ├── objective-triumph.json │ │ │ ├── powerful-rewards.json │ │ │ ├── pursuits.json │ │ │ ├── raid-mod-plug-category-hashes.json │ │ │ ├── reduced-cost-mod-mappings.ts │ │ │ ├── season-tags.json │ │ │ ├── season-to-source.json │ │ │ ├── seasonal-armor-mods.json │ │ │ ├── seasonal-challenges.json │ │ │ ├── seasons.json │ │ │ ├── seasons_backup.json │ │ │ ├── source-info-v2.ts │ │ │ ├── source-info.ts │ │ │ ├── source-to-season-v2.json │ │ │ ├── sources.json │ │ │ ├── special-vendors-strings.json │ │ │ ├── spider-mats.json │ │ │ ├── spider-purchaseables-to-mats.json │ │ │ ├── subclass-plug-category-hashes.json │ │ │ ├── trait-definition-ids.json │ │ │ ├── trait-to-enhanced-trait.json │ │ │ ├── universal-ornament-aux-sets.json │ │ │ ├── universal-ornament-plugset-hashes.json │ │ │ ├── unreferenced-collections-items.json │ │ │ ├── unstackable-mods.json │ │ │ ├── voice-dim-valid-perks.json │ │ │ ├── watermark-to-event.json │ │ │ ├── watermark-to-season.json │ │ │ └── weapon-from-quest.json │ │ └── font/ │ │ ├── d2-font-glyphs.ts │ │ ├── dim-custom-symbols.ts │ │ └── symbol-name-sources.ts │ ├── earlyErrorReport.js │ ├── fa-subset.js │ ├── global.d.ts │ ├── htaccess │ ├── images/ │ │ └── holofoil-anim.apng │ ├── index.html │ ├── locale/ │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── esMX.json │ │ ├── fr.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── locales.test.ts │ │ ├── pl.json │ │ ├── ptBR.json │ │ ├── ru.json │ │ ├── zhCHS.json │ │ └── zhCHT.json │ ├── nuke.php │ ├── return.html │ ├── robots.txt │ ├── service-worker.ts │ └── testing/ │ ├── data/ │ │ ├── d1profiles-2022-10-24.json │ │ ├── linkedaccounts-2025-07-15.json │ │ ├── profile-2025-12-02.json │ │ └── vendors-2025-12-02.json │ ├── global.d.ts │ ├── jest-setup.cjs │ ├── precache-manifest.test.ts │ ├── test-item-utils.ts │ ├── test-utils.ts │ └── utils/ │ └── i18next.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ # Dependency directories node_modules # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Documentation docs/ # Misc. .vscode/ build/ ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 indent_style = space indent_size = 2 [*.md] trim_trailing_whitespace = false ================================================ FILE: .git-blame-ignore-revs ================================================ # Added trailing commas everywhere 4ecced03d68e8ef1c4addd24ad32d1cde3d393e9 ================================================ FILE: .gitattributes ================================================ # Default core.autocrlf on * text=auto CHANGELOG.md merge=union *.png binary *.jpg binary # Generated files can always be collapsed in diffs *.m.scss.d.ts linguist-generated ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [DestinyItemManager] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: dim # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel custom: # Replace with a single custom sponsorship URL ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug description: File a bug or report an issue labels: [Bug] body: - type: markdown attributes: value: | Thank you for taking the time to fill out a bug report! To save both you and us time, please make sure to check the [FAQ](https://guide.dim.gg/FAQ) and [search issues](https://github.com/DestinyItemManager/DIM/issues?q=is%3Aissue) to see if this has already been reported. - type: input id: DIMversion attributes: label: DIM Version description: Find the version in "About DIM" from the menu (the button with three bars on the upper left of the screen). Is the DIM icon orange, or blue? placeholder: ex. Version 6.78.0.1000630 (beta), built on 8/15/2021, 8:04:14 PM validations: required: true - type: input id: BrowserVersion attributes: label: Browser Details description: What browser and version are you experiencing this issue on? placeholder: ex. Chrome 92.0.4515.131 validations: required: true - type: input id: OSversion attributes: label: OS Details description: What operating system are you experiencing this issue on? placeholder: ex. Windows 10 validations: required: true - type: textarea attributes: label: Describe the bug description: Tell us what's wrong. A great format for bug reports is "I did X, and I expected Y, but instead Z happened." If it helps illustrate the issue, please add screenshots to help explain your problem. validations: required: true - type: textarea attributes: label: Logs description: If you can, open the devtools console (ctrl+shift+J on PC and cmd+option+J on Mac) and paste what you see in the window that pops up render: shell validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature Request description: Let us know what you want to see next in DIM labels: [Enhancement] body: - type: markdown attributes: value: | Thank you for taking the time to fill out a feature request and let us know what you'd like to see in DIM! To save both you and us time, please make sure to check the [Frequently Requested Features](https://guide.dim.gg/Frequently-Requested-Features) and [search issues](https://github.com/DestinyItemManager/DIM/issues?q=is%3Aissue) to see if this has already been reported. - type: textarea id: ChangeDesc attributes: label: Proposed change description: Explain what you'd like to see as a new feature or enhancement to DIM. validations: required: true - type: textarea id: Workflow attributes: label: How does this fit into your workflow? description: Tell us how you'd use this. Having this context is very helpful for us to understand how a proposed change would fit into the way you play. validations: required: true ================================================ FILE: .github/actions/setup-pnpm/action.yml ================================================ name: 'Setup PNPM Environment' description: 'Setup Node.js with PNPM and install dependencies with proper caching' inputs: install: description: 'Whether to install dependencies (default: true)' required: false default: 'true' runs: using: 'composite' steps: - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: ${{ inputs.install == 'true' && 'pnpm' || '' }} - name: Install dependencies if: inputs.install == 'true' shell: bash run: pnpm install --frozen-lockfile --prefer-offline ================================================ FILE: .github/copilot-instructions.md ================================================ # General DIM Code Style Always follow the [Contributing Guide](../docs/CONTRIBUTING.md). ## JS - Pure JavaScript files should be named in lowercase kebab-case, with a .ts extension. These should not use a default export. - Files with JSX should be named in PascalCase with a .tsx extension. In general we should have a separate file for each component. The component should be that module's default export, and the file should have the same name as the component. - Prefer using "function" to declare functions over const arrow functions. ## CSS - All CSS should be put in CSS modules. The CSS module file should have the same name as the file it is used from but with the extension .m.scss. The classes from the module should be imported as `styles` and referenced in JSX. - Do not use inline styles. - Do not use string classNames, only use CSS modules. - Use the clsx helper to combine classNames or conditionally include classes. ## Tests - For unit tests, prefer defining a table of inputs and expected outputs, and then looping through that to create tests. This is also called "table-style testing". - Run `pnpm test -- -u` to update snapshots. This is especially necessary when adding new search terms. - Run `pnpm build:beta` to make sure the code builds. - Run `pnpm test` to run all tests. ## Localization - Only ever add strings to `config/i18n.json`. Then run `pnpm i18n` and commit the updated `src/locale/en.json` file. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - directory: / package-ecosystem: "npm" schedule: interval: "weekly" time: "18:00" # UTC, = ~10AM Pacific groups: major: update-types: - major minor: update-types: - minor - patch - directory: / package-ecosystem: "github-actions" schedule: interval: "weekly" time: "18:00" # UTC, = ~10AM Pacific groups: major: update-types: - major minor: update-types: - minor - patch # Dependabot doesn't support docker-compose yet # - directory: / # package-ecosystem: "docker" # schedule: # interval: "weekly" # time: "18:00" # UTC, = ~10AM Pacific ================================================ FILE: .github/pull_request_template.md ================================================ ================================================ FILE: .github/scripts/discord_changelog.py ================================================ #!/usr/bin/env python3 """ Discord changelog helper for DIMmit. Used by .github/workflows/notify-discord-changelog.yml Usage: python3 discord_changelog.py Environment variables: DISCORD_CHANGELOG_BOT_TOKEN — required (deletes existing beta messages before posting) DISCORD_CHANGELOG_WEBHOOK — required (posts the new changelog embed) """ import json import os import re import sys import time import urllib.request import urllib.error CHANGELOG_FILE = "docs/CHANGELOG.md" CHANNEL_ID = "894808801109245952" BETA_AVATAR_HASH = "5153E66D003AFF489DC73FF9EE151A6F" CHUNK_SIZE = 4000 # safely under Discord's 4096 embed limit REQUEST_TIMEOUT = 10 # seconds ICONS_BASE = "https://raw.githubusercontent.com/DestinyItemManager/DIM/refs/heads/master/icons" AVATAR_BETA = f"{ICONS_BASE}/beta/favicon-96x96.png" AVATAR_PROD = f"{ICONS_BASE}/release/favicon-96x96.png" PROFILES = { "beta": {"avatar_url": AVATAR_BETA, "color": 0x68A0B7}, "prod": {"avatar_url": AVATAR_PROD, "color": 0xF37423}, } # ── Changelog parsing ────────────────────────────────────────────────────────── def _sections(): """Return a dict of {heading: body} parsed from CHANGELOG.md.""" with open(CHANGELOG_FILE) as f: content = f.read() parts = re.split(r"^(## .*)", content, flags=re.MULTILINE) result = {} for i, part in enumerate(parts): if part.startswith("## ") and i + 1 < len(parts): result[part.strip()] = parts[i + 1].strip() return result def detect_profile(sections): """Return 'beta', 'prod', or 'none' based on changelog content.""" beta_body = sections.get("## Next", "") if re.search(r"^\*", beta_body, re.MULTILINE): return "beta" for heading, body in sections.items(): if re.match(r"## \d", heading) and re.search(r"^\*", body, re.MULTILINE): return "prod" return "none" def get_content(sections, profile): """Return formatted Discord content for the given profile.""" if profile == "beta": return "### Destiny Item Manager - BETA\n" + sections.get("## Next", "") if profile == "prod": for heading, body in sections.items(): if re.match(r"## \d", heading): version = re.sub(r"<.*?>", "", heading[3:]).strip() return f"### Destiny Item Manager v{version}\n" + body return "" # ── Discord API helpers ──────────────────────────────────────────────────────── def bot_api(method, path, token, data=None): url = f"https://discord.com/api/v10/{path}" body = json.dumps(data).encode() if data else None req = urllib.request.Request( url, data=body, headers={"Authorization": f"Bot {token}", "Content-Type": "application/json"}, method=method ) try: with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as r: raw = r.read() return json.loads(raw) if raw else None except urllib.error.HTTPError as e: print(f"HTTP {e.code} on {method} {path}: {e.read().decode()}") raise def chunk_content(content): """Split content into <=CHUNK_SIZE chunks on newline boundaries.""" chunks = [] current = "" for line in content.splitlines(keepends=True): if len(current) + len(line) > CHUNK_SIZE: chunks.append(current.rstrip()) current = line else: current += line if current.strip(): chunks.append(current.rstrip()) return chunks # ── Commands ─────────────────────────────────────────────────────────────────── LOOKBACK_DAYS = 8 # releases are weekly; 8 days covers the full beta window def _snowflake_from_days_ago(days): """Return the Discord snowflake ID for a timestamp N days ago. Used as an `after` cursor to avoid scanning the full channel history. """ ms = int((time.time() - days * 86400) * 1000) return str((ms - 1420070400000) << 22) def _bot_api_with_retry(method, path, token, data=None, retries=5): """Call bot_api with exponential backoff on 429 rate-limit responses.""" for attempt in range(retries): try: return bot_api(method, path, token, data) except urllib.error.HTTPError as e: if e.code == 429 and attempt < retries - 1: retry_after = float(json.loads(e.read()).get("retry_after", 1)) print(f"Rate limited, retrying in {retry_after}s...") time.sleep(retry_after) else: raise def delete_beta(): """Delete beta-avatar messages from the last LOOKBACK_DAYS days.""" token = os.environ["DISCORD_CHANGELOG_BOT_TOKEN"] after_id = _snowflake_from_days_ago(LOOKBACK_DAYS) messages = [] last_id = None while True: path = f"channels/{CHANNEL_ID}/messages?limit=100&after={after_id}" if last_id: path += f"&before={last_id}" batch = _bot_api_with_retry("GET", path, token) if not batch: break messages.extend(batch) if len(batch) < 100: break last_id = batch[-1]["id"] deleted = 0 for msg in messages: if msg.get("author", {}).get("avatar", "").upper() == BETA_AVATAR_HASH: _bot_api_with_retry("DELETE", f"channels/{CHANNEL_ID}/messages/{msg['id']}", token) deleted += 1 time.sleep(0.5) # stay under rate limit print(f"Deleted {deleted} beta message(s)") # ── Entrypoint ───────────────────────────────────────────────────────────────── if __name__ == "__main__": sections = _sections() profile = detect_profile(sections) if profile == "none": print("No changelog content detected, skipping Discord post.") sys.exit(0) content = get_content(sections, profile) delete_beta() webhook = os.environ["DISCORD_CHANGELOG_WEBHOOK"] avatar = PROFILES[profile] for chunk in chunk_content(content): payload = { "username": "DIMmit", "avatar_url": avatar["avatar_url"], "content": "", "embeds": [{"description": chunk, "color": avatar["color"]}] } data = json.dumps(payload).encode() req = urllib.request.Request( webhook, data=data, headers={"Content-Type": "application/json"} ) urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) print(f"Posted chunk ({len(chunk)} chars)") ================================================ FILE: .github/scripts/i18n_discord.py ================================================ #!/usr/bin/env python3 """ Discord i18n diff helper for DIMi18n. Used by .github/workflows/notify-discord-i18n.yml Usage: python3 i18n_discord.py Environment variables: DISCORD_I18N_WEBHOOK — required (posts the diff embed) """ import json import os import subprocess import sys import urllib.request import urllib.error LOCALE_FILE = "src/locale/en.json" CHUNK_SIZE = 4000 # safely under Discord's 4096 embed limit REQUEST_TIMEOUT = 10 # seconds ROLE_MENTION = "<@&622449489008918548>" AVATAR_URL = "https://raw.githubusercontent.com/DestinyItemManager/DIM/refs/heads/master/icons/pr/android-chrome-mask-512x512-6-2018.png" COLOR = 0xFF64E7 CROWDIN_URL = "https://crowdin.com/project/destiny-item-manager" # ── JSON diffing ─────────────────────────────────────────────────────────────── def flatten(obj, prefix=""): """Flatten a nested JSON object to dot-notation keys.""" result = {} for key, value in obj.items(): new_key = f"{prefix}.{key}" if prefix else key if isinstance(value, dict): result.update(flatten(value, new_key)) else: result[new_key] = value return result def compare(old_flat, new_flat): """Return a dict of added, modified, and removed keys.""" old_keys = set(old_flat) new_keys = set(new_flat) added = [ {"path": k, "new": new_flat[k]} for k in new_keys - old_keys ] removed = [ {"path": k, "old": old_flat[k]} for k in old_keys - new_keys ] modified = [ {"path": k, "old": old_flat[k], "new": new_flat[k]} for k in old_keys & new_keys if json.dumps(old_flat[k]) != json.dumps(new_flat[k]) ] return { "added": sorted(added, key=lambda x: x["path"]), "modified": sorted(modified, key=lambda x: x["path"]), "removed": sorted(removed, key=lambda x: x["path"]), } # ── Markdown report ──────────────────────────────────────────────────────────── MAX_VALUE_LEN = 100 def fmt(value): """Format a value for display, truncating if needed.""" if value is None: return "`null`" s = f'"{value}"' if isinstance(value, str) else json.dumps(value) if len(s) > MAX_VALUE_LEN: s = s[:MAX_VALUE_LEN] + "..." return f"`{s}`" def generate_report(diff, total): added = diff["added"] modified = diff["modified"] removed = diff["removed"] lines = [ "## Summary\n", f"**Total Changes:** {total}\n", f"- **Added Translation(s):** {len(added)}", f"- **Modified Translation(s):** {len(modified)}", f"- **Removed Translation(s):** {len(removed)}", ] if added: lines.append(f"\n## ➕ Added Keys ({len(added)})\n") for i, c in enumerate(added, 1): lines.append(f"### {i}. `{c['path']}`\n") lines.append(fmt(c["new"])) if modified: lines.append(f"\n## 🔄 Modified Keys ({len(modified)})\n") for i, c in enumerate(modified, 1): lines.append(f"### {i}. `{c['path']}`\n") lines.append(f"<:minus:1105075710818783372> {fmt(c['old'])}") lines.append(f"<:plus:1105075707593371738> {fmt(c['new'])}") if removed: lines.append(f"\n## ➖ Removed Keys ({len(removed)})\n") for i, c in enumerate(removed, 1): lines.append(f"### {i}. `{c['path']}`\n") lines.append(f"**Previous Value:** {fmt(c['old'])}") return "\n".join(lines) # ── Discord posting ──────────────────────────────────────────────────────────── def chunk_content(content): """Split content into <=CHUNK_SIZE chunks on newline boundaries.""" chunks = [] current = "" for line in content.splitlines(keepends=True): if len(current) + len(line) > CHUNK_SIZE: chunks.append(current.rstrip()) current = line else: current += line if current.strip(): chunks.append(current.rstrip()) return chunks def post_to_discord(report): webhook = os.environ["DISCORD_I18N_WEBHOOK"] chunks = chunk_content(report) for i, chunk in enumerate(chunks): payload = { "username": "i18n Bot", "avatar_url": AVATAR_URL, "content": ROLE_MENTION if i == 0 else "", "embeds": [{ "title": "DIM - crowdin", "url": CROWDIN_URL, "description": chunk, "color": COLOR, }] } data = json.dumps(payload).encode() req = urllib.request.Request( webhook, data=data, headers={"Content-Type": "application/json"} ) try: urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) print(f"Posted chunk {i + 1}/{len(chunks)} ({len(chunk)} chars)") except urllib.error.HTTPError as e: print(f"HTTP {e.code}: {e.read().decode()}") raise # ── Entrypoint ───────────────────────────────────────────────────────────────── if __name__ == "__main__": old_json = subprocess.run( ["git", "show", f"HEAD^:{LOCALE_FILE}"], capture_output=True, text=True, check=True ).stdout old = flatten(json.loads(old_json)) with open(LOCALE_FILE) as f: new = flatten(json.load(f)) diff = compare(old, new) total = len(diff["added"]) + len(diff["modified"]) + len(diff["removed"]) if total == 0: print("No changes detected, skipping Discord post.") sys.exit(0) report = generate_report(diff, total) post_to_discord(report) ================================================ FILE: .github/workflows/auto-merge.yml ================================================ name: Auto-merge on: pull_request_target: permissions: contents: write pull-requests: write jobs: auto-merge: runs-on: ubuntu-latest timeout-minutes: 10 if: github.actor == 'd2ai-bot' || github.actor == 'dependabot[bot]' steps: - name: Generate token uses: actions/create-github-app-token@v3 id: app-token with: app-id: ${{ secrets.AUTOMERGE_APP_ID }} private-key: ${{ secrets.AUTOMERGE_PRIVATE_KEY }} - name: Dependabot metadata if: github.actor == 'dependabot[bot]' id: metadata uses: dependabot/fetch-metadata@v2 with: github-token: ${{ steps.app-token.outputs.token }} - name: Enable auto-merge for d2ai PRs if: github.actor == 'd2ai-bot' run: gh pr merge --auto --merge "${{ github.event.pull_request.html_url }}" env: GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Enable auto-merge for Dependabot PRs if: github.actor == 'dependabot[bot]' && (steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor') run: gh pr merge --auto --merge "${{ github.event.pull_request.html_url }}" env: GH_TOKEN: ${{ steps.app-token.outputs.token }} ================================================ FILE: .github/workflows/changelog-updater.yml ================================================ name: Update Changelog on: push: branches: [master] paths-ignore: - 'docs/CHANGELOG.md' # Prevent infinite loops permissions: contents: write jobs: update-changelog: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v6 with: token: ${{ secrets.I18N_PAT }} - uses: ./.github/actions/setup-pnpm with: install: 'false' # Don't need dependencies for this script - name: Update changelog from commits env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} COMMIT_SHAS: ${{ join(github.event.commits.*.id, ' ') }} run: | # Process all commits and automatically detect associated PRs using GitHub API node build/update-changelog.js $COMMIT_SHAS > docs/CHANGELOG.md.new mv docs/CHANGELOG.md.new docs/CHANGELOG.md - name: Commit files uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: Apply changelog updates ================================================ FILE: .github/workflows/copilot-setup-steps.yml ================================================ name: "Copilot Setup Steps" on: workflow_dispatch: push: paths: - .github/workflows/copilot-setup-steps.yml pull_request: paths: - .github/workflows/copilot-setup-steps.yml jobs: copilot-setup-steps: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v5 - name: Setup Node uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: pnpm - name: Install run: pnpm install --frozen-lockfile --prefer-offline ================================================ FILE: .github/workflows/deploy-beta.yml ================================================ name: Deploy Beta on: workflow_dispatch: push: branches: - master # Ensures that only one deploy task per branch/environment will run at a time. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: deploy: runs-on: ubuntu-latest environment: name: beta url: https://beta.dim.gg steps: - uses: actions/checkout@v6 with: fetch-depth: 2 # So sentry can get the previous commit - uses: ./.github/actions/setup-pnpm - name: Install SSH key uses: benoitchantre/setup-ssh-authentication-action@1.0.1 with: private-key: ${{ secrets.SSH_KEY }} private-key-name: dim.rsa known-hosts: ${{ secrets.REMOTE_HOST }} - name: Get package version id: package-version uses: martinbeentjes/npm-get-version-action@v1.3.1 - name: Set beta environment run: | echo "build_level='beta'" >> $GITHUB_ENV - name: Bump release version (beta) env: RUN_NUM: ${{ github.run_number }} run: | echo "VERSION=${{ steps.package-version.outputs.current-version }}.$(($RUN_NUM))" >> $GITHUB_ENV - name: Build run: pnpm build:beta env: NODE_OPTIONS: "--max_old_space_size=8192" WEB_API_KEY: ${{ secrets.BUNGIE_API_KEY }} WEB_OAUTH_CLIENT_ID: ${{ secrets.OAUTH_CLIENT_ID }} WEB_OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_SECRET }} DIM_API_KEY: ${{ secrets.DIM_API_KEY }} - name: Send bundle stats to RelativeCI uses: relative-ci/agent-action@v3 with: key: ${{ secrets.RELATIVE_CI_KEY }} token: ${{ secrets.GITHUB_TOKEN }} webpackStatsFile: ./webpack-stats.json - name: Check Syntax run: pnpm syntax - name: Rsync to Server run: ./build/rsync-deploy.sh env: SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }} SOURCE: "dist/" REMOTE_USER: ${{ secrets.REMOTE_USER }} REMOTE_HOST: ${{ secrets.REMOTE_HOST }} REMOTE_PATH: beta.destinyitemmanager.com - name: Purge CloudFlare cache run: ./build/purge-cloudflare.sh env: CLOUDFLARE_KEY: ${{ secrets.CLOUDFLARE_KEY }} CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} APP_DOMAIN: beta.destinyitemmanager.com - name: Create Sentry release uses: getsentry/action-release@v3 env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: destiny-item-manager SENTRY_PROJECT: dim with: environment: beta release: ${{ env.VERSION }} ignore_missing: true ================================================ FILE: .github/workflows/deploy-prod.yml ================================================ name: Deploy Prod on: workflow_dispatch: inputs: patch: description: 'Should this be a patch release?' type: boolean required: true default: true schedule: # Deploy at 5pm Sunday PST, which is 1am Monday UTC # * is a special character in YAML so you have to quote this string - cron: '0 1 * * 1' jobs: deploy: runs-on: ubuntu-latest environment: name: release url: https://app.dim.gg steps: - uses: actions/checkout@v6 with: fetch-depth: 2 # So sentry can get the previous commit # Use the dim-release-bot token rather than the default token: ${{ secrets.GH_TOKEN }} - uses: ./.github/actions/setup-pnpm - name: Install SSH key uses: benoitchantre/setup-ssh-authentication-action@1.0.1 with: private-key: ${{ secrets.SSH_KEY }} private-key-name: dim.rsa known-hosts: ${{ secrets.REMOTE_HOST }} - name: Build and deploy run: ./build/deploy-prod.sh env: PATCH: ${{ github.event.inputs.patch }} NODE_OPTIONS: "--max_old_space_size=8192" WEB_API_KEY: ${{ secrets.BUNGIE_API_KEY }} WEB_OAUTH_CLIENT_ID: ${{ secrets.OAUTH_CLIENT_ID }} WEB_OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_SECRET }} DIM_API_KEY: ${{ secrets.DIM_API_KEY }} REMOTE_USER: ${{ secrets.REMOTE_USER }} REMOTE_HOST: ${{ secrets.REMOTE_HOST }} REMOTE_PATH: app.destinyitemmanager.com SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} # Use the dim-release-bot token rather than the default GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - name: Purge CloudFlare cache run: ./build/purge-cloudflare.sh env: CLOUDFLARE_KEY: ${{ secrets.CLOUDFLARE_KEY }} CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} APP_DOMAIN: app.destinyitemmanager.com - name: Get package version id: package-version shell: bash run: | echo "current-version=$(node -p 'const pkg = require("./web-console/package.json"); pkg.version')" >> $GITHUB_OUTPUT - name: Create Sentry release uses: getsentry/action-release@v3 env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: destiny-item-manager SENTRY_PROJECT: dim with: environment: release release: ${{ steps.package-version.outputs.current-version }} ignore_missing: true ================================================ FILE: .github/workflows/i18n-bot-download.yml ================================================ # This workflow runs every Saturday @ 1900 UTC (NOON PST) name: i18n download bot on: workflow_dispatch: schedule: - cron: "0 19 * * 6" jobs: download: runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout DIM uses: actions/checkout@v6 with: ref: ${{ github.head_ref }} token: ${{ secrets.I18N_PAT }} - uses: ./.github/actions/setup-pnpm - name: Download updated i18n files uses: crowdin/github-action@v2.16.0 with: upload_sources: false upload_translations: false download_translations: true create_pull_request: false push_translations: false project_id: ${{ secrets.CROWDIN_PROJECT_ID }} token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} env: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Sort locale JSON files run: | allLocales=("en" "de" "es" "esMX" "fr" "it" "ja" "ko" "pl" "ptBR" "ru" "zhCHS" "zhCHT") for lang in ${allLocales[@]}; do jq -S . src/locale/$lang.json > src/locale/sorted_$lang.json && mv src/locale/sorted_$lang.json src/locale/$lang.json done - name: Build browsercheck utils run: pnpm bcu - name: Check for changes uses: dorny/paths-filter@v4 id: filter with: base: HEAD filters: | changed: - '**' - name: Commit files if: steps.filter.outputs.changed == 'true' uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: "i18n: Update translations from Crowdin" commit_user_name: DIM i18n Bot commit_user_email: destinyitemmanager@gmail.com commit_author: DIM i18n Bot ================================================ FILE: .github/workflows/i18n-bot-upload.yml ================================================ # This workflow runs whenever locale/en.json is updated on the master branch # It updates crowdin with new translation strings name: i18n upload bot on: workflow_dispatch: push: paths: - "src/locale/en.json" branches: [master] jobs: upload: runs-on: ubuntu-latest steps: - name: Checkout DIM uses: actions/checkout@v6 - name: Sort en.json run: dimJSON="$(jq -S . src/locale/en.json)" && echo "${dimJSON}" > src/locale/en.json - name: Upload updated en.json to crowdin uses: crowdin/github-action@v2.16.0 with: upload_sources: true upload_translations: false project_id: ${{ secrets.CROWDIN_PROJECT_ID }} token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} env: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Check for changes uses: dorny/paths-filter@v4 id: filter with: base: HEAD filters: | changed: - 'src/locale/en.json' ================================================ FILE: .github/workflows/i18n-update.yml ================================================ name: i18n-update on: push: branches: [master] paths: - 'config/i18n.json' - 'src/locale/en.json' jobs: update: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 3 token: ${{ secrets.I18N_PAT }} - uses: ./.github/actions/setup-pnpm - name: Run i18n update run: pnpm i18n - name: Commit files uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: "i18n: Apply automated updates" ================================================ FILE: .github/workflows/lint-workflows.yml ================================================ name: Lint Workflows on: pull_request: paths: - '.github/workflows/**' - '.github/actions/**' push: branches: - master paths: - '.github/workflows/**' - '.github/actions/**' workflow_dispatch: jobs: actionlint: runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@v6 - name: Run actionlint uses: reviewdog/action-actionlint@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review filter_mode: nofilter fail_level: error actionlint_flags: -color ================================================ FILE: .github/workflows/notify-discord-changelog.yml ================================================ name: Notify Discord - Changelog on: workflow_dispatch: push: branches: - master paths: - 'docs/CHANGELOG.md' jobs: post-changelog: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Post changelog to Discord env: DISCORD_CHANGELOG_BOT_TOKEN: ${{ secrets.DISCORD_CHANGELOG_BOT_TOKEN }} DISCORD_CHANGELOG_WEBHOOK: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }} run: python3 .github/scripts/discord_changelog.py ================================================ FILE: .github/workflows/notify-discord-i18n.yml ================================================ name: Notify Discord - i18n on: workflow_dispatch: push: branches: - master paths: - 'src/locale/en.json' jobs: post-i18n-diff: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 2 # needed to access HEAD^ for the previous version - name: Post i18n diff to Discord env: DISCORD_I18N_WEBHOOK: ${{ secrets.DISCORD_I18N_WEBHOOK }} run: python3 .github/scripts/i18n_discord.py ================================================ FILE: .github/workflows/pr-cleanup.yml ================================================ name: PR Cleanup on: pull_request: types: [closed] jobs: # Remove the preview build directory cleanup: runs-on: ubuntu-latest environment: pr if: ${{github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'}} steps: - name: Install SSH key uses: benoitchantre/setup-ssh-authentication-action@1.0.1 with: private-key: ${{ secrets.SSH_KEY }} private-key-name: dim.rsa known-hosts: ${{ secrets.REMOTE_HOST }} - name: Delete preview build run: ssh -i ~/.ssh/dim.rsa -o StrictHostKeyChecking=no ${{secrets.REMOTE_USER}}@${{secrets.REMOTE_HOST}} "rm -rf pr.destinyitemmanager.com/${{ github.event.number }}" ================================================ FILE: .github/workflows/pr-reports.yml ================================================ name: PR Reports on: workflow_run: workflows: ['PR Validation'] types: [completed] permissions: checks: write pull-requests: write actions: read jobs: # Check all artifacts in a single job to reduce API calls check-artifacts: runs-on: ubuntu-latest timeout-minutes: 5 if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'cancelled' outputs: has-test-results: ${{ steps.check.outputs.test-results }} has-eslint-results: ${{ steps.check.outputs.eslint-results }} has-webpack-stats: ${{ steps.check.outputs.webpack-stats }} steps: - name: Check for all artifacts id: check uses: actions/github-script@v8 with: script: | const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: context.payload.workflow_run.id }); const hasTestResults = artifacts.data.artifacts.some(a => a.name === 'test-results'); const hasEslintResults = artifacts.data.artifacts.some(a => a.name === 'eslint-results'); const hasWebpackStats = artifacts.data.artifacts.some(a => a.name === 'webpack-stats'); console.log(`Test results: ${hasTestResults ? '✅' : '❌'}`); console.log(`ESLint results: ${hasEslintResults ? '✅' : '❌'}`); console.log(`Webpack stats: ${hasWebpackStats ? '✅' : '❌'}`); core.setOutput('test-results', hasTestResults); core.setOutput('eslint-results', hasEslintResults); core.setOutput('webpack-stats', hasWebpackStats); test-report: needs: check-artifacts runs-on: ubuntu-latest timeout-minutes: 10 if: needs.check-artifacts.outputs.has-test-results == 'true' steps: - name: Download test results uses: actions/download-artifact@v8 with: name: test-results github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - name: Publish test report uses: dorny/test-reporter@v3 with: artifact: test-results name: JEST Tests path: '*.xml' reporter: jest-junit eslint-annotate: needs: check-artifacts runs-on: ubuntu-latest timeout-minutes: 10 if: needs.check-artifacts.outputs.has-eslint-results == 'true' steps: - name: Download ESLint results uses: actions/download-artifact@v8 with: name: eslint-results github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - name: Annotate ESLint results uses: ataylorme/eslint-annotate-action@v4 with: report-json: eslint.results.json fail-on-error: false fail-on-warning: false bundle-analysis: needs: check-artifacts runs-on: ubuntu-latest timeout-minutes: 10 if: needs.check-artifacts.outputs.has-webpack-stats == 'true' && github.event.workflow_run.conclusion == 'success' steps: - name: Download webpack stats uses: actions/download-artifact@v8 with: name: webpack-stats run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Upload to RelativeCI uses: relative-ci/agent-action@v3 with: key: ${{ secrets.RELATIVE_CI_KEY }} token: ${{ secrets.GITHUB_TOKEN }} artifactName: webpack-stats webpackStatsFile: ./webpack-stats.json ================================================ FILE: .github/workflows/pr-validation.yml ================================================ name: PR Validation on: pull_request: types: [opened, synchronize, reopened] paths-ignore: - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v6 # Inline setup - required for fork PR compatibility # Cannot use ./.github/actions/setup-pnpm as it won't work from forks - uses: pnpm/action-setup@v5 - name: Setup Node uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline - name: Cache Manifest uses: actions/cache@v5 with: path: manifest-cache key: destiny-manifest-${{ github.run_number }} restore-keys: | destiny-manifest- - name: Run tests run: pnpm test env: CLEAN_MANIFEST_CACHE: true - name: Upload test results uses: actions/upload-artifact@v7 if: success() || failure() with: name: test-results path: junit.xml retention-days: 7 lint: runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v6 # Inline setup - required for fork PR compatibility - uses: pnpm/action-setup@v5 - name: Setup Node uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline # Optimized cache key: Only invalidate on config changes, not all dependencies - name: Cache ESLint uses: actions/cache@v5 with: path: | .eslintcache node_modules/.cache key: ${{ runner.os }}-eslint-${{ hashFiles('eslint.config.js', 'tsconfig.json') }}-${{ hashFiles('src/**/*.ts', 'src/**/*.tsx', 'src/**/*.js', 'src/**/*.jsx') }} restore-keys: | ${{ runner.os }}-eslint-${{ hashFiles('eslint.config.js', 'tsconfig.json') }}- ${{ runner.os }}-eslint- - name: Prettier run: pnpm lint:prettier - name: StyleLint run: pnpm lint:stylelint - name: ESLint id: eslint run: pnpm lint-report:cached continue-on-error: true - name: Upload ESLint results if: always() uses: actions/upload-artifact@v7 with: name: eslint-results path: eslint.results.json if-no-files-found: ignore retention-days: 7 # Annotation happens in separate workflow (pr-reports.yml) # This allows annotations to work for fork PRs too - name: Fail if ESLint has errors if: steps.eslint.outcome == 'failure' run: | pnpm lintcached exit 1 build: runs-on: ubuntu-latest timeout-minutes: 30 environment: name: pr url: ${{ steps.deploy-url.outputs.url }} steps: - uses: actions/checkout@v6 # Inline setup - required for fork PR compatibility - uses: pnpm/action-setup@v5 - name: Setup Node uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile --prefer-offline - name: Get package version id: package-version uses: martinbeentjes/npm-get-version-action@v1.3.1 - name: Set version run: | echo "VERSION=${{ steps.package-version.outputs.current-version }}.${{ github.run_number }}" >> $GITHUB_ENV - name: Build PR version run: pnpm build:pr env: NODE_OPTIONS: "--max_old_space_size=8192" WEB_API_KEY: ${{ secrets.BUNGIE_API_KEY }} WEB_OAUTH_CLIENT_ID: ${{ secrets.OAUTH_CLIENT_ID }} WEB_OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_SECRET }} DIM_API_KEY: ${{ secrets.DIM_API_KEY }} PUBLIC_PATH: /${{ github.event.number }}/ - name: Check for build pipeline updates id: filter uses: dorny/paths-filter@v4 with: filters: | build-pipeline: - package.json - pnpm-lock.yaml - config/webpack.ts - babel.config.cjs - name: Upload webpack stats if: steps.filter.outputs.build-pipeline == 'true' uses: actions/upload-artifact@v7 with: name: webpack-stats path: webpack-stats.json retention-days: 1 - name: Install SSH key if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} uses: benoitchantre/setup-ssh-authentication-action@1.0.1 with: private-key: ${{ secrets.SSH_KEY }} private-key-name: dim.rsa known-hosts: ${{ secrets.REMOTE_HOST }} - name: Check Syntax if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} run: pnpm syntax - name: Deploy to preview if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} run: ./build/rsync-deploy.sh env: SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }} SOURCE: "dist/" REMOTE_USER: ${{ secrets.REMOTE_USER }} REMOTE_HOST: ${{ secrets.REMOTE_HOST }} REMOTE_PATH: pr.destinyitemmanager.com/${{ github.event.number }} - name: Purge CloudFlare cache if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} run: ./build/purge-cloudflare.sh env: CLOUDFLARE_KEY: ${{ secrets.CLOUDFLARE_KEY }} CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} APP_DOMAIN: pr.destinyitemmanager.com/${{ github.event.number }} - name: Set preview URL if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} id: deploy-url run: | echo "url=https://pr.dim.gg/${{ github.event.number }}/" >> $GITHUB_OUTPUT ================================================ FILE: .gitignore ================================================ dist/ .settings/ .idea/ node_modules/ coverage/ *.DS_Store config/dim_travis.rsa cert.pem key.pem ca.crt ca.key .brackets.json npm-debug.log yarn-error.log .eslintcache manifest-cache Heap.* eslint_report.json webpack-stats.json webpack-stats.json.br junit.xml eslint.results.json .pnpm-store /sonda-report.html sonda-report/ .claude/ ================================================ FILE: .gitmodules ================================================ [submodule "destiny-icons"] path = destiny-icons url = https://github.com/justrealmilk/destiny-icons branch = master ================================================ FILE: .husky/pre-commit ================================================ node_modules/.bin/lint-staged ================================================ FILE: .npmrc ================================================ # Tells pnpm to use a shell emulator that works the same on macOS, linux, and Windows shell-emulator=true ================================================ FILE: .nvmrc ================================================ 24 ================================================ FILE: .prettierignore ================================================ Gruntfile.js tools/ dist/ extension/ *.md *.yml *.m.scss.d.ts src/data/** ================================================ FILE: .prettierrc ================================================ { "printWidth": 100, "singleQuote": true, "plugins": ["prettier-plugin-organize-imports"] } ================================================ FILE: .stylelintrc ================================================ { "plugins": [ "stylelint-order" ], "extends": [ "stylelint-config-standard-scss", "stylelint-config-css-modules" ], "rules": { "alpha-value-notation": "number", "shorthand-property-no-redundant-values": null, "rule-empty-line-before": null, "value-keyword-case": null, "comment-empty-line-before": null, "scss/double-slash-comment-empty-line-before": null, "scss/dollar-variable-empty-line-before": null, "scss/comment-no-empty": null, "function-name-case": null, "function-calc-no-unspaced-operator": null, "media-feature-range-notation": null, "declaration-empty-line-before": null, "custom-property-empty-line-before": null, "scss/no-global-function-names": null, "scss/operator-no-newline-after": null, "color-function-notation": "legacy", "at-rule-no-unknown": null, "scss/at-rule-no-unknown": true, "media-feature-name-no-unknown": null, "selector-not-notation": null, "selector-pseudo-class-no-unknown": [ true, { "ignorePseudoClasses": [ "horizontal", "global" ] } ], "hue-degree-notation": "number", "selector-id-pattern": "(^[a-z][a-zA-Z0-9]+$|^([a-z][a-z0-9]*)(-[a-z0-9]+)*$)", "selector-class-pattern": "(^[a-z][a-zA-Z0-9]+$|^([a-z][a-z0-9]*)(-[A-Za-z0-9]+)*$)", "scss/dollar-variable-pattern": "(^[a-z][a-zA-Z0-9]+$|^([a-z][a-z0-9]*)(-[a-z0-9]+)*$)", "value-no-vendor-prefix": [ true, { "ignoreValues": [ "box" ] } ], "color-no-invalid-hex": true, "unit-no-unknown": true, "no-duplicate-selectors": true, "declaration-block-no-duplicate-properties": [ true, { "ignore": [ "consecutive-duplicates-with-different-values" ] } ], "declaration-block-no-redundant-longhand-properties": [ true, { "ignoreShorthands": [ "/grid-template/" ] } ], "scss/function-no-unknown": [ true, { "ignoreFunctions": "/^dim-([a-z][a-z0-9]*)(-[a-z0-9]+)*$/" } ], "scss/at-function-pattern": [ "^dim-([a-z][a-z0-9]*)(-[a-z0-9]+)*$", { "message": "Expected function name to be kebab-case and prefixed with 'dim-'" } ], "scss/load-partial-extension": null, "order/order": [ "custom-properties", "dollar-variables", "declarations", { "type": "at-rule", "name": "include", "hasBlock": false }, { "type": "at-rule", "name": "include", "hasBlock": true }, { "type": "at-rule" }, "rules" ], "declaration-block-no-shorthand-property-overrides": true } } ================================================ FILE: .svgo.yml ================================================ plugins: - name: preset-default params: overrides: removeViewBox: false ================================================ FILE: .vscode/css.json ================================================ { "version": 1.1, "properties": [ { "name": "composes", "description": "css modules compose" } ], "atDirectives": [ { "name": "@value", "description": "css modules value import" } ], "pseudoClasses": [], "pseudoElements": [] } ================================================ FILE: .vscode/dim.code-snippets ================================================ { // Place your global snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. // Placeholders with the same ids are connected. // Example: // "Print to console": { // "scope": "javascript,typescript", // "prefix": "log", // "body": [ // "console.log('$1');", // "$2" // ], // "description": "Log output to console" // } "SFC Component": { "prefix": "sfc", "body": [ "export default function ${1:ClassName}({", "}: {", "}) {", "\treturn (", "\t\t$0", "\t);", "}", "", ], "description": "TypeScript Stateless Functional Component", "scope": "javascriptreact,typescriptreact", }, "import i18next": { "prefix": "imt", "body": ["import { t } from 'app/i18next-t';"], "description": "Import t helper from i18next", "scope": "javascriptreact,typescriptreact", }, "import clsx": { "prefix": "imc", "body": ["import clsx from 'clsx';"], "description": "Import clsx", "scope": "javascriptreact,typescriptreact", }, "import styles": { "prefix": "ims", "body": ["import * as styles from './$TM_FILENAME_BASE.m.scss';"], "description": "Import CSS module", "scope": "javascriptreact,typescriptreact", }, } ================================================ FILE: .vscode/extensions.json ================================================ { // See http://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp "Jacano.vscode-pnpm", "esbenp.prettier-vscode", "streetsidesoftware.code-spell-checker", "dbaeumer.vscode-eslint", "orta.vscode-jest", "stylelint.vscode-stylelint", "amodio.tsl-problem-matcher", "timonwong.shellcheck", "github.vscode-github-actions" ] } ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "preLaunchTask": "start", "name": "Launch Chrome against localhost", "url": "https://localhost:8080/", "webRoot": "${workspaceFolder}" }, { "type": "node", "request": "launch", "name": "Jest All", "program": "${workspaceFolder}/node_modules/.bin/jest", "args": ["--runInBand"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "disableOptimisticBPs": true, "windows": { "program": "${workspaceFolder}/node_modules/jest/bin/jest" } }, { "type": "node", "request": "launch", "name": "Jest Current File", "program": "${workspaceFolder}/node_modules/.bin/jest", "args": ["${fileBasenameNoExtension}", "--config", "jest.config.js"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "disableOptimisticBPs": true, "windows": { "program": "${workspaceFolder}/node_modules/jest/bin/jest" } } ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.tabSize": 2, "editor.trimAutoWhitespace": true, "files.trimTrailingWhitespace": true, "typescript.tsdk": "./node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, "editor.formatOnSave": true, "jest.runMode": "on-demand", "editor.defaultFormatter": "esbenp.prettier-vscode", "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], "search.exclude": { "**/data/d1/manifests": true, "src/testing/data": true, "destiny-icons": true, "manifest-cache": true }, "files.readonlyInclude": { "node_modules/**": true, "src/data/**": true, "destiny-icons/**": true, "src/locale/*": true, "**/*.m.scss.d.ts": true }, "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, // "jest.jestCommandLine": "NODE_ENV=test LOCAL_MANIFEST=true npx jest --verbose", "[javascript]": { "editor.codeActionsOnSave": { "source.organizeImports": "never" } }, "[typescript]": { "editor.codeActionsOnSave": { "source.organizeImports": "never" } }, "[typescriptreact]": { "editor.codeActionsOnSave": { "source.organizeImports": "never" } }, "cSpell.enabledLanguageIds": [ "jsonc", "json", "markdown", "javascript", "scss", "typescript", "typescriptreact", "yaml" ], "css.customData": ["./.vscode/custom.css-data.json"], "css.validate": false, "scss.validate": false, "stylelint.validate": ["css", "scss"], "explorer.fileNesting.enabled": true, "explorer.fileNesting.expand": false, "explorer.fileNesting.patterns": { "*.ts": "${capture}.js, ${capture}.test.ts", "*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.test.js", "*.jsx": "${capture}.js, ${capture}.test.js", "*.tsx": "${capture}.ts, ${capture}.test.ts", "*.m.scss": "${capture}.m.scss.d.ts", "tsconfig.json": "tsconfig.*.json", "package.json": "package-lock.json, pnpm-lock.yaml" }, "eslint.lintTask.options": "src", "[javascript][typescript][typescriptreact]": { "editor.codeActionsOnSave": { "source.organizeImports": "never" } } } ================================================ FILE: .vscode/tasks.json ================================================ { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "label": "start", "type": "shell", "command": "pnpm", "args": ["start"], "isBackground": true, "problemMatcher": ["$ts-checker-webpack", "$ts-checker-eslint-webpack"] } ] } ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2018 Destiny Item Manager Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Crowdin](https://badges.crowdin.net/destiny-item-manager/localized.svg)](https://crowdin.com/project/destiny-item-manager) [![OpenCollective](https://opencollective.com/dim/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/dim/sponsors/badge.svg)](#sponsors) ### [Launch DIM](https://app.destinyitemmanager.com) ## Destiny Item Manager Destiny Item Manager (DIM) lets [Destiny](http://destinythegame.com/) game players easily move items between their Guardians and the Vault. DIM's goal is to let players equip their guardians quickly. Our Loadouts feature accomplishes this by removing manual steps needed when transferring items. Loadouts give players the ability to define sets of items that they want on their Guardians. When a loadout is selected, DIM will move all of the items referenced by the Loadout to a Guardian. If the item was equipped by another guardian, the Loadouts feature will replace that item with a similar item, if possible, to allow the Loadout referenced item to be transferred. With a single click of a button, you can have a PVP, PVE, or Raid-ready guardian. DIM is based on the same services used by the Destiny Companion app to move and equip items. DIM will not be able to dismantle any of your items. Visit [Discord](https://discordapp.com/invite/UK2GWC7) for updates and more details. ## Bugs and feature requests Have a bug or a feature request? Please first search for [existing and closed issues](https://github.com/DestinyItemManager/DIM/issues). If your problem or idea is not addressed yet, please open a new issue. ## Backers [Support us](https://opencollective.com/dim#backer) with a one-time or monthly donation and help us continue our active development. ## Sponsors Become a sponsor and get your logo here with a link to your site. ## Translation If you speak a language other than English that Destiny supports, a great way to help with DIM development is to provide translations. See [translation guide](https://github.com/DestinyItemManager/DIM/blob/master/docs/TRANSLATIONS.md) for more info on how to help. ## Contributing See [CONTRIBUTING.md](https://github.com/DestinyItemManager/DIM/blob/master/docs/CONTRIBUTING.md) for information on how to Contribute to the development of DIM. ## License Code released under the [MIT license](http://choosealicense.com/licenses/mit/). ================================================ FILE: babel.config.cjs ================================================ const coreJSPackage = require('core-js/package.json'); module.exports = function (api) { const isProduction = api.env('production'); const isTest = api.env('test'); const plugins = [ // Statically optimize away clsx functions 'babel-plugin-optimize-clsx', // Improve performance by turning large objects into JSON.parse 'object-to-json-parse', [ '@babel/plugin-transform-runtime', { useESModules: !isTest, }, ], ]; if (isProduction) { plugins.push( // Optimize React components at the cost of some memory by automatically // factoring out constant/inline JSX fragments '@babel/plugin-transform-react-constant-elements', // This transform is not compatible with React 19 // '@babel/plugin-transform-react-inline-elements', ); } else { if (!isTest) { plugins.push('react-refresh/babel'); } // In dev, compile TS with babel plugins.push(['@babel/plugin-transform-typescript', { isTSX: true, optimizeConstEnums: true }]); } const corejs = { version: coreJSPackage.version }; const presetEnvOptions = { bugfixes: true, modules: false, loose: true, useBuiltIns: 'usage', corejs, shippedProposals: true, // Set to true and run `pnpm build:beta` to see what plugins and polyfills are being used debug: false, // corejs includes a bunch of polyfills for behavior we don't use or bugs we don't care about exclude: [ // Really edge-case bugfix for Array.prototype.push and friends 'es.array.push', 'es.array.unshift', // This fixes an obscure Webkit bug that we don't care about 'es.map.group-by', // Remove this if we start using proposed set methods like .intersection /^es(next)?\.set/, // Remove this if we start using iterator-helpers (which would be nice!) /^es(next)?\.iterator\.(?!concat)/, // Not sure what exactly this is, but we have our own error-cause stuff 'es.error.cause', // Only used when customizing JSON parsing w/ a "reviver" 'esnext.json.parse', // Edge-case bugfixes for URLSearchParams.prototype.has, delete, and size /^web\.url-search-params/, // Unneeded mis-detected DOMException extension 'web.dom-exception.stack', // Not needed in worker context 'web.self', // Mis-detected by usage of Array.prototype.at 'es.string.at-alternative', // We're not doing weird stuff with structured clone 'web.structured-clone', ], }; if (isTest) { presetEnvOptions.targets = { node: 'current' }; presetEnvOptions.modules = 'auto'; } return { presets: [ ['@babel/preset-env', presetEnvOptions], [ '@babel/preset-react', { useBuiltIns: true, loose: true, corejs, runtime: 'automatic', useSpread: true, }, ], ], plugins, // https://babeljs.io/docs/en/assumptions assumptions: { noDocumentAll: true, noClassCalls: true, setPublicClassFields: true, setSpreadProperties: true, }, }; }; ================================================ FILE: build/deploy-prod.sh ================================================ #!/bin/sh -exu git config --global user.email "destinyitemmanager@gmail.com" git config --global user.name "DIM Release Bot" if [ "$PATCH" = 'true' ]; then VERSION=$(npm version patch --no-git-tag-version | sed 's/v//') else VERSION=$(npm version minor --no-git-tag-version | sed 's/v//') fi awk '/## Next/{flag=1;next}/##/{flag=0}flag' docs/CHANGELOG.md >release-notes.txt # update changelog OPENSPAN='\' CLOSESPAN='\<\/span\>' DATE=$(TZ="America/Los_Angeles" date +"%Y-%m-%d") perl -i'' -pe"s/^## Next/## Next\n\n## $VERSION $OPENSPAN($DATE)$CLOSESPAN/" docs/CHANGELOG.md # Add these other changes to the version commit git add -u git commit -m"$VERSION" git tag "v$VERSION" # build and check VERSION=$VERSION pnpm build:release pnpm syntax # rsync the files onto the remote host using SSH keys ./build/rsync-deploy.sh # push tags and changes git push --tags origin master:master # publish a release on GitHub gh release create "v$VERSION" -F release-notes.txt -t "$VERSION" ================================================ FILE: build/purge-cloudflare.sh ================================================ #!/bin/bash -eux set -o pipefail # Set variables CLOUDFLARE_EMAIL, CLOUDFLARE_KEY, and APP_DOMAIN # Purge the cache in CloudFlare for long-lived files curl -X POST "https://api.cloudflare.com/client/v4/zones/2c34c69276ed0f6eb2b9e1518fe56f74/purge_cache" \ -H "X-Auth-Email: $CLOUDFLARE_EMAIL" \ -H "X-Auth-Key: $CLOUDFLARE_KEY" \ -H "Content-Type: application/json" \ --data '{"files":["https://'"$APP_DOMAIN"'", "https://'"$APP_DOMAIN"'/index.html", "https://'"$APP_DOMAIN"'/version.json", "https://'"$APP_DOMAIN"'/service-worker.js", "https://'"$APP_DOMAIN"'/return.html", "https://'"$APP_DOMAIN"'/.well-known/assetlinks.json", "https://'"$APP_DOMAIN"'/.well-known/apple-app-site-association", "https://'"$APP_DOMAIN"'/manifest-webapp.json"]}' ================================================ FILE: build/rsync-deploy.sh ================================================ #!/bin/sh -exu REMOTE_SHELL="ssh -i ~/.ssh/dim.rsa -o StrictHostKeyChecking=no" # Sync everything but the HTML first, so it's ready to go rsync dist/ "$REMOTE_USER"@"$REMOTE_HOST":"$REMOTE_PATH" --rsh "$REMOTE_SHELL" --recursive --exclude=*.html --exclude=*.html.br --exclude=service-worker.js --exclude=service-worker.js.br --exclude=version.json --exclude=version.json.br --verbose # Then sync the HTML which will start using the new content rsync dist/*.html dist/*.html.br dist/service-worker.js dist/service-worker.js.br dist/version.json dist/version.json.br "$REMOTE_USER"@"$REMOTE_HOST":"$REMOTE_PATH" --rsh "$REMOTE_SHELL" --verbose ================================================ FILE: build/test-changelog.js ================================================ #!/usr/bin/env node // Simple test for the changelog update logic import { readFileSync, unlinkSync, writeFileSync } from 'fs'; // Test data - mock GitHub commits const testCommits = [ { id: 'abc123', message: 'Fix bug in loadout optimizer\n\nChangelog: Fixed crash in Loadout Optimizer when using +Artifice option', author: { name: 'Test User' }, }, { id: 'def456', message: 'Add new feature\n\nChangelog: Added tier-level pips to item icons\nChangelog: Fixed some engrams not looking like engrams', author: { name: 'Test User' }, }, { id: 'ghi789', message: 'Regular commit without changelog entry', author: { name: 'Test User' }, }, ]; // Create test commits.json writeFileSync('commits.json', JSON.stringify(testCommits, null, 2)); // Create a test changelog const testChangelog = `## Next * DIMmit is back, for all your changelog notifications. ## 8.82.2 (2025-07-22) * Fix an item inspection crash in D1. ## 8.82.1 (2025-07-21) * Some other changes here.`; // Backup the original changelog const originalChangelog = readFileSync('docs/CHANGELOG.md', 'utf8'); writeFileSync('docs/CHANGELOG.md.backup', originalChangelog); // Write test changelog writeFileSync('docs/CHANGELOG.md', testChangelog); console.log('Running changelog update test...'); // Import and run the main script try { // Since we can't easily import the other script, let's just test the expected behavior console.log('Test commits:'); testCommits.forEach((commit) => { console.log(` - ${commit.id}: ${commit.message.split('\n')[0]}`); }); console.log('\nExpected changelog entries to be extracted:'); const expectedEntries = [ 'Fixed crash in Loadout Optimizer when using +Artifice option', 'Added tier-level pips to item icons', 'Fixed some engrams not looking like engrams', ]; expectedEntries.forEach((entry) => { console.log(` * ${entry}`); }); console.log('\nTest setup complete. You can now run:'); console.log(" echo './commits.json' | node build/update-changelog.js"); console.log(' # or:'); console.log(' cat commits.json | node build/update-changelog.js'); console.log('\nTo restore original changelog:'); console.log(' mv docs/CHANGELOG.md.backup docs/CHANGELOG.md'); } catch (error) { console.error('Test failed:', error.message); } finally { // Restore original changelog writeFileSync('docs/CHANGELOG.md', originalChangelog); // Clean up test files try { unlinkSync('commits.json'); unlinkSync('docs/CHANGELOG.md.backup'); } catch (e) { // Ignore cleanup errors } } ================================================ FILE: build/update-changelog.js ================================================ #!/usr/bin/env node import { readFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // GitHub API functions async function fetchCommitFromAPI(owner, repo, sha, token) { const url = `https://api.github.com/repos/${owner}/${repo}/commits/${sha}`; const response = await fetch(url, { headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${token}`, 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'DIM-Changelog-Updater', }, }); if (!response.ok) { throw new Error(`Failed to fetch commit ${sha}: ${response.status} ${response.statusText}`); } return await response.json(); } async function fetchAssociatedPRsFromGraphQL(owner, repo, sha, token) { const query = ` query associatedPRs($sha: String!, $repo: String!, $owner: String!) { repository(name: $repo, owner: $owner) { commit: object(expression: $sha) { ... on Commit { associatedPullRequests(first: 5) { edges { node { title number body merged } } } } } } } `; const variables = { sha, repo, owner }; const response = await fetch('https://api.github.com/graphql', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': 'DIM-Changelog-Updater', }, body: JSON.stringify({ query, variables }), }); if (!response.ok) { throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`); } const result = await response.json(); if (result.errors) { throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`); } // Extract the PRs from the GraphQL response const commit = result.data?.repository?.commit; if (!commit?.associatedPullRequests?.edges) { return []; } return commit.associatedPullRequests.edges.map((edge) => edge.node).filter((pr) => pr.merged); // Only return merged PRs } function extractChangelogEntries(commits) { const entries = []; if (!Array.isArray(commits)) { console.error('Commits is not an array:', typeof commits); return entries; } for (const commit of commits) { if (!commit || typeof commit.commit?.message !== 'string') { console.error('Invalid commit object:', commit); continue; } // Use the full commit message from the API response const message = commit.commit.message; const lines = message.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.toLowerCase().startsWith('changelog:')) { // Extract text after "Changelog:" and trim whitespace const changelogText = trimmedLine.substring(10).trim(); if (changelogText) { entries.push(changelogText); } } } } return entries; } function extractChangelogEntriesFromText(text, source = 'unknown') { const entries = []; if (typeof text !== 'string') { console.error(`Invalid text from ${source}:`, typeof text); return entries; } const lines = text.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.toLowerCase().startsWith('changelog:')) { // Extract text after "Changelog:" and trim whitespace const changelogText = trimmedLine.substring(10).trim(); if (changelogText) { console.error(`Found changelog entry from ${source}: ${changelogText}`); entries.push(changelogText); } } } return entries; } function updateChangelog(entries, originalChangelog) { if (entries.length === 0) { // No entries to add, return original content unchanged return originalChangelog; } // Find the "## Next" section const nextSectionRegex = /^## Next\s*$/m; const nextMatch = originalChangelog.match(nextSectionRegex); if (!nextMatch) { console.error('Could not find "## Next" section in CHANGELOG.md'); process.exit(1); } // Find the position after the "## Next" line const nextSectionIndex = nextMatch.index + nextMatch[0].length; // Look for the next section (next "##" header) or end of file const afterNextSection = originalChangelog.substring(nextSectionIndex); const nextHeaderMatch = afterNextSection.match(/^## /m); let insertPosition; let existingContent = ''; if (nextHeaderMatch) { // There's another section after "## Next" const nextHeaderIndex = nextSectionIndex + nextHeaderMatch.index; existingContent = originalChangelog.substring(nextSectionIndex, nextHeaderIndex).trim(); insertPosition = nextHeaderIndex; } else { // "## Next" is the last section existingContent = originalChangelog.substring(nextSectionIndex).trim(); insertPosition = originalChangelog.length; } // Format new entries as bullet points const newEntries = entries.map((entry) => `* ${entry}`).join('\n'); // Build the new content for the "## Next" section let newNextContent = ''; if (existingContent) { // Preserve existing content and add new entries newNextContent = `\n${existingContent}\n${newEntries}\n\n`; } else { // No existing content, just add new entries newNextContent = `\n${newEntries}\n\n`; } // Construct the new changelog content const newChangelogContent = originalChangelog.substring(0, nextSectionIndex) + newNextContent + originalChangelog.substring(insertPosition); return newChangelogContent; } async function fetchCommitsFromAPI(commitShas, githubToken, githubRepository) { const [owner, repo] = githubRepository.split('/'); const commits = []; for (const sha of commitShas) { if (sha.trim()) { try { const commit = await fetchCommitFromAPI(owner, repo, sha.trim(), githubToken); commits.push(commit); console.error( `Fetched commit ${sha.substring(0, 7)}: ${commit.commit.message.split('\n')[0]}`, ); } catch (error) { console.error(`Failed to fetch commit ${sha}: ${error.message}`); } } } return commits; } async function main() { try { // Get commit SHAs from command line arguments const commitShas = process.argv.slice(2); if (commitShas.length === 0) { console.error('No commit SHAs provided'); process.exit(1); } // Get GitHub token and repository from environment const githubToken = process.env.GITHUB_TOKEN; const githubRepository = process.env.GITHUB_REPOSITORY; if (!githubToken) { console.error('GITHUB_TOKEN environment variable is required'); process.exit(1); } if (!githubRepository) { console.error('GITHUB_REPOSITORY environment variable is required'); process.exit(1); } const [owner, repo] = githubRepository.split('/'); console.error(`Processing ${commitShas.length} commit SHAs...`); // Fetch commits from GitHub API const commits = await fetchCommitsFromAPI(commitShas, githubToken, githubRepository); console.error(`Successfully fetched ${commits.length} commits`); // Extract changelog entries from commit messages let changelogEntries = extractChangelogEntries(commits); // For each commit, check for associated PRs using GraphQL const processedPRs = new Set(); // Avoid duplicate PR processing for (const sha of commitShas) { if (sha.trim()) { try { console.error(`Checking for associated PRs for commit ${sha.substring(0, 7)}...`); const associatedPRs = await fetchAssociatedPRsFromGraphQL( owner, repo, sha.trim(), githubToken, ); for (const pr of associatedPRs) { // Only process each PR once if (!processedPRs.has(pr.number)) { processedPRs.add(pr.number); console.error(`Found associated PR #${pr.number}: ${pr.title}`); const prEntries = extractChangelogEntriesFromText(pr.body || '', `PR #${pr.number}`); changelogEntries = changelogEntries.concat(prEntries); } } } catch (error) { console.error(`Failed to fetch associated PRs for commit ${sha}: ${error.message}`); } } } console.error(`Processed ${processedPRs.size} unique associated PRs`); // Read the current changelog const changelogPath = join(__dirname, '..', 'docs', 'CHANGELOG.md'); let changelog = readFileSync(changelogPath, 'utf8'); // Update the changelog content if (changelogEntries.length > 0) { changelog = updateChangelog(changelogEntries, changelog); } // Output the updated changelog to stdout process.stdout.write(changelog); console.error(`Successfully processed ${changelogEntries.length} changelog entries:`); changelogEntries.forEach((entry) => console.error(` * ${entry}`)); } catch (error) { console.error('Error processing commits:', error.message); process.exit(1); } } main(); ================================================ FILE: config/.well-known/android-config.beta.json ================================================ [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app", "package_name": "com.destinyitemmanager.beta.twa", "sha256_cert_fingerprints": [ "2C:73:F4:D4:FB:D3:48:87:73:D0:34:54:82:D8:4F:32:0E:F7:37:71:23:6C:F1:4E:AC:0F:7A:83:71:E5:11:55" ] } } ] ================================================ FILE: config/.well-known/android-config.json ================================================ [ { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app", "package_name": "com.destinyitemmanager.app", "sha256_cert_fingerprints": [ "48:9B:BD:A6:8F:4C:DE:F3:35:83:2F:4B:3A:BC:85:0A:F1:D8:FE:6D:62:E7:53:83:B5:E1:86:11:89:50:E4:B6", "E8:7B:18:E2:6E:24:12:1C:A3:D3:3D:1A:C2:7C:39:3A:D9:9B:33:95:8D:42:AD:79:B7:80:F5:24:05:69:F1:7D", "1E:4C:58:AD:FF:D6:3A:E1:BD:89:C1:39:46:8C:1B:C0:06:19:0A:EE:67:0D:BB:85:F3:DE:1E:3D:6B:C6:DC:59", "B6:43:A6:AD:81:A3:42:E0:F5:39:D2:C8:AF:79:08:1A:4D:80:B5:85:B1:A3:95:37:0F:37:42:FE:BD:AE:AE:4A" ] } }, { "relation": [ "delegate_permission/common.handle_all_urls" ], "target": { "namespace": "android_app", "package_name": "gg.dim.app", "sha256_cert_fingerprints": [ "45:1D:42:60:22:60:32:8B:71:61:99:28:EF:BE:D3:EC:19:D8:24:E0:B6:81:11:85:CA:24:68:C3:BF:74:A4:B5" ] } } ] ================================================ FILE: config/.well-known/apple-config.json ================================================ { "applinks": { "details": [ { "appIDs": ["P7G764E7RR.com.destinyitemmanager.app"], "components": [ { "#": "no_universal_links", "exclude": true, "comment": "Matches any URL with a fragment that equals no_universal_links and instructs the system not to open it as a universal link." }, { "/": "*", "comment": "Matches any URL." } ] } ] } } ================================================ FILE: config/content-security-policy.ts ================================================ import builder from 'content-security-policy-builder'; import { type FeatureFlags } from './feature-flags.ts'; const SELF = "'self'"; /** * Generate a Content Security Policy directive for a particular DIM environment (beta, release) */ export default function csp( env: 'release' | 'beta' | 'dev' | 'pr', featureFlags: FeatureFlags, version: string | undefined, ) { const baseCSP: Record = { defaultSrc: ["'none'"], scriptSrc: [ SELF, 'https://*.googletagmanager.com', 'https://*.google-analytics.com', // OpenCollective backers 'https://opencollective.com', ], workerSrc: [SELF], styleSrc: [ SELF, // For our inline styles "'unsafe-inline'", // Google Fonts 'https://fonts.googleapis.com/', ], connectSrc: [ SELF, // Google Analytics 'https://*.google-analytics.com', 'https://*.analytics.google.com', 'https://*.googletagmanager.com', // Bungie.net API 'https://www.bungie.net', // Sentry featureFlags.sentry && 'https://sentry.io/api/279673/', // Wishlists featureFlags.wishLists && 'https://raw.githubusercontent.com', featureFlags.wishLists && 'https://gist.githubusercontent.com', // DIM Sync 'https://api.destinyitemmanager.com', // Clarity featureFlags.clarityDescriptions && 'https://database-clarity.github.io', // Stream Deck Plugin featureFlags.elgatoStreamDeck && 'ws://localhost:9120', featureFlags.elgatoStreamDeck && 'http://localhost:9120', // Game2Give featureFlags.issueBanner && 'https://bungiefoundation.donordrive.com', ].filter((s) => s !== false), imgSrc: [ SELF, // Webpack inlines some images 'data:', // Bungie.net images 'https://www.bungie.net', // Google analytics tracking 'https://*.google-analytics.com', 'https://*.googletagmanager.com', // OpenCollective backers 'https://opencollective.com', ], fontSrc: [ SELF, 'data:', // Google Fonts 'https://fonts.gstatic.com', ], childSrc: [SELF], frameSrc: [ // OpenCollective backers 'https://opencollective.com', // Mastodon feed 'https://www.mastofeed.com/apiv2/feed', ], prefetchSrc: [SELF], objectSrc: SELF, // Web app manifest manifestSrc: SELF, }; // Turn on CSP reporting to sentry.io on beta only if (featureFlags.sentry && env === 'beta') { baseCSP.reportUri = `https://sentry.io/api/279673/csp-report/?sentry_key=1367619d45da481b8148dd345c1a1330&sentry_environment=${env}`; if (version) { baseCSP.reportUri += `&sentry_release=${version}`; } } return builder({ directives: baseCSP, }); } ================================================ FILE: config/cspell/bungie-dict.txt ================================================ +s bnet Bungie Calus cooldown Crota* crownofsorrow cryptarchs daito España Eververse exlusive Felwinter* fotl Fynch Gahlran gardenofsalvation Gjallarhorn grimoire Hadium hakke Ikora infusable* Intrinsics Italiano Karn LFR* mechanica México Mida Nessus omolon PGCR Português Rahool reforgeable Shaxx suros Ticuu Tyra Variks vaultofglass veist vowofthedisciple wotm wrathofthemachine Xur Zavala Русский Telesto Neomuna ================================================ FILE: config/cspell/dim-dict.txt ================================================ ARGH bois curations dedupe deduping deduplicating dequip dequipped dequipping dequips DIM DTR dupelower elgato equippability equippable favorited hashta holofoil ICHs indimloadout iningameloadout inleftchar inloadout inloadouts inmiddlechar inpostmaster inrightchar invault itemname itemtype janky lightgg loadout merch misdetection needsxp notexample notransfer onwrongclass reacquirable recalc recents regen reimplemented Repurchasable reqs reusables showxp stackable swappable tshirt unacquired Uncollectible unequip unequippable unequipped unexclude unfavorited uninstanced unselectable untrack untracked wayyyyy Xbox xpcomplete xpincomplete ================================================ FILE: config/cspell/dim-username-dict.txt ================================================ 48klocs bhollis delphiactual iihavetoes JFLAY lowlines Mercules pandapaxxy tehdaw vthorn ================================================ FILE: config/cspell/programming-dict.txt ================================================ asyncs brotli caniuse canonicalization canonicalize ceaser clsx Cruver destructurable divs Dont falsyness frecency gstatic immer indexeddb initi18n Interp keyi18n KHTML klass lngs Metadatas mkcert mkdir msapplication mstile nums oanimationend Ofint32 Ofint64 onbeforeinstallprompt onoffswitch outl pmmmwh popperjs rbpage React's reauth rsync Scroller Spawnfx SVGs swipable tappable UniFFFD unmount unparse upgradeiOS uuidv4 vpadding vxvy wasm xxhash ================================================ FILE: config/dim_travis.rsa.pub ================================================ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCiwgjtkq2QrAjJnK2EYfbkSGYTLIaJfQ2ILZ79ntBIti2ZXIvagbabOjjiirfIJrtN2j4zT0/UExGsUwUZMVsHU04jeLlg9xNioRfRw1NrMz/cVR29W8nYKnkxQhdMjN8Ss7iOcYULZHoC1g77IeHLih79+t5Utc0dWU9CSHDQ4hFk7ImkWsLvnLw43PuzJ0/rhe1966o1H4kTsxGNs6GkjB2b/3EVyO6h5w6Y2AMQ/BHRR2/FHW8PFkH+UFJ1UfLVhfXG4dl5JQP57TGlIckB7pI9QkfV6f6lmmJlMDlnV+krfSJ4E7rLClU3V+KJlNLe/qFfmXiLIEyqAA+A7Uw0Zz5tc2v0AXjusZ+IweadZeApqS+jykU5ZvpGlPNpeTaA2T/D3lQ8DqNFV9YQ2NBIajexMEQpe0j777Va88mNVhOdcnpSfHwTuX+kQgfgmngiJ2YpXcI5G5ZyfGWgEug569XNw4d7CdO3iVEE3cS14XUSrA074B4g9rI/0Raew8q1L7tdaJwHtftkVP+vszM0b3CnkKGb9VLtecXHq7O6qOGNQnmDnN9zErFa2yG0RJtkfmfhf65NZsoCMJJjnYVsVQr135XKrQn9QAoxXR/4CKDwC3QLG4eEKVdljhCllA2xhWhOPOE9c0aVtjTphI21KKG3P0fAQgCzRa9mJSPtUw== brh@legion.local ================================================ FILE: config/feature-flags.ts ================================================ /** * Return a set of compile time feature flags. These values will be inlined into * the code at build time, based on the version of the app being built. This * will then allow Webpack/Terser to fully remove code if the feature flag is * off. We build features behind these feature flags so we can easily remove * them from the app, or keep them in beta/dev for a longer time without * releasing to app. */ export function makeFeatureFlags(env: { release: boolean; beta: boolean; dev: boolean; pr: boolean; }) { return { // Print debug info to console about item moves debugMoves: !env.release, // Debug Service Worker debugSW: !env.release, // Send exception reports to Sentry.io on beta/prod only sentry: !env.dev && !env.pr, // Community-curated wish lists wishLists: true, // Show a banner for supporting a charitable cause issueBanner: false, // Show the triage tab in the item popup triage: true, // Advanced Write Actions (inserting mods) awa: process.env.USER === 'brh', // Only Ben has the keys... // Item feed sidebar itemFeed: true, // Clarity perk descriptions clarityDescriptions: true, // Elgato Stream Deck integration elgatoStreamDeck: true, // Warn when DIM Sync is off and you save some DIM-specific data warnNoSync: true, // Expose the "Automatically add stat mods" Loadout Optimizer toggle loAutoStatMods: true, // Pretend that Bungie.net is down for maintenance simulateBungieMaintenance: false, // Pretend that Bungie.net is not returning sockets info simulateMissingSockets: false, // Request the PresentationNodes component only needed during // Solstice to associate each character with a set of triumphs. // Solstice 2022 had a set of challenges for each character, // while Solstice 2023 had shared progress/challenges, so maybe // this won't be needed going forward? solsticePresentationNodes: false, // not ready to turn these on but the code is there customStatWeights: false, // On the Loadouts page, run Loadout Optimizer to find better tiers for loadouts. runLoInBackground: true, // Whether to allow setting in-game loadout identifiers on DIM loadouts. editInGameLoadoutIdentifiers: false, // Whether to sync DIM API data instead of loading everything dimApiSync: true, // Whether to show the "New Items" dot newItems: false, }; } export type FeatureFlags = ReturnType; ================================================ FILE: config/i18n.json ================================================ { "AWA": { "ConfirmDescription": "Please use the Destiny 2 Companion App to approve DIM to modify your items.", "ConfirmTitle": "Confirm Action", "Error": "Error changing mods or perks", "ErrorMessage": "We couldn't equip {{plug}} into {{item}}.\n\n{{error}}", "FailedToken": "Couldn't get permission to change item", "IrreversiblePlugging": "You don't own {{plug}}, so we won't overwrite it.", "NotSupported": "We cannot change this type of mod or perk" }, "Accounts": { "Choose": "Profiles for {{bungieName}}", "Title": "Accounts", "NoCharacters": "You have no Destiny characters associated with this Bungie.net account. Try logging into a different account.", "ErrorLoading": "Unable to load Destiny accounts from Bungie.net", "MissingAccountWarning": "If you don't see your account here, you may not have logged in to the right Bungie.net account, or Bungie.net may be down for maintenance.", "MissingTitle": "Account Not Found", "MissingDescription": "The account you're trying to view is not an account linked to your Bungie.net profile. Select one of your accounts below.", "ErrorLoadInventory": "Unable to load your Destiny {{version}} characters and inventory", "ErrorLoadManifest": "Unable to load Destiny info database from Bungie", "SwitchAccounts": "You can switch accounts later from the menu in the header.", "NoCharactersTitle": "No Characters Found" }, "Activities": { "Activities": "Activities", "Hard": "Hard", "Nightfall": "Nightfall Strike", "Normal": "Normal", "WeeklyHeroic": "Weekly Heroic Strike" }, "Armory": { "AlternateItems": "Alternate Versions", "Armory": "Armory", "DifferentSeason": "Reissue from a different season", "OpenInArmory": "view in Armory", "WishlistedRolls_one": "Wishlisted Roll", "WishlistedRolls_other": "{{count, number}} Wishlisted Rolls", "TrashlistedRolls_one": "Trashlisted Roll", "TrashlistedRolls_other": "{{count, number}} Trashlisted Rolls", "NoNotes": "No Notes", "YourItems": "Your Items", "Unknown": "Unknown Item", "Season": "Season {{season}}, Year {{year}}", "UnknownPerkHash": "The perk hash {{hash}} ({{perkName}}) does not appear on this item, so this wish list roll is invalid. Please contact the wish list author to correct this. Note that wish lists should always specify the non-enhanced version of perks." }, "Browsercheck": { "Unsupported": "The DIM team does not support using this browser. Some or all DIM features may not work.", "Steam": "The Steam overlay browser is very old and some or all DIM features may not work. We cannot provide support for it.", "Samsung": "Samsung Internet can make sites look too dark when dark mode is on. Enable Settings > Labs > Use website dark theme or switch to another browser." }, "Bucket": { "Armor": "Armor", "Class": "Subclass", "General": "General", "Ghost": "Ghost", "Inventory": "Inventory", "Postmaster": "Postmaster", "Progress": "Progress", "Reputation": "Reputation", "Unknown": "Unknown", "Vault": "Vault", "Weapons": "Weapons" }, "BulkNote": { "Title_one": "Change notes for 1 item", "Title_other": "Change notes for {{count}} items", "Replace": "Replace notes", "Append": "Append to notes / add #hashtags", "Remove": "Remove from notes / remove #hashtags", "Confirm": "Update Notes" }, "BungieAlert": { "Title": "A message from Bungie:" }, "BungieService": { "AppNotPermitted": "DIM does not have permission to perform this action.", "DestinyLegacyPlatform": "Bungie's services currently have a bug that prevents DIM from loading info for your Destiny 2 account if you played Destiny 1 on a last-gen console. Bungie will fix this soon, but until then you must play Destiny 1 on a current-gen console to be able to access your info.", "DevVersion": "Are you running a development version of DIM? You must register your chrome extension with Bungie.net.", "Difficulties": "Bungie.net is currently experiencing difficulties.", "ErrorTitle": "Bungie.net Error", "ItemUniquenessExplanation": "A character can only have one of '{{name}}' on it.", "Maintenance": "Bungie.net servers are down for maintenance.", "MissingInventory": "Bungie.net did not return your inventory, possibly because your privacy settings prevent it. Try logging out and logging back in.", "DestinyCannotPerformActionAtThisLocation": "You cannot equip items or change mods while in an activity. Try heading to orbit or a social area. This is a limitation of the Bungie.net API, not DIM.", "DestinyItemUnequippable": "You cannot equip this item. If this character's last activity locked their equipment, try logging into the character again.", "NetworkError": "Network error - {{status}} {{statusText}}", "NoAccount": "No Destiny account was found. Do you have the right platform selected?", "NoAccountForPlatform": "Failed to find a Destiny account for you on {{platform}}.", "NotConnected": "You may not be connected to the internet.", "NotConnectedOrBlocked": "You may not be connected to the internet, or an ad blocking or privacy extension may be blocking Bungie.net.", "NotLoggedIn": "Please authorize DIM in order to use this app.", "SlowResponse": "Bungie.net was too slow to respond.", "Slow": "Bungie.net is slow right now", "SlowDetails": "Bungie.net is taking a long time to return your information. This can happen when a lot of players are in the game at once, or if Bungie.net is having issues. You also might be having an internet connection issue. We'll keep waiting for a response.", "Throttled": "Bungie.net is limiting how many requests DIM can make.", "Twitter": "Get status updates on:", "UnknownError": "Bungie.net message: {{message}}", "VendorNotFound": "Vendor data is unavailable." }, "Compare": { "NoModArmor": "Pre-mods", "Button": "Compare", "Archetype": "Archetype", "ButtonHelp": "Compare Items", "CompareBaseStats": "Show Base Stats", "BaseStatsDescription": "Base stats, without Masterwork or Mods", "AssumeMasterworked": "Assume Masterworked", "AssumeMasterworkedDescription": "Stats if fully Masterworked, without current Mods", "CurrentStats": "Current Stats", "CurrentStatsDescription": "Current stats, including Mods and Masterwork level", "InitialItem": "This is the item the Compare tool was launched from", "IsVendorItem": "This item is not in your inventory, but {{vendorName}} sells it.", "Error": { "Unmatched": "This item doesn't match the type of items being compared.", "Invalid": "There are no valid items for comparison." } }, "Cooldown": { "Grenade": "Grenade cooldown: {{cooldown}}", "Melee": "Melee cooldown: {{cooldown}}", "Super": "Super cooldown: {{cooldown}}" }, "Countdown": { "Days_one": "1 Day", "Days_compact_one": "{{count}}d", "Days_compact_other": "{{count}}d", "Days_other": "{{count}} Days" }, "Csv": { "EmptyFile": "There were no rows in the file.", "ImportConfirm": "Are you sure you want to import tags/notes from CSV? This will overwrite tags/notes for all items contained in your spreadsheet.", "ImportFailed": "Failed to import tags/notes from CSV: {{error}}", "ImportSuccess_one": "Tags/notes loaded for one item.", "ImportSuccess_other": "Tags/notes loaded for {{count}} items.", "ImportWrongFileType": "File is not a CSV file.", "WrongFields": "CSV must have 'Id', 'Notes', 'Tag', and 'Hash' columns." }, "Dialog": { "Cancel": "Cancel", "OK": "OK" }, "EnergyMeter": { "Energy": "Energy", "Used": "Used", "Unused": "Unused", "UpgradeNeeded": "This item's current energy capacity is {{energyCapacity}}. To fit the selected mods, its energy capacity must be {{energyUsed}}." }, "ErrorBoundary": { "Title": "Something went wrong" }, "ErrorPanel": { "BrowserTooOldTitle": "Incompatible browser", "BrowserTooOld": "Your browser is too old to use DIM. Please update your browser to the latest version.", "Description": "Try loading your inventory in the Destiny 2 Companion App to see if Bungie.net is down.", "Troubleshooting": "Troubleshooting Guide", "SystemDown": "This affects all Destiny apps, and the DIM team cannot fix or bypass it.", "ReadTheGuide": "Read our User Guide (linked from the menu) for troubleshooting steps." }, "FarmingMode": { "Vault": "It will move items to the vault to make room.", "D2Desc_one": "DIM is preventing items from going to the Postmaster by making sure there's always one empty space per item type on {{store}}.", "D2Desc_female_one": "DIM is preventing items from going to the Postmaster by making sure there's always one empty space per item type on {{store}}.", "D2Desc_male_one": "DIM is preventing items from going to the Postmaster by making sure there's always one empty space per item type on {{store}}.", "D2Desc_other": "DIM is preventing items from going to the Postmaster by making sure there's always {{count}} empty spaces per item type on {{store}}.", "D2Desc_female_other": "DIM is preventing items from going to the Postmaster by making sure there's always {{count}} empty spaces per item type on {{store}}.", "D2Desc_male_other": "DIM is preventing items from going to the Postmaster by making sure there's always {{count}} empty spaces per item type on {{store}}.", "Desc_one": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping one empty space open per item type to prevent anything from going to the Postmaster.", "Desc_female_one": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping one empty space open per item type to prevent anything from going to the Postmaster.", "Desc_male_one": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping one empty space open per item type to prevent anything from going to the Postmaster.", "Desc_other": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping {{count}} spaces open per item type to prevent anything from going to the Postmaster.", "Desc_female_other": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping {{count}} spaces open per item type to prevent anything from going to the Postmaster.", "Desc_male_other": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping {{count}} spaces open per item type to prevent anything from going to the Postmaster.", "FarmingMode": "Farming Mode", "FarmingModeNote": "(maintain space for drops)", "MakeRoom": { "Desc": "DIM is moving only Engram and Glimmer items from {{store}} to the vault or other characters to prevent anything from going to the Postmaster.", "Desc_female": "DIM is moving only Engram and Glimmer items from {{store}} to the vault or other characters to prevent anything from going to the Postmaster.", "Desc_male": "DIM is moving only Engram and Glimmer items from {{store}} to the vault or other characters to prevent anything from going to the Postmaster.", "MakeRoom": "Make room to pick up items by moving equipment", "Tooltip": "If checked, DIM will move weapons and armor around to make space in the vault for engrams." }, "OutOfRoom": "You're out of space to move items off of {{character}}. Time to clear out the trash!", "OutOfRoomTitle": "Out of Room", "Stop": "Stop" }, "FashionDrawer": { "Accept": "Save fashion", "CannotFitOrnament": "This item does not have an ornament socket or you have no ornaments for it.", "CannotFitShader": "This item cannot fit a shader", "ClearOrnaments": "Clear Ornaments", "ClearOrnamentsTitle": "Reset all ornaments to \"no preference\"", "ClearShaders": "Clear Shaders", "ClearShadersTitle": "Reset all shaders to \"no preference\"", "NoPreference": "No preference - this socket won't be changed", "Reset": "Clear fashion", "Sync": "Sync", "SyncOrnaments": "Sync Ornaments", "SyncOrnamentsTitle": "Use ornaments from the same set on all items, if they're unlocked", "SyncShaders": "Sync Shaders", "SyncShadersTitle": "Use the same shader on all items", "Title": "Choose shaders and ornaments", "UseEquipped": "Use equipped fashion" }, "FileUpload": { "Instructions": "Click or drag files" }, "LoadoutFilter": { "Contains": "Shows loadouts which have an item or a mod matching the filter text. Search for items with spaces in their name using quotes.", "Name": "Shows loadouts whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.", "Notes": "Search for loadouts by their notes field.", "PartialMatch": "Shows loadouts where their name or notes has a partial match to the filter text. Search for entire phrases using quotes.", "Season": "Shows loadouts by which season of Destiny 2 they were last modified in.", "FashionOnly": "Shows loadouts that contain only fashion (shaders or ornaments).", "ModsOnly": "Shows loadouts that only contain armor mods.", "Subclass": "Shows loadouts whose subclass name or damage type partially matches the filter text.", "LoadoutLight": "Shows loadouts based on their calculated light level. Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits." }, "Filter": { "Adept": "\\(Adept\\)", "AmmoType": "Shows items based on their ammo type.", "ArmorCategory": "Shows armors based on their category.", "ArmorIntrinsic": "Shows legendary armor which has an intrinsic perk, such as Artifice Armor.", "Artifice": "Shows Artifice armor.", "Ascended": "Shows items that have an ascend node which have been ascended.", "Unascended": "Shows items that have an ascend node which have not been ascended.", "Breaker": "Filter by breaker type or corresponding champion type. breaker:instrinsic shows items with intrinsic breaker ability.", "BulkClear_one": "Removed tag from 1 item.", "BulkClear_other": "Removed tags from {{count}} items.", "BulkRevert_one": "Reverted tag on 1 item.", "BulkRevert_other": "Reverted tags on {{count}} items.", "BulkTag_one": "Tagged selected item as {{tag}}.", "BulkTag_other": "Tagged {{count}} selected items as {{tag}}.", "Catalyst": "Shows catalysts based on their status. catalyst:complete shows catalysts you have completed and applied, catalyst:incomplete shows catalysts you have unlocked but either not completed the objective or applied the catalyst, and catalyst:missing shows items that can have a catalyst but you haven't found it yet.", "Class": "Shows items based on their class affinity.", "Combine": "Filters can be combined or grouped with parentheses, \"or\" and \"and\" to narrow down your search, for example \"{{example}}\".", "ContributePower": "Shows items that have power and can contribute to your power level.", "Craftable": "Shows items that are craftable.", "CraftedDupe": "Shows duplicate weapons where at least one of the duplicates is crafted.", "Curated": "Shows items that are a curated roll.", "CurrentClass": "Shows items that are equippable on the currently logged in guardian.", "DamageType": "Shows items based on their damage type.", "Deepsight": "Shows weapons with Deepsight Resonance, which can have their pattern extracted, or which can have Deepsight Resonance enabled using a Deepsight Harmonizer.", "Deprecated": "This filter is no longer supported.", "Description": "Description", "DescriptionFilter": "Shows items whose description has a partial match to the filter text. Search for entire phrases using quotes.", "RetiredPerk": "Shows weapons with perks that no longer obtainable.", "Dupe": "Shows duplicate items, including reissues", "DupeCount": "Items that have the specified number of duplicates.", "DupeLower": "Duplicate items, including reissues, that are not the highest power dupe. Only one duplicate is chosen as the highest, and the rest are considered lower.", "DupePerks": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "DupeTraits": "Weapons whose traits are either a duplicate of, or a subset of, another weapon of the same type.", "DupeStats": "Shows armor with identical base stats, and matching stat adjustment mods like Artifice or Tuners.", "DupeUntunedStats": "Groups armor with identical base stats, ignoring stat adjustment mods.", "DupeTunedStat": "Groups armor with the same Tuned stat.", "DupeArchetype": "Groups armor with the same stat Archetype.", "DupeTertiary": "Groups armor with the same tertiary stat.", "DupeSetBonus": "Groups armor with the same set bonus.", "DupeZeroStats": "Groups armor with the same 3 non-zero base stats.", "Energy": "Shows items that use the Armor 2.0 mod system introduced in Shadowkeep.", "EnergyCapacity": "Shows items based on their current energy capacity.", "Armor3": "Shows items that use the Armor 3.0 stat system introduced in Edge of Fate.", "Engrams": "Shows engrams.", "Enhanceable": "Shows weapons that can be enhanced.", "Enhanced": "Shows weapons based on their enhancement tier.", "EnhancedPerk": "Shows weapons that have the specified number of enhanced perk columns.", "EnhancementReady": "Shows weapons that have reached level thresholds for perk enhancement.", "Equipment": "Items that can be equipped.", "Equipped": "Items that are currently equipped on a character.", "Event": "Shows items from which event in Destiny 2 they appeared in.", "ExtraPerk": "Shows random-rolled Legendary weapons with an additional selectable perk.", "Featured": "Items that count as one of the \"New Gear\" or \"Featured Items\" in the current season.", "Filter": "Filter", "FilterWith": "Filter with:", "Focusable": "Shows items that can be focused at a vendor", "Foundry": "Shows items by which foundry created them.", "Glimmer": "Shows items that are consumables that are related to gaining glimmer.", "Memento": "Shows weapons that have a memento socket.", "InfusionFodder": "Shows items that could be infused into lower-power versions of the same item for only glimmer.", "HasNotes": "Show items that have notes applied.", "HasShader": "Shows items that have a shader applied.", "HasOrnament": "Shows items that have an ornament applied.", "Harrowed": "\\(Harrowed\\)", "InLoadout": "is:inloadout shows items that are included in any loadout. Searching with inloadout: shows items that are included in loadouts with matching titles. When used with a hashtag, inloadout: shows items whose loadouts have the hashtag in the title or notes. When used with a range, it shows items that are in that many loadouts.", "InInGameLoadout": "is:iningameloadout shows items that are included in any in-game loadout.", "InDimLoadout": "is:indimloadout shows items that are included in any DIM loadout.", "Infusable": "Shows items that can be infused.", "IsAdept": "Shows weapons compatible with Adept mods.", "IsCrafted": "Shows weapons that have been crafted.", "ItemId": "Shows the item with the given inventory item ID. For advanced users.", "ItemHash": "Shows the items with the given inventory item hash. For advanced users.", "Leveling": { "Complete": "{{term}} - shows items that are totally complete - every upgrade unlocked.", "Incomplete": "{{term}} - shows items that are not complete - there's still at least one upgrade to unlock.", "NeedsXP": "{{term}} - shows items that can still have XP put into them.", "Upgraded": "{{term}} - shows items that have enough XP to unlock all their nodes, but not all the nodes have been unlocked.", "XPComplete": "{{term}} - shows items that cannot have XP put into them (whether or not their upgrades have been unlocked)." }, "Location": "Shows items based on their location within the app. left/middle/right are the visual location of the char, and while inleftchar will always work, the other two are based on how many characters you have. current is your last/current logged char (that is marked with a yellow triangle).", "LockAllFailed": "Failed to lock items", "LockAllSuccess": "Locked {{num}} items", "Locked": "Shows items based on their locked status.", "Masterwork": "Shows items based on their masterwork stat or masterwork level.", "MasterworkKills": "Shows items based on their masterwork kill tracker count.", "MaxPowerLoadout": "Shows the items in the loadout that would maximize your Power Level for each character class.", "MaxPower": "Shows the items at the highest power per slot.", "Mods": { "Y3": "Shows items with any mods applied." }, "DisabledModSlot": "Shows items with a disabled mod.", "ModSlot": "Shows armor with a specific mod type slot.", "Name": "Shows items whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.", "NamedStat": "Shows armor that has points in the named stat.", "Negate": "To negate a search, prefix that search term with a minus sign or the word \"not\", for example \"{{notexample}}\" or \"{{notexample2}}\".", "NewItems": "Shows new items.", "Notes": "Search for items that you have tagged with custom notes.", "OriginTrait": "Shows weapons that have an origin trait perk.", "Ornament": "Shows items with ornaments and filters for their status.", "InInventory": "Shows items that you have at least one copy of in your inventory. Only really useful in the Vendors and Records screens.", "PartialMatch": "Shows items where their name, description, any perk, or any mod has a partial match to the filter text. Search for entire phrases using quotes.", "PatternUnlocked": "Shows items that have a crafting pattern unlocked, even if the item itself isn't crafted.", "Perk": "Shows items where one of their perks or mods has a partial match to the filter text in their name or description. Search for entire phrases using quotes.", "PerkName": "Shows items with a perk or mod whose name matches (exactperk:) or partially matches (perkname:) the filter text. Search for entire phrases using quotes.", "Postmaster": "Items that are currently in the Postmaster.", "PowerLevel": "Shows items based on their power level. $t(Filter.PowerKeywords)", "PowerKeywords": "Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.", "PowerfulReward": "Shows pursuits which produce a powerful reward.", "PinnacleReward": "Shows pursuits which produce a pinnacle reward.", "PrismaticDamageType": "Shows items based on if they are a light or darkness damage type. Light types are arc, solar, and void. Darkness types are stasis and strand.", "Quality": "Shows items based on their total stat quality percentage. '{{percentage}}' is an alias for '{{quality}}'.", "RandomRoll": "Shows items that drop with random rolls.", "RarityTier": "Shows items based on their rarity tier.", "Reforgeable": "Shows items that can be reforged at the Gunsmith.", "Release": "Shows items available from a specific release or event.", "Holofoil": "Shows holofoil weapons.", "Weapon": "Shows items that are weapons.", "Armor": "Shows items that are armor.", "Cosmetic": "Shows items that are flair or cosmetic.", "RequiredLevel": "Shows items based on their required level.", "SearchPrompt": "Search available filter commands", "Season": "Shows items from which season of Destiny 2 they appeared in.", "StackLevel": "Shows items based on the quantity of items in its stack.", "Stackable": "Shows items that can stack (ammo synths, strange coin, etc)", "StackFull": "Show items which are at-capacity for their stack (Enhancement Cores, Strange Coins, Gunsmith Materials etc)", "StatLower": "Shows armor whose stats are strictly lower than another of the same type of armor.", "CustomStatLower": "Shows armor whose stats are strictly lower than another of the same type of armor, only taking into account stats that are in any of that class' custom stat total list.", "Stats": "Shows items based on a specific stat value. $t(Filter.StatsExtras)", "StatsBase": "Filters armor based on its base stat value, not including attached mods or masterworking. $t(Filter.StatsExtras)", "StatsExtras": "Supports stat addition by connecting multiple stat names with the + or & symbol. There are also special keywords highest, secondhighest, thirdhighest, etc. which match stats based on their rank within an item's stats. Each custom stats also has its own search term, shown in the Custom Stats settings.", "StatsLoadout": "Finds a set of items to equip for the maximum total value of a specific stat.", "StatsOrdinal": "Finds armor 3.0 with the specified stat focusing.", "StatsMax": "Finds armor with the highest number for a specific stat. Includes all items with the highest stat.", "Tags": { "Tag": "Shows items that have a specific tag.", "Tagged": "Shows items that have any tag." }, "Tier": "Shows items based on their tier from 0-5.", "Timelost": "\\(Timelost\\)", "Tracked": "Shows quests/bounties based on their tracked status.", "Transferable": "Items that can be moved between characters.", "Trashlist": "Shows items that match your wish list's trash list.", "TunedStat": "Shows items with tuning mods for the specified stat.", "Undo": "Undo", "UnlockAllFailed": "Failed to unlock items", "UnlockAllSuccess": "Unlocked {{num}} items", "Vendor": "Item is available from a specific vendor.", "VendorItem": "Item is from a vendor, not in your inventory. Useful for excluding vendor items from Loadout Optimizer.", "WeaponType": "Shows weapons based on their weapon type.", "WeaponLevel": "Shows weapons based on their Weapon Level.", "Wishlist": "Shows items that match your wish list.", "WishlistDupe": "Shows duplicate items where at least one of the duplicates is on your wish list.", "WishlistNotes": "Shows wish listed items whose notes match the search.", "WishlistUnknown": "Shows items with no roll recommendations in the loaded wish lists.", "WishlistEnabled": "Shows items that are eligible to have wish list rolls.", "Year": "Shows items from which year of Destiny they appeared in." }, "General": { "ClickForDetails": "Click for details", "Confirm": "Confirm?", "UserGuideLink": "User guide", "Close": "Close" }, "Glyphs": { "Missing": "Missing", "Axe": "Axe", "DarkAbility": "Darkness Ability", "LightAbility": "Light Ability", "Gilded": "Gilded", "Harmonic": "Harmonic", "HiveSword": "Hive Sword", "LightLevel": "Light Level", "Prismatic": "Prismatic", "RespawnRestricted": "Respawn Restricted", "Smoke": "Smoke", "Misadventure": "Misadventure", "Quickfall": "Quickfall", "ScorchCannon": "Scorch Cannon", "OpenSymbolsPicker": "Open Symbols Picker", "SearchSymbols": "Search Symbols..." }, "Header": { "About": "About DIM", "AutoRefresh": "DIM will automatically reload as long as you are still playing.", "BulkTag": "Bulk tag items", "BungieNetAlert": "Bungie Alert", "Clear": "Clear search filter", "TagAs": "Tag as '{{tag}}'", "CompareMatching": "Compare Items", "DeleteSearch": "Delete Search", "FilterHelp": "Search item/perk, {{example}}, and more", "FilterHelpBrief": "Search items", "FilterHelpRecords": "Search triumphs and collections", "FilterHelpProgress": "Search milestones and bounties", "FilterHelpOptimizer": "Filter armor included in builds, e.g.: {{example}}", "FilterHelpLoadouts": "Search loadout names and notes", "FilterMatchCount_one": "1 item", "FilterMatchCount_other": "{{count}} items", "Filters": "Filters", "FilterHelpMenuItem": "Filters Help...", "LaunchDIMAlone": "Separate Window", "InstallDIM": "Install as an App", "InstallDIMBanner": "Install DIM as an app on your home screen", "Inventory": "Inventory", "IosPwaPrompt": "In Safari, click the share icon (middle button on the bottom) and select \"Add to Home Screen\".", "KeyboardShortcuts": "Keyboard Shortcuts", "MaterialCounts": "Material Counts", "Menu": "Menu", "ProfileAge": "Destiny servers last sent updated data {{age}} ago.\nRefreshing from DIM may get newer data, but Bungie.net may also repeat cached information.", "Refresh": "Refresh Destiny Data [R]", "ReloadApp": "Reload App", "ReportBug": "Report a Bug", "SaveSearch": "Save Search", "SearchResults": "Show Items", "SearchActions": "Open Search Actions", "Shop": "Shop", "UpgradeDIM": "Update DIM", "WhatsNew": "What's New" }, "Help": { "CannotMove": "Cannot move that item off this character.", "NoStorage": "DIM can't store data", "NoStorageMessage": "DIM can't store data in your browser. This can be caused by browsing in private or incognito mode, or when you have low disk space, or a browser bug. Try restarting your computer! You won't be able to log in to or use DIM until you fix this." }, "Hotkey": { "Armory": "Show Armory for an item", "CheatSheetTitle": "Keyboard Shortcuts:", "ClearDialog": "Dismiss dialog", "ClearNewItems": "Clear new items", "Enter": "ENTER", "ItemPopupTab": "Switch item details tab", "LockUnlock": "Lock or unlock an item", "MarkItemAs": "Mark item as '{{tag}}'", "Menu": "Toggle menu", "Note": "Enter notes", "Pull": "Pull item to active character", "RefreshInventory": "Refresh inventory", "ShowHotkeys": "Show keyboard shortcuts", "StartSearch": "Start a search", "StartSearchClear": "Start a fresh search", "Tab": "TAB", "Vault": "Send item to vault" }, "Infusion": { "Filter": "Filter items", "InfuseSource": "Select item to infuse {{name}} into", "InfuseTarget": "Select item to infuse into {{name}}", "InfusionMaterials": "Infusion Materials", "NoItems": "No infusable items available.", "NoTransfer": "Transfer infusion material\n {{target}} cannot be moved.", "SwitchDirection": "Switch", "TransferItems": "Transfer" }, "InGameLoadout": { "ClearSlot": "Clear Slot {{index}}", "Create": "Create Loadout", "CreateTitle": "Create In-Game Loadout From Current Equipment", "CurrentlyEquipped": "Currently Equipped", "PrepareEquip": "Prepare Equip", "EditIdentifiers": "Edit Identifiers", "EditTitle": "Edit Loadout Name and Icon", "EditFailed": "Failed to update loadout", "EquipReady": "In-game Equip Ready", "EquipNotReady": "In-game Equip Not Ready", "LoadoutDetails": "Loadout Details", "LoadoutSlotNum": "Slot {{index}}", "MatchingLoadouts": "Matching Loadouts:", "Deleted": "Loadout Deleted", "DeletedBody": "Cleared the in-game loadout at slot {{index}}", "DeleteFailed": "Failed to delete loadout", "Save": "Update Loadout", "SaveIdentifiers": "Update Identifiers", "SaveToDimLoadout": "Save as DIM Loadout", "Replace": "Replace Loadout {{index}}", "SnapshotFailed": "Failed to snapshot equipped loadout" }, "Inventory": { "ClickToExpand": "(Click to expand)", "MissingSilver": "Your Silver balance is only available while you are playing the game." }, "Item": { "ThumbsUp": "Thumbs Up", "ThumbsDown": "Thumbs Down", "SetBonus": { "NPiece_one": "{{count}} Piece", "NPiece_other": "{{count}} Piece" } }, "ItemFeed": { "Description": "Item Feed", "HideTagged": "Hide Tagged", "ClearFeed": "Clear Feed", "ShowOlderItems": "Show older items", "NoNewItems": "No new items" }, "ItemMove": { "Consolidate": "Consolidated {{name}}", "Distributed": "Distributed {{name}}\n {{name}} is now equally divided between characters.", "MovingItem": "Transfer to vault", "MovingItem_male": "Transfer to {{target}}", "MovingItem_female": "Transfer to {{target}}", "ToStore": "All {{name}} are now on your {{store}}.", "ToVault": "All {{name}} are now in your vault." }, "ItemPicker": { "ChooseItem": "Choose an item:", "SearchPlaceholder": "Search items" }, "ItemService": { "BucketFull": { "Guardian": "There are too many '{{itemtype}}' items on your {{store}}.", "Guardian_female": "There are too many '{{itemtype}}' items on your {{store}}.", "Guardian_male": "There are too many '{{itemtype}}' items on your {{store}}.", "Vault": "There are too many '{{itemtype}}' items in the {{store}}." }, "Classified": "This item is classified and cannot be transferred at this time.", "Classified2": "Classified item. Bungie does not yet provide information about this item. Add notes to this item and use the \"notes:\" search filter to find it.", "Deequip": "Cannot find another item to equip in order to deequip {{itemname}}", "ExoticError": "'{{itemname}}' cannot be equipped because the exotic in the {{slot}} slot cannot be unequipped. ({{error}})", "NotEnoughRoom": "There's nothing we can move out of {{store}} to make room for {{itemname}}", "NotEnoughRoomGeneral": "There's not enough room to move this item.", "OnlyEquippedClassLevel": "This can only be equipped on a {{class}} at or above level {{level}}.", "OnlyEquippedLevel": "This can only be equipped on characters at or above level {{level}}.", "PostmasterAlmostFull": "Almost full!", "PostmasterFull": "Full!", "PreviewVendor": "Preview {{type}} contents", "StackFull": "You already have a full stack of {{name}}", "StoreName": "{{genderRace}} {{className}}" }, "KillType": { "Melee": "Melee", "Super": "Super", "Grenade": "Grenade", "Finisher": "Finisher", "Precision": "Precision", "ClassAbilities": "Class Ability" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "The postmaster is almost full! ({{number}}/{{postmasterSize}})", "PostmasterFull": "The postmaster is full! ({{number}}/{{postmasterSize}})" }, "LB": { "AdvancedOptions": "Advanced Options", "ChooseAMod": "Choose your mods", "ChooseAnExotic": "Choose your exotic", "ChooseASetBonus": "Choose your set bonuses", "ClearLocked": "Clear Locked", "ContainsVendorItems": "This loadout contains vendor items", "Current": "Current", "Equip": "Equip on {{character}}", "Exclude": "Excluded Items", "ExcludeHelp": "Shift + click an item (or drag and drop into this bucket) to build sets without specific gear.", "ExistingBuildStats": "Existing Build Stats", "ExistingBuildStatsNote": "Only showing builds with strictly higher stats.", "FilterSets": "Filter sets", "Help": { "And": "Armor with all of these perks will be used (\"and\")", "ChangeNodes": "Change the Intellect, Discipline, or Strength nodes in game to what is displayed to create each loadout.", "Discipline": "Discipline speeds up Grenade recharge time", "DragAndDrop": "Drag and drop items into the locked buckets to build sets with only that gear", "Help": "Need help?", "HigherTiers": "Higher Tiers are better", "Intellect": "Intellect speeds up Super recharge time", "Lock": "Lock a set of perks by clicking a lock bucket and selecting perks", "MultiPerk": "To use armor with multiple perks together shift+click the desired perks", "NoPerk": "If a perk doesn't appear it means that you own no armor with that perk", "Or": "Armor with any of these perks will be used (\"or\")", "ShiftClick": "Shift click an item to build sets without that gear", "StatsIncrease": "As an items defense level increases, the stats on that item (int/dis/str) also increase.", "Strength": "Strength speeds up Melee recharge time", "Synergy": "Try to find armor that has ammo increasing perks for weapon types that you use.", "Tier11Example": "4/5/2 (a Tier 11 build) is 4 Intellect, 5 Discipline, 2 Strength (4+5+2 = Tier 11)" }, "HideAllConfigs": "Hide all configurations", "HideConfigs": "Hide configurations", "IncompatibleWithOptimizer": "This item is incompatible with the Optimizer. Please reacquire a new version from Collections.", "LB": "Loadout Optimizer", "LightMode": { "HelpCurrent": "Calculates loadouts at current defense levels.", "HelpScaled": "Calculates loadouts as if all items were 350 defense.", "LightMode": "Light mode" }, "Loading": "Loading best sets", "LockEquipped": "Lock Equipped", "LockPerk": "Lock perk", "Locked": "Locked Items", "LockedHelp": "Drag and drop any item into its bucket to build set with that specific gear. Shift + click to exclude items.", "Missing2": "Missing rare, legendary, or exotic pieces to build a full set!", "ProcessingMode": { "Fast": "Fast", "Full": "Full", "HelpFast": "Only looks at your best gear.", "HelpFull": "Looks at more gear, but takes longer.", "ProcessingMode": "Processing mode" }, "Scaled": "Scaled", "SearchAMod": "Search for mod name or description", "SearchAnExotic": "Search for exotic name or description", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Activity Mods", "SelectExotic": "Select exotic", "SelectMods": "Select Mods", "SelectSetBonus": "Select Set Bonuses", "AddStack": "Add another copy of this mod", "RemoveStack": "Remove a copy of this mod", "SelectSubclassOptions": "Customize subclass", "ShowAllConfigs": "Show all configurations", "ShowConfigs": "Show configurations", "ShowGear": "{{class}} Armor", "Vendor": "Include Vendor items" }, "Loading": { "Code": "Loading DIM code...", "Profile": "Loading Destiny profile...", "Accounts": "Loading Destiny accounts...", "FilterHelp": "Loading search help...", "Vendors": "Loading Destiny vendors..." }, "LoadoutBuilder": { "AutomaticallyPicked": "This mod was added automatically to improve build stats.", "AlwaysAutoMods": "Artifice and Tuning mods will always be chosen automatically.", "DisabledByAutoStatMods": "Stat mods are being chosen automatically by Loadout Optimizer.", "AutoStatMods": "Automatically add stat mods", "All": "All", "AnyExotic": "Any Exotic", "AnyExoticDescription": "Sets must contain an exotic, but any exotic will do.", "Artifice": "Artifice", "AssumeMasterwork": "Assume Masterwork", "AssumeMasterworkOptions": { "All": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nArmor 2.0 Exotics: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Enhanced to accept Artifice stat mods.", "Legendary": "Legendary: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExotic: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "None": "All armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Current": "Current stats, assumed energy level at least {{minLoItemEnergy}}.", "Masterworked": "Full masterwork stat bonuses, assumed energy level at least 10." }, "ChooseAlternateTitle": "Choose another item", "CompareLoadout": "Compare Loadout", "ConfirmOverwrite": "Are you sure you want to replace the armor in the loadout \"{{name}}\" with this new set of armor?", "DisabledDueToMaintenance": "The Loadout Optimizer is currently disabled due to Bungie API maintenance.", "EquipItems": "Equip", "ExcludeItem": "Exclude Item", "ExcludedItems": "Excluded Items", "Exotic": "Exotic Armor", "MwExotic": "Exotic", "ExcludeVendors": "Search \"not:vendor\" to exclude vendor items from Loadout Optimizer.", "ExistingLoadout": "Existing Loadout", "FOTLWildcardWarning": "This set contains a Festival of the Lost mask. Manually apply the correct mod to activate desired set bonuses.", "ExoticClassItemPerks": "If you want specific perks, use searches like exactperk:\"spirit of verity\". Click perks in the Optimizer results to add or remove them from the item filter.", "ExoticSpecialCategory": "Special", "Filter": "Settings", "Legendary": "Legendary", "LockItem": "Pin item", "PinnedItems": "Pinned Items", "PinnedItemsFinePrint": "Search filters are saved with Loadout Optimizer settings, but pinned and excluded items are not. Pins and exclusions will be ignored when DIM checks existing Loadouts for better stat builds.", "MissingClass": "Build is for: {{className}}", "MissingClassDescription": "The build you're trying to view is for a character class you don't have.", "NoBuildsFoundExplainer": { "Header": "No builds were found. Here are possible reasons DIM couldn't find any builds:", "AlwaysInvalidMods": "These mods don't fit into any of your owned items:", "RemoveMods": "Remove these mods", "MaybeRemoveMods": "Consider removing some mods:", "AssumptionsRestricted": "DIM cannot recommend armor energy changes:", "AssumeMasterworked": "Allow DIM to recommend masterworking armor", "ActiveSearchQuery": "An active search query is restricting the items DIM can include in builds", "MaybeRemoveSearchQuery": "Consider clearing or changing the filter in the search bar", "ExoticDoesNotExist": "You don't have any of the selected exotic armor in your inventory.", "MaybeAllowMoreItems": "Consider allowing other items:", "BadSlot": "In the {{bucketName}} slot, none of the allowed items could accommodate these mods:", "LowerBoundsFailed": "Many sets did not meet minimum stat requirements", "ModAssignmentFailed": "Many sets could not accommodate all requested mods", "MaybeDecreaseLowerBounds": "Consider reducing minimum stat requirements", "AllowAutoStatMods": "Allow DIM to automatically include additional stat mods", "SetBonuses": "You have chosen some set bonuses, maybe you don't have the right items to use them.", "RemoveSetBonuses": "Consider removing some set bonuses" }, "NoExotic": "No Exotic", "NoExoticDescription": "Equivalent to searching \"not:exotic\" in the search bar - sets will not use any exotic armor.", "NoExoticPreference": "No Exotic Selected", "NoExoticPreferenceDescription": "Exotic armor will be used if it maximizes stats.", "NoLoadoutsToCompare": "No loadouts to compare", "None": "None", "OptimizerExplanationStats": "Drag the most important stats to the top, and uncheck stats you don't want to maximize.", "OptimizerExplanationMods": "Choose an exotic, mods, and a subclass. These will contribute stats to the build, while any mods already on the armor are ignored.", "OptimizerExplanationSearch": "Use the search bar to narrow down which armor to consider, e.g. {{example}}. If no armor in a slot matches the search, all items will be considered for that slot.", "OptimizerExplanationGuide": "Read the User Guide for more info and a video tutorial.", "OptimizerSet": "Optimizer Set", "ProcessingSets": "Finding highest stat sets...", "SaveAs": "Save as", "SetBonus": "Set Bonuses", "SpeedReport": "Evaluated {{combos, number}} combinations in {{time}} seconds using {{cpus}} CPU cores.", "StatConstraints": "Stat Priorities & Ranges", "StatMax": "Max", "StatMin": "Min", "StatRangeTooltip": "With the current min/max setting, loadouts exist which have {{min}} to {{max}} points in this stat. Double-click to set min to {{max}}.", "StatTotal": "Total: {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "Unable to add all mods.", "UnableToAddAllModsBody": "There weren't enough mod slots available to fit {{mods}}.", "UnlockItem": "Unpin Item", "IncreaseStatPriority": "Increase stat priority", "DecreaseStatPriority": "Decrease stat priority", "IgnoreStat": "If unchecked, Loadout Optimizer will pretend this stat doesn't exist when building sets", "LimitToNewFeaturedGear": "Limit to new/featured gear" }, "Loadouts": { "Abilities": "Abilities", "Actions": "Actions for {{title}}", "AddEquippedItems": "Add Equipped", "AddNotes": "Add Notes", "AddUnequippedItems": "Add Unequipped", "Any": "Any class", "ArmorStats": "Armor Stats", "ArtifactUnlocks": "Artifact Unlocks", "ArtifactUnlocksWithSeason": "Artifact Unlocks – S{{seasonNumber}}", "ArtifactUnlocksDesc": "Due to Bungie.net limitations, DIM cannot automatically configure your artifact. You need to perform these unlocks in-game before applying the Loadout.", "Apply": "Apply", "ApplySearch": "Transfer search \"{{query}}\"", "BadLoadoutShare": "Unable to load shared loadout", "BadLoadoutShareBody": "The loadout you're trying to load is invalid: {{error}}", "Before": "Before '{{name}}'", "CannotCustomizeSubclass": "This subclass cannot be configured", "ChooseItem": "Add {{name}}", "Classified": "Some of your items are classified, and cannot be included in the max power calculation.", "ClassType": "Any class loadout", "ClassType_male": "{{className}} loadout", "ClassType_female": "{{className}} loadout", "ClassTypeMismatch": "A {{className}} item cannot be added to this loadout", "ClassTypeMissing": "You do not have a {{className}} to create a loadout for", "ClearSection": "Remove all", "ClearLoadoutParameters": "Remove Loadout Optimizer settings", "ClearSpace": "Move others away", "ClearSpaceWeapons": "Move other weapons away", "ClearSpaceArmor": "Move other armor away", "ClearUnsetMods": "Remove other mods", "CopyAndEdit": "Edit Copy", "Create": "Create Loadout", "CurrentlyEquipped": "Currently Equipped", "Delete": "Delete", "DimLoadouts": "DIM Loadouts", "Edit": "Edit Loadout", "EditBrief": "Edit", "EquippableDifferent1": "Multiple Exotic items were used to calculate your Maximum Power, so the number shown may not be achievable when equipping your items in-game.", "EquippableDifferent2": "Maximum Power isn't limited by the \"One Exotic\" rule when determining the Power of your drops, powerful, and pinnacle rewards.", "Equipped": "Equipped", "Fashion": "Choose fashion", "FashionOnly": "Fashion-only", "ModsOnly": "Mods-only", "Filters": "Loadout Filters", "FilteredItems": "Filtered Items", "FindAnother": "Find another {{name}}", "FromEquipped": "Equipped", "Generated": "{{statTotal}} Stat Point Loadout", "HashtagTip": "Tip: Use #hashtags in your loadout names or notes and they'll show up here.", "ImportLoadout": "Import Loadout", "Import": { "PasteHere": "Paste a loadout link to open the loadout.", "BadURL": "Not a valid loadout share URL.", "Error404": "This loadout doesn't exist.", "Error": "Error getting loadout:" }, "IncludeRuntimeStatBenefits": "Include Font mod stats", "IncludeRuntimeStatBenefitsDesc": "\"Font of ...\" armor mods provide a flat boost to character stats while you have Armor Charges.\n\nWith this setting, DIM considers these mods active and adds their benefits to this Loadout's stats in calculations and optimizations.", "InGameLoadouts": "In-Game Loadouts", "SetBonusesDesc": "Required set bonuses", "InGameActions": "In-Game Loadout Actions", "ItemLeveling": "Item Leveling", "LoadoutName": "Loadout name", "Loadouts": "Loadouts", "LoadoutParameters": "Loadout Optimizer settings", "LoadoutParametersExotic": "Loadout must include this exotic: {{exoticName}}", "LoadoutParametersQuery": "Items must match this search filter", "LoadoutParametersStats": "Stat priorities and minimum/maximum stat ranges", "MakeRoom": "Make Room for Postmaster", "MakeRoomDone_one": "Finished making room for 1 Postmaster item by moving 1 item off of {{store}}.", "MakeRoomDone_female_one": "Finished making room for 1 Postmaster item by moving 1 item off of {{store}}.", "MakeRoomDone_male_one": "Finished making room for 1 Postmaster item by moving 1 item off of {{store}}.", "MakeRoomDone_other": "Finished making room for {{count}} Postmaster items by moving {{movedNum}} items off of {{store}}.", "MakeRoomDone_female_other": "Finished making room for {{count}} Postmaster items by moving {{movedNum}} items off of {{store}}.", "MakeRoomDone_male_other": "Finished making room for {{count}} Postmaster items by moving {{movedNum}} items off of {{store}}.", "MakeRoomError": "Unable to make room for all Postmaster items: {{error}}.", "ManageLoadouts": "Manage Loadouts", "MaxSlots": "You can only have {{slots}} {{bucketName}} in a loadout.", "MaximizeLight": "Max Light", "MaximizePower": "Max Power", "MaximizeStat": "Maximize Stat", "ModPlacement": { "InvalidMods": "Invalid Mods", "InvalidModsDesc_one": "1 mod cannot fit into any armor piece.", "InvalidModsDesc_other": "{{count}} mods cannot fit into any armor piece.", "ModPlacement": "Mod Placement", "UnassignedMods": "Unassigned Mods", "UnassignedModsDesc_one": "1 mod did not fit due to insufficient energy capacity or mod slots. Energy upgrades to the selected armor will not fix the issue.", "UnassignedModsDesc_other": "{{count}} mods did not fit due to insufficient energy capacity or mod slots. Energy upgrades to the selected armor will not fix the issue.", "UpgradeCosts": "Upgrade Costs", "UpgradeCostsDesc": "Some armor needs energy capacity upgrades to fit the requested mods. In total, these upgrades cost:", "UnstackableMod": "Not Stackable", "StackableMod": "Stackable" }, "MissingItemsWarning": "Some of the items in this loadout are no longer in your inventory.", "MissingItems": "Missing Items", "ModErrorSummary_one": "1 mod error:", "ModErrorSummary_other": "{{count}} mod errors:", "ItemErrorSummary_one": "1 item error:", "ItemErrorSummary_other": "{{count}} item errors:", "Mods": "Mods", "NoneMatch": "None of your loadouts matched the filters.", "NotesPlaceholder": "Write some notes about this loadout, or use #hashtags to categorize it", "NotificationTitle": "Loadout: {{name}}", "OnlyItems": "Only equippable items, materials, and consumables can be added to a loadout.", "OnWrongCharacterWarning": "This character's most powerful armor is on another character. To count toward the Power of drops, powerful, and pinnacle rewards, armor must be on this character or in the Vault.", "OnWrongCharacterAdvice": "Click here to find this character's highest Power items.", "OpenInOptimizer": "Optimize Armor", "PickArmor": "Pick Armor", "PickMods": "Add armor mods", "PullFromPostmaster": "Collect Postmaster", "PullFromPostmasterNotification_one": "Pulling 1 Postmaster item to {{store}}.", "PullFromPostmasterNotification_female_one": "Pulling 1 Postmaster item to {{store}}.", "PullFromPostmasterNotification_male_one": "Pulling 1 Postmaster item to {{store}}.", "PullFromPostmasterNotification_other": "Pulling {{count}} Postmaster items to {{store}}.", "PullFromPostmasterNotification_female_other": "Pulling {{count}} Postmaster items to {{store}}.", "PullFromPostmasterNotification_male_other": "Pulling {{count}} Postmaster items to {{store}}.", "PullFromPostmasterError": "Unable to pull from Postmaster: {{error}}.", "PullFromPostmasterGeneralError": "Unable to pull all items from Postmaster.", "PullFromPostmasterPopupTitle": "Pull from Postmaster", "NoSpace": "You're out of space in the vault and any other characters.", "Prismatic": { "Aspect": "Prismatic Aspect", "Grenade": "Prismatic Grenade", "Melee": "Prismatic Melee", "Super": "Super Ability" }, "Random": "Random", "Randomize": "Randomize Loadout", "RandomizeNew": "Create Random", "RandomizeButton": "Randomize", "RandomizePrompt": "Randomize your equipped weapons, armor and ghost?", "RandomizeQueryHint": "Tip: Search for items first to restrict what items can be randomly chosen.", "RandomizeSearch": "Randomize from Search", "RandomizeSearchPrompt": "Randomize your equipped items from search \"{{query}}\"?", "RandomizeWeapons": "Randomize your equipped weapons?", "Redo": "Redo", "RestoreAllItems": "All Items", "Save": "Save", "OpenOnStreamDeck": "Open on Stream Deck", "SaveLoadout": "Save Loadout", "UpdateLoadout": "Update Loadout", "SaveAsDIM": "Save as DIM Loadout", "SaveAsNew": "Save as New", "SaveAsNewTooltip": "Keep the original loadout and save this as a new loadout", "SaveDisabled": { "AlreadyExists": "Choose a new name for the loadout.", "Empty": "The loadout is empty.", "NoName": "The loadout needs a name." }, "Season": "Season {{season}}", "Snapshot": "Save As In-Game Loadout", "SortByName": "Sort by name", "SortByEditTime": "Sort by last edited", "Share": { "Title": "Share \"{{name}}\"", "Placeholder": "Loading share link", "Error": "Error getting share link", "Copied": "Copied loadout link to clipboard", "CopyButton": "Copy Link", "NativeShare": "Share Link", "Summary": "Share this loadout containing:", "NumItems_one": "{{count}} item - recipients will be prompted to select a comparable item from their inventory", "NumItems_other": "{{count}} items - recipients will be prompted to select comparable items from their inventory", "NumMods_one": "{{count}} mod", "NumMods_other": "{{count}} mods", "Fashion": "Fashion (shaders & ornaments)", "Subclass": "Subclass customization", "LoadoutOptimizer": "Loadout Optimizer settings", "Notes": "Notes" }, "ShareLoadout": "Share", "ShowModPlacement": "Show Mod Placement", "SubclassOptions": "{{subclass}} options", "SubclassOptionsSearch": "Search {{subclass}} options", "SyncFromEquipped": "Sync from equipped", "FillFromEquipped": "Fill in using equipped", "FillFromInventory": "Fill in using non-equipped", "TooManyRequested": "You have {{total}} {{itemname}} but your loadout asks for {{requested}}. We transferred all you had.", "UnassignedModError": "Mod didn't fit on your current armor", "Undo": "Undo", "Unequipped": "Unequipped", "VendorsCannotEquip": "You don't have these items. Tap to pick a replacement or click the X to remove:", "WeaponsOnly": "Weapons Only", "NotStarted": "Waiting for other actions to complete, or an inventory refresh to finish loading", "Deequip": "De-equipping items from other characters", "MoveItems": "Moving items", "EquipItems": "Equipping items", "EquipInGameLoadout": "Equipping in-game loadout", "ApplyInGameLoadoutInGame": "Your loadout is ready to equip but since you're in an activity you need to equip it in-game.", "SocketOverrides": "Changing subclass options", "ApplyMods": "Applying mods", "ClearingSpace": "Moving other items away", "Succeeded": "Loadout succeeded", "Failed": "Loadout failed to apply completely", "Update": "Save Changes", "TuningMods": "Tuning Mods", "SalvationsEdgeMods": "Salvation's Edge Mods", "CancelEditing": "Cancel Editing" }, "LoadoutAnalysis": { "Analyzing": "Analyzing {{numAnalyzed}}/{{numLoadouts}} Loadouts", "Analyzed": "Analyzed {{numLoadouts}} Loadouts", "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat to exceed 200. DIM may identify better stats by reducing the amount of excess stats. If this is undesired, disable \"$t(Loadouts.IncludeRuntimeStatBenefits)\" in the Loadout.", "MissingItems": { "Name": "Missing Items", "Description": "Some of the items in this loadout are no longer in your inventory." }, "InvalidMods": { "Name": "Deprecated Mods", "Description": "Some mods in this loadout are deprecated or do not otherwise fit into any of your armor pieces." }, "EmptyFragmentSlots": { "Name": "Empty Fragment Slots", "Description": "There are empty fragment slots in this subclass." }, "TooManyFragments": { "Name": "Too Many Fragments", "Description": "There are more fragments configured on the subclass than granted by aspects." }, "NeedsArmorUpgrades": { "Name": "Needs Armor Upgrades", "Description": "Armor in this loadout needs to be upgraded to accommodate all mods or reach specified stats." }, "BetterStatsAvailable": { "Name": "Better Stats Available", "Description": "Choosing different armor or mods for this loadout will allow reaching higher stats. Choose \"$t(Loadouts.OpenInOptimizer)\" to view better builds." }, "NotAFullArmorSet": { "Name": "Not A Full Armor Set", "Description": "This loadout could not be analyzed further because it does not include a full set of armor." }, "DoesNotRespectExotic": { "Name": "Wrong Exotic", "Description": "This loadout's Loadout Optimizer settings specify an exotic choice, but the loadout does not match that exotic." }, "ModsDontFit": { "Name": "Unassigned Mods", "Description": "Armor in this loadout cannot accommodate all loadout mods, even if the armor was upgraded." }, "UsesSeasonalMods": { "Name": "Uses Seasonal Mods", "Description": "This loadout relies on mods that are only available in some seasons. When the season ends, some mods will be unavailable or exceed armor energy capacity." }, "DoesNotSatisfyStatConstraints": { "Name": "Wrong Stat Minimums", "Description": "Loadout Optimizer settings for this Loadout specify stat minimums, but the Loadout does not reach them." }, "InvalidSearchQuery": { "Name": "Invalid Search Query", "Description": "This loadout was created with a search query in Loadout Optimizer that is not valid." }, "ItemsDoNotMatchSearchQuery": { "Name": "Search Excludes Items", "Description": "This loadout was created with a search query in Loadout Optimizer, and that search query excludes at least one of the items in the loadout." } }, "Manifest": { "Download": "Downloading latest Destiny info database from Bungie...", "Error": "Error loading Destiny info database:\n{{error}}\nReload to retry.", "Load": "Loading Destiny info database..." }, "Milestone": { "Daily": "Daily Challenge", "OneTime": "One Time Challenge", "SeasonalRank": "Seasonal Rank {{rank}}", "SeasonEnds": "Season ends in ", "Special": "Special Event Challenge", "Tutorial": "Tutorial Challenge", "Unknown": "Challenge", "Weekly": "Weekly Challenge" }, "Mods": { "HarmonicModDescription": "This mod's effect comes at a reduced cost and changes element depending on the equipped subclass." }, "MoveAmount": { "Amount": "Amount:" }, "MovePopup": { "Acquired": "This item is unlocked in collections.", "AcquiredMod": "This mod is unlocked in collections.", "AddToLoadout": "Loadout", "AddToLoadoutTitle": "Add this to a loadout", "AddNote": "Add notes", "All": "All", "CantPullFromPostmaster": "You must visit the postmaster in game to retrieve this item.", "CannotCurrentlyRoll": "This perk cannot be rolled on the current version of this item.", "UnreliablePerkOption": "This perk appears only in the collections view. It might not roll randomly on this item.", "Consolidate": "Consolidate", "CommunityData": "Community Insight", "DistributeEvenly": "Distribute Evenly", "EnhancementTier": "Tier {{tier}}", "Equip": "Equip on:", "EquipWithName": "Equip on {{character}}", "FavoriteUnFavorite": { "Favorite": "Favorite {{itemType}}", "Unfavorite": "Unfavorite {{itemType}}", "Favorited": "Favorited", "Unfavorited": "Unfavorited" }, "Infuse": "Infuse", "InfuseTitle": "Open the infusion fuel finder", "LoadingSockets": "Perk and stat details have not loaded yet for this item.", "LockUnlock": { "Lock": "Lock {{itemType}}", "Unlock": "Unlock {{itemType}}", "Locked": "Locked", "Unlocked": "Unlocked", "AutoLock": "Lock state is synced to this item's tag" }, "MissingSockets": "Perk and mod details are unavailable while Bungie is updating their services. It will return when they are done, usually in a few hours.", "Notes": "Notes:", "OverviewTab": "Overview", "Owned": "This item is in your inventory.", "OwnedMod": "This mod is in your modifications inventory.", "PullItem": "Pull from {{bucket}} to {{store}}", "PullPostmaster": "Pull from Postmaster", "ReadLore": "Read lore on Ishtar Collective", "ReadLoreLink": "Read lore", "Rewards": "Rewards:", "SendToVault": "Send to Vault", "Store": "Pull to:", "OpenOnStreamDeck": "Open on Stream Deck", "StoreWithName": "Pull to {{character}}", "Subtitle": { "Type": "{{classType}} {{typeName}}", "QuestProgress": "Step {{questStepNum}} of {{questStepsTotal}}" }, "TabList": "Item detail tabs", "ToggleSidecar": "Expand or collapse item actions", "TrackUntrack": { "Track": "Track {{itemType}}", "Untrack": "Untrack {{itemType}}", "Tracked": "Tracked", "Untracked": "Untracked" }, "TriageTab": "Triage", "Vault": "Vault", "WeaponLevel": "Weapon Level {{level}}", "CatalystProgress": "Catalyst Progress", "ArtifactBreaker": "This weapon has {{breaker}} because of an unlocked artifact perk.", "IntrinsicBreaker": "This weapon intrinsically has {{breaker}}." }, "Notes": { "Error": "Error! Max 120 characters for notes.", "Help": "Add notes, #hashtags, and :symbols:" }, "Notification": { "Cancel": "Cancel", "OK": "Dismiss" }, "Objectives": { "Complete": "Complete", "Incomplete": "Incomplete" }, "Organizer": { "Organizer": "Organizer", "OpenIn": "Show in Organizer", "EnabledColumns": "Enabled Columns", "BulkMove": "Move To", "BulkTag": "Tag", "BulkMoveLoadoutName": "Selected in Organizer", "SelectItem": "Select or unselect {{name}}", "SelectAll": "Select All", "Lock": "Lock", "Unlock": "Unlock", "ShiftTip": "Tip: Hold the Shift key and click on a cell to filter items", "NoMobile": "Turn your phone sideways to use the Organizer.", "NoItems": "No items match the filters. If you have a search query, try clearing it.", "Note": "Set Notes", "Columns": { "Stats": "Stats", "BaseStats": "Base Stats", "Breaker": "Breaker", "Icon": "Icon", "Name": "Name", "Power": "Power", "Damage": "Damage", "Ammo": "Ammo", "Foundry": "Foundry", "Featured": "New Gear", "Holofoil": "Holofoil", "OriginTraits": "Origin Trait", "Shaders": "Cosmetics", "Locked": "Locked", "Energy": "Energy", "Location": "Location", "Tag": "Tag", "WishList": "Wish List", "PercentComplete": "% Complete", "StatQuality": "Stat Quality", "StatQualityStat": "{{stat}}%", "Perks": "Perks", "PerksGrid": "Perks Grid", "Mods": "Mods", "Quality": "Quality %", "ItemTier": "Tier", "Tier": "Rarity", "Source": "Source", "Year": "Year", "Season": "Season", "Event": "Event", "ModSlot": "Mod Slot", "Archetype": "Archetype", "Frame": "Frame", "PerksMods": "Perks & Mods", "OtherPerks": "Weapon Components", "Traits": "Weapon Traits", "TertiaryStat": "3rd Stat", "TuningStat": "Tuner", "CustomTotal": "Custom Total", "MasterworkTier": "MW Tier", "MasterworkStat": "MW Stat", "Level": "Level", "Harmonizable": "Harmonizable", "KillTracker": "Kills", "Loadouts": "Loadouts", "Notes": "Notes", "WishListNotes": "Wish List Notes", "New": "New", "Recency": "Recency", "Crafted": "Shaped Date" }, "Stats": { "RPM": "RPM", "Reload": "Reload", "Aim": "Aim", "Recoil": "Recoil", "Power": "Power", "Airborne": "Airborne", "AmmoGeneration": "Ammo Gen" } }, "Progress": { "Bounties": "Bounties", "CrucibleRank": "Ranks", "RewardPassPrestigeRank": "Prestige Rank {{rank}}", "RewardPassEndsIn": "Reward Pass ends in ", "Items": "Quest Items", "Milestones": "Milestones & Challenges", "PaleHeartPathfinder": "Pale Heart Pathfinder", "PercentPrestige": "{{pct}}% to reset", "PercentMax": "{{pct}}% to maximum", "PointsUsed_one": "1 point used", "PointsUsed_other": "{{count}} points used", "PowerBonusHeader": "+{{powerBonus}} Power Rewards", "PowerBonusHeaderUndefined": "Other Rewards", "Progress": "Progress", "QuestExpired": "Expired", "QuestExpires": "Expires in ", "Quests": "Quests", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}pts", "Resets_one": "1 reset", "Resets_other": "{{count}} resets", "GambitPathfinder": "Gambit Pathfinder", "CruciblePathfinder": "Crucible Pathfinder", "VanguardPathfinder": "Vanguard Pathfinder", "SeasonalHub": "Seasonal Hub", "SecretTriumph": "Secret Triumph", "StatTrackers": "Stat Trackers", "TrackedTriumphs": "Tracked Triumphs", "QueryFilteredTrackedTriumphs": "None of your tracked triumphs matched the search", "NoTrackedTriumph": "You have no tracked triumphs. Track as many as you like in DIM.", "NoEventChallenges": "You have completed all event challenges", "CatalystSource": "Source: {{source}}" }, "RecordBooks": { "HideCompleted": "Hide completed records", "RecordBooks": "Record Books" }, "Records": { "Title": "Records", "UniversalOrnamentSetOther": "Other" }, "SearchHistory": { "Description": "These are all your past and saved searches. You can delete them from here.", "DeleteAll": "Delete all non-starred searches", "Date": "Last Used", "UsageCount": "# Used", "Query": "Search", "Link": "View and edit search history", "Title": "Search History", "Item": "Item Searches", "Loadout": "Loadout Searches" }, "Settings": { "Appearance": "Appearance", "ArmorArchetypeModslot": "Armor Archetype / Modslot", "AutoLockTagged": "Sync item lock state with tag", "AutoLockTaggedExplanation": "DIM will automatically lock and unlock items to match their tag. Crafted items will remain unlocked to allow reshaping. When this setting is enabled, the lock icon will not be shown on the item tile for tagged items.", "BadgePostmaster": "Show the number of postmaster items for the current character on app icon", "BadgePostmasterExplanation": "For this to work you must install DIM as an app and your OS must support displaying badges", "BungieDescriptionOnly": "Bungie Descriptions", "CommunityDescriptionOnly": "Community Descriptions", "BothDescriptions": "Both Descriptions", "CharacterOrder": "Sort characters by", "CharacterOrderFixed": "Character age (buggy on PC)", "CharacterOrderRecent": "Most recent character", "CharacterOrderReversed": "Most recent character (reversed)", "ColumnSize": "{{num}} items", "ColumnSizeAuto": "Auto", "CommunityData": "Community Perk Insights", "CsvImport": "Import CSV", "CustomStatTitle": "Custom Stat Total", "CustomStatDesc1": "Choose desired armor stats to make a custom total stat.", "CustomStatDesc3": "Custom stats will appear in the Item Popup, Organizer, and Compare.", "CustomDesc": "Custom total of selected base stats, ignoring mods or masterworks. Visit Settings to configure which stats are included.", "CustomStatDelete": "Delete this Custom Stat", "CustomStatDeleteConfirm": "Delete this Custom Stat?", "CustomStatCreate": "Create a new custom stat", "CustomStatChooseName": "Choose a Custom Stat name", "CustomErrorValues": "Stat weights must be positive numbers.\nAt least 2 stat weights must be above zero.", "CustomErrorLabel": "A stat name must contain word characters, and be different from other stat names for this Guardian class.", "Data": "Spreadsheets", "DefaultItemSizeNote": "An item size of 50px will look the sharpest, without blurring the item picture or text.", "DontForgetDupes": "Don't forget you can search is:dupe to quickly find duplicate items, and you can use the comparison tool or organizer to evaluate related items.", "EnableAdvancedStats": "Show stat quality rating on armor (D1)", "ExportProfile": "Export API profile response", "ExportSS": "Inventory spreadsheets", "ExportSSHelp": "Download a CSV list of your items that can be easily viewed in the spreadsheet app of your choice.", "ExportLoadoutSS": "Loadout spreadsheets", "ExportLoadoutSSHelp": "Download a CSV list of your DIM Loadouts that can be easily viewed in the spreadsheet app of your choice.", "ExportSSNoStores": "You need to load your inventory once before clicking this button.", "HidePullFromPostmaster": "Hide the \"$t(Loadouts.PullFromPostmaster)\" button", "Inventory": "Inventory Display", "InventoryColumns": "Character inventory width", "InventoryColumnsMobile": "Character inventory width on mobile portrait", "InventoryColumnsMobileLine2": "The items will be resized to accommodate the new setting", "InventoryNumberOfSpacesToClear": "Number of empty spaces to make when using Farming Mode", "Items": "Item Display", "Language": "Language", "LogOut": "Log out", "Masterworked": "Masterworked", "MaxParallelCores": "Maximum cores for parallel tasks", "MaxParallelCoresExplanation": "Controls how many CPU cores DIM can use for intensive tasks like Loadout Optimizer and Loadout Analyzer. Higher values may improve performance but use more system resources.", "OrnamentDisplay": "Show Ornaments on item tiles", "OrnamentDisplayExplanationDisabled": "Items will never display their ornaments", "OrnamentDisplayExplanationEnabled": "Hovering or long-pressing armor will hide its ornament", "OrnamentDisplayExplanationHide": "Hovering or long-pressing an item will hide its ornament", "OrnamentDisplayExplanationShow": "Hovering or long-pressing an item will show its ornament", "ResetToDefault": "Reset", "ReverseSort": "Toggle forward/reverse sort", "SetSort": "Sort items by:", "SetVaultWeaponGrouping": "Group vault weapons by:", "VaultArmorGroupingStyle": "Separate armor on different lines by class", "VaultWeaponGroupingStyle": "Separate weapon groups on different lines", "Settings": "Settings", "ShowNewItems": "Show a red dot on new items", "ExpandSingleCharacter": "Show all characters", "SingleCharacter": "Single-Character View", "SingleCharacterExplanation": "DIM will show only the most recently played character.\nItems held by hidden characters will appear in the vault, if they can be used by the current character.\nItems specific to other classes will be hidden entirely.", "SizeItem": "Item size", "SortByAmmoType": "Ammo Type", "SortByAmount": "Stack Size", "SortByClassType": "Required Class", "SortByCrafted": "Crafted (D2)", "SortByDeepsight": "Deepsight (D2)", "SortByFeatured": "New Gear / Featured (D2)", "SortByPrimary": "Power level", "SortByRarity": "Rarity", "SortByRating": "Armor Quality (D1)", "SortByRecent": "Recently Acquired (D2)", "SortByTag": "Tag ({{taglist}})", "SortByTier": "Tier (D2)", "SortByType": "Type", "SortBySeason": "Season (D2)", "SortByWeaponElement": "Damage Type", "SortCustom": "Custom Sort", "SortName": "Name", "SpacesSize_one": "{{count}} space", "SpacesSize_other": "{{count}} spaces", "Theme": "Theme", "Troubleshooting": "Troubleshooting", "VaultGroupingNone": "None", "VaultUnder": "Show vaulted items under equipped items", "RestoreVaultSide": "Show vaulted items in their own column", "WeaponFrame": "Weapon Frame", "WishlistRefreshNotificationBody": "If you do not see any updates, be sure the source (such as GitHub) reflects them!", "WishlistRefreshNotificationTitle": "Wishlists Reloaded" }, "Sockets": { "ApplyPerks": "Apply Perks", "Search": "Search names or descriptions", "SelectWishlistPerks": "Preview Wishlist Perks", "Insert": { "Mod": "Insert Mod", "Shader": "Apply Shader", "Ornament": "Apply Ornament", "Ability": "Equip Ability", "Fragment": "Insert Fragment", "Aspect": "Insert Aspect", "Transmat": "Apply Transmat Effect", "Projection": "Apply Ghost Projection", "Super": "Equip Super" }, "Select": { "Mod": "Preview Mod", "Shader": "Preview Shader", "Ornament": "Preview Ornament", "Ability": "Preview Ability", "Fragment": "Preview Fragment", "Aspect": "Preview Aspect", "Transmat": "Preview Transmat Effect", "Projection": "Preview Ghost Projection", "Super": "Preview Super" }, "ListStyle": "Display perks as a list", "GridStyle": "Display perks as a grid" }, "Stats": { "Custom": "Custom Total", "CustomDesc": "Custom total of selected base stats, ignoring mods or masterworks. Visit Settings to configure which stats are included.", "Discipline": "Discipline", "Intellect": "Intellect", "MaxGearPower": "Maximum Power of equippable gear", "DropLevel": "Account Power", "DropLevelExplanation1": "Account Power is the base power level when calculating the increased level of rewards.", "DropLevelExplanation2": "Account Power uses the highest level item in each slot, regardless of required Class or the \"One Exotic\" rule.", "EquippableGear": "Equippable Gear", "MaxGearPowerOneExoticRule": "Maximum Power of equippable gear\n(only one Exotic armor piece equipped)", "MaxGearPowerAll": "Maximum Power of all gear", "PowerModifier": "Power granted by seasonal experience progression", "MaxTotalPower": "Maximum total Power", "Milliseconds": "ms", "NoBonus": "No Bonus", "NotApplicable": "N/A", "OfMaxRoll": "{{range}} of max roll", "PercentHelp": "Click for more information about what Stats Quality is.", "Prestige": "Prestige Level: {{level}}\n{{exp}}xp until 5 motes of light.", "Quality": "Stats quality", "Strength": "Strength", "TierProgress": "T{{tier}} {{statName}} ({{progress}}/60 for T{{nextTier}})\n", "TierProgress_Max": "T{{tier}} {{statName}} ({{progress}}/300)\n", "Total": "Total", "MetersPerSecond": "m/s", "Percentage": "%", "HP": "HP", "TimeToFullHP": "Time to Full HP", "WalkingSpeed": "Walking", "StrafingSpeed": "Strafing", "CrouchingSpeed": "Crouching", "TotalHP": "Total HP", "ShieldHP": "Shield HP", "DamageResistance": "PvE Damage Resist", "FlinchResistance": "Flinch Resist", "WeaponPart": "Weapon Part" }, "Storage": { "ApiPermissionPrompt": { "Title": "Enable DIM Sync?", "Description": "DIM can now store your tags, loadouts, and settings on our own servers and sync that data between different versions of DIM, with no separate login. You can import your existing data from the Settings page if you haven't enabled DIM Sync before. This was made possible by the support of our OpenCollective backers!", "Yes": "Enable Sync", "No": "Not right now" }, "AutoBackup": "We've backed up your data to a file in your downloads folder called dim-data.json, just in case.", "BackUpFirst": "You MUST back up your data first, before you delete it all. Just in case.", "BrowserMayClearData": "The browser may delete this information if you run out of space or don't visit DIM frequently.", "DataIsLocal": "Tag and notes data is local only", "DeleteAllData": "Delete ALL Data from DIM Sync Servers", "DeleteAllDataConfirm": "Are you sure you want to delete ALL your data, for all accounts, from DIM Sync? You can't undo this.", "DeleteAllDataLabel": "Wipe DIM Sync Data", "Details": { "IndexedDBStorage": "Local storage will save your information only on this browser. Clearing your browsing data will delete this information." }, "DimApiFinePrint": "DIM will save your tags, loadouts, and settings to the DIM servers and sync them between different versions of DIM.", "DimSyncEnabled": "DIM Sync Enabled", "DimSyncDown": "DIM Sync is not connected due to a problem talking to the server.", "UpdateQueueLength_one": "{{count}} new change will be saved when we can reconnect.", "DimSyncNotEnabled": "DIM Sync is not enabled, so your settings, tags, loadouts, and searches are only stored locally and will be lost if you clear your browser storage. Enable DIM Sync in Settings to back up your data automatically, or regularly back up your data manually.", "UpdateQueueLength_other": "{{count}} new changes will be saved when we can reconnect.", "EnableDimApi": "Enable DIM Sync (recommended)", "Export": "Download Data Backup", "ExportError": "Failed to download backup from DIM Sync", "ExportErrorBody": "DIM Sync may be down, or you are having trouble with your connection. We will download a copy of your locally saved data instead.", "Import": "Import Data Backup", "ImportConfirmDimApi": "Are you sure you want to overwrite your current tags, loadouts, and settings with this version? It will completely replace what you had.", "ImportNotification": { "SuccessTitle": "Import Successful", "SuccessBodyForced": "Imported settings, {{loadouts}} loadouts, and {{tags}} tagged items from your backup into DIM Sync, replacing what was already there.", "SuccessBodyLocal": "Imported settings, {{loadouts}} loadouts, and {{tags}} tagged items from your backup into local storage, replacing what was already there. We cannot guarantee that local storage won't be lost - consider enabling DIM Sync.", "FailedTitle": "Import Failed", "FailedBody": "Unable to import data. {{error}}", "NoData": "No loadouts or tags found in the backup" }, "ImportExport": "Backup & Import", "ImportFailed": "Import Failed! {{error}}", "ImportNoFile": "No file selected!", "ImportTooManyFiles": "Please only select one file to import.", "ImportWrongFileType": "File is not a JSON file. It may not be a DIM backup.", "IndexedDBStorage": "Local Browser Storage", "LearnMore": "Learn more about DIM Sync", "MenuTitle": "Sync & Backups", "ProfileErrorTitle": "DIM Sync Download Error", "ProfileErrorBody": "We had a problem communicating with DIM Sync. Your latest settings, tags, loadouts, and searches may not be shown. Your data is still on our servers, and any updates you make locally will be saved when we can reconnect. We'll keep retrying while DIM is open.", "RefreshDimSync": "Reload remote data from DIM Sync", "UpdateInvalid": "Failed to save data to DIM Sync", "UpdateInvalidBody": "Data sent to DIM Sync was invalid and will not be saved.", "UpdateInvalidBodyLoadout": "The loadout \"{{name}}\" is invalid and will not be saved. If you imported it from another site, please let them know that they are exporting invalid loadouts.", "UpdateErrorTitle": "DIM Sync Save Error", "UpdateErrorBody": "We had a problem saving your data to DIM Sync. We'll keep retrying while DIM is open.", "Usage": "DIM is using {{usage, humanBytes}} out of {{quota, humanBytes}} available to it on this device. This includes the downloaded Destiny item databases from Bungie.net." }, "StripSockets": { "Action": "Strip Sockets", "Button": "Strip {{numSockets}} Sockets", "Choose": "Choose Sockets to strip", "Running": "Stripping Sockets", "Done": "Stripped Sockets", "Ok": "Ok", "Cancel": "Cancel", "NoSockets": "No Sockets to clear", "Shaders": "{{count}}x Shader", "Ornaments": "{{count}}x Ornament", "WeaponMods": "{{count}}x Weapon Mod", "DiscountedMods": "{{count}}x Discounted Mod", "ArmorMods": "{{count}}x Armor Mod", "Subclass": "{{count}}x Subclass Option", "Others": "{{count}}x Ghost Projection" }, "StreamDeck": { "name": "Stream Deck", "Tooltip": { "Title": "DIM Stream Deck Plugin", "Version": "Version:", "Application": "Stream Deck Application", "Plugin": "Plugin", "Error": "Your Stream Deck plugin is no longer supported. Please update to the latest version. This plugin requires at least:", "AuthRequired": "Click this button or go to settings and click \"Connect application\".", "ExtensionIssue": "Extensions Issue", "ErrorConnection": "if you're already using the latest version, check if some browser extension is blocking the connection." }, "Error": { "Title": "Stream Deck Plugin Error", "Body": "There was an error sending data to the Stream Deck plugin. Please contact the plugin developer. {{error}}" }, "Enable": "Stream Deck Plugin", "FinePrint": "Enable the connection with the DIM Stream Deck plugin. This plugin is a separate project that is neither written by nor supported by the DIM team.", "Install": "Install plugin", "Authorize": "Connect application", "MissingAuthorization": "You must authorize the Stream Deck application to connect to DIM. Go to settings and click \"Connect application\"." }, "Tags": { "Archive": "Archive", "ClearTag": "Clear Tag", "Favorite": "Favorite", "Infuse": "Infuse", "Junk": "Junk", "Keep": "Keep", "LockAll": "Lock Items", "TagItem": "Tag Item", "UnlockAll": "Unlock Items" }, "Triage": { "BetterWorseArmor": "Better/Worse Armor", "BetterWorseIncludes": "Identifies armor pieces with:", "WorseArmor": "Strictly Worse Armor", "WorseStatArmor": "Worse Stat Armor", "WorseArtificeArmor": "Worse Non-Artifice Armor", "WorseStatArtificeArmor": "Worse Stat Non-Artifice Armor", "StatWorseArmorDesc": "No better stats, and at least one worse stat.", "PerkWorseArmorDesc": "The same intrinsic perk, or none.", "BetterArmor": "Strictly Better Armor", "BetterStatArmor": "Better Stats Armor", "BetterArtificeArmor": "Better Artifice Armor", "BetterStatArtificeArmor": "Better Stat Artifice Armor", "StatBetterArmorDesc": "All stats at least as high, and at least one stat better.", "PerkBetterArmorDesc": "The same, or more, intrinsic perks or special mod slots.", "AccountsForArtifice": "This tests whether an Artifice armor piece could be better, if a +3 stat mod were used.", "StatNotPerkArmorDesc": "This tests only stats. A lower piece may still have special mod slots or intrinsic perks.", "YourBestItem": "Your best item", "ThisItem": "This item", "SimilarItems": "Similar Items", "InLoadouts": "In Loadouts", "HighStats": "High Stats", "OwnedCount": "# Owned" }, "Triumphs": { "HideCompleted": "Hide completed triumphs", "RevealRedacted": "Reveal redacted triumphs", "SortRecords": "Sort triumphs by completion", "GildingTriumph": "Gilding Triumph" }, "Vendors": { "Collections": "Collections", "Engram": "Rank", "FilterToUnacquired": "Only show uncollected items", "HideSilverItems": "Hide Silver items", "NoItems": "This Vendor is currently not offering any items.", "Vendors": "Vendors", "RefreshTime": "Inventory refreshes in:" }, "Views": { "About": { "APIHistory": "View the history of all actions taken by DIM (and other Destiny apps)", "BungieCopyright": "All images and content are property of Bungie.", "CommunityInsight": "Community Insights for Perks and Character Stats courtesy of {{clarityLink}}. If you notice inaccuracies or have questions, join the {{clarityDiscordLink}}.", "Discord": "Discord", "DiscordHelp": "Ask questions, give feedback, and get support in our Discord channels.", "FAQ": "Frequently Asked Questions", "FAQAccess": "How does DIM get access to my Destiny data?", "FAQAccessAnswer": "We use Bungie's app authentication to grant access to DIM to see and move your items. DIM never sees your username or password. This is the same way the Companion app works.", "FAQKeyboard": "Does DIM support keyboard shortcuts?", "FAQKeyboardAnswer": "Yes! Press \"?\" to see a list of available shortcuts.", "FAQLogout": "How can I log out of DIM?", "FAQLogoutAnswer": "Open the menu from the top left icon and choose \"Log Out\"", "FAQLostItem": "I lost my item using your tool!", "FAQLostItemAnswer": "Bungie doesn't allow apps to delete items (even their own app!). More than likely a transfer failed, leaving your item in the vault or on another character. You could search for the item. If that doesn't turn it up, reload the page. Check {{link}} or in game to see if your item still exists. We're sure it's still there.", "FAQMobile": "Does DIM support mobile? Will there be an app?", "FAQMobileAnswer": "The DIM website can be loaded on phones and tablets today, and you can add it to your home screen for an app-like experience.", "GitHub": "GitHub", "GitHubHelp": "If you're interested in contributing to the project, visit us at our project page on {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM is a free, open source app built by community developers upon the same services used by Bungie.net and the Destiny Companion App.", "Schedule": { "beta": "This beta version of DIM is updated every time we change the code - it gets the latest features and fixes, but also the latest bugs!", "release": "This version of DIM is updated once a week, at approximately midnight on Sundays, US Pacific time." }, "Translation": "Join the Translation Team!", "TranslationText": "We use {{link}} for ease of translation. If you want to improve one of DIM's translations, join the team.", "Version": "Version {{version}} ({{flavor}}), built on {{date}}", "Wiki": "DIM User Guide", "WikiHelp": "Learn how to use DIM's features." }, "Login": { "Auth": "Authorize with Bungie.net", "BackupPrompt": "Are you sure you want to enable DIM Sync without a backup?", "EnableDimSyncWarning": "You had previously disabled DIM Sync and were only using local data storage. Enabling DIM Sync will replace any local data with the data from DIM Sync. You should back up your data before enabling DIM Sync. You can restore from that backup in Settings.", "DisabledDimSyncWarning": "You had previously enabled DIM Sync and were only using local data storage. Enabling DIM Sync will replace any local data with the data from DIM Sync. You should back up your data before enabling DIM Sync. You can restore from that backup in Settings.", "Explanation": "Allow DIM to view and modify your Destiny characters, vault, and progression.", "LearnMore": "Learn more about accounts and login", "NewAccount": "Log in with a different Bungie.net account", "Permission": "We need your permission..." }, "Support": { "BackersDetail": "Support us with a one-time or monthly donation and help us continue our active development.", "FreeToDownload": "DIM is a product that is free to download and use. The source code for DIM is open source and free for anyone to enhance. You will never see an ad in DIM. That is our commitment.", "OpenCollective": "We are using {{link}} as a service to provide compensation to our developers for their dedication and time spent on this project.", "Store": "We have merch with our logo and other designs for sale on {{link}}", "Support": "Support DIM" } }, "WishListRoll": { "BestRatedTip_one": "This perk exactly matches a weapon roll on your wishlist.", "BestRatedTip_other": "These perks exactly match a weapon roll on your wishlist.", "WorstRatedTip_one": "This perk exactly matches a weapon roll on your trashlist.", "WorstRatedTip_other": "These perks exactly match a weapon roll on your trashlist.", "Clear": "Clear Wish List", "ExternalSource": "Add another wish list", "ExternalSourcePlaceholder": "Paste wish list URL here", "Header": "Wish List", "Import": "Load Wish List Rolls", "ImportFailed": "None of your wish lists contained any valid rolls.", "ImportError": "Error loading wish list from \"{{url}}\": {{error}}", "ImportNoFile": "No file selected.", "InvalidExternalSource": "Please enter a valid URL for your external wish list source. The URL must start with one of the following:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "Last updated: {{lastUpdatedDate}} at {{lastUpdatedTime}}", "Num": "{{num, number}} rolls in your wish list", "NumRolls": "{{num, number}} rolls", "DupeRolls": " (+{{num, number}} ignored dupes)", "PreMadeFiles": "Use A Pre-Made Wish List", "Refresh": "Refresh Wishlist", "SourceAlreadyAdded": "Wish List already added", "UpdateExternalSource": "Add Wish List", "Untitled": "Untitled Wish List", "Voltron": "voltron (default)", "WishListNotes": "Wish List Notes:", "CopyLine": "Copy Selected Perks as Wish List Roll", "CopiedLine": "Wish List roll copied to clipboard" }, "wrong-level": "wrong-level", "no-space": "no-space" } ================================================ FILE: config/manifest-webapp.ts ================================================ export default function createWebAppManifest(publicPath: string) { return { name: 'Destiny Item Manager', short_name: 'DIM', description: 'An item and loadout manager for Destiny.', icons: [ { src: `${publicPath}android-chrome-192x192-6-2018.png`, sizes: '192x192', type: 'image/png', }, { src: `${publicPath}android-chrome-512x512-6-2018.png`, sizes: '512x512', type: 'image/png', }, { src: `${publicPath}android-chrome-mask-512x512-6-2018.png`, sizes: '512x512', type: 'image/png', purpose: 'maskable', }, ], shortcuts: [ { name: 'Vendors', url: `${publicPath}vendors`, description: "View vendors' wares", }, { name: 'Loadouts', url: `${publicPath}loadouts`, description: 'Create & share loadouts', }, { name: 'Settings', url: `${publicPath}settings`, }, ], screenshots: [ { src: `${publicPath}screenshots/inventory.jpg`, sizes: '1902x1080', type: 'image/jpeg', form_factor: 'wide', label: 'DIM Inventory Screen', }, { src: `${publicPath}screenshots/mobile.jpg`, sizes: '390x770', type: 'image/jpeg', form_factor: 'narrow', label: 'DIM on Mobile', }, ], theme_color: '#000000', background_color: '#000000', display: 'standalone', display_override: ['window-controls-overlay'], categories: ['games', 'entertainment', 'productivity', 'utilities'], start_url: `${publicPath}?utm_source=homescreen`, launch_handler: { client_mode: ['navigate-existing'], }, }; } ================================================ FILE: config/notify-webpack-plugin.ts ================================================ // Originally from https://github.com/joshhunt/notify-webpack-plugin/, converted to TypeScript import { type Compiler, type Stats } from '@rspack/core'; import chalk from 'chalk'; export default class NotifyPlugin { name: string; firstRun: boolean; hideChildren: boolean; constructor(name: string, isProd?: boolean) { this.name = name; this.firstRun = true; this.hideChildren = !isProd; } apply(compiler: Compiler) { // Hack to get rid of the 'Child extract-text...' log spam compiler.hooks.done.tap('NotifyPlugin', (stat) => { stat.compilation.children = this.hideChildren ? [] : stat.compilation.children; }); compiler.hooks.compile.tap('NotifyPlugin', () => { const action = this.firstRun ? 'starting to build' : 'updating'; console.log('==> ' + chalk.cyan(`Webpack is ${action} ${this.name}...`)); }); compiler.hooks.done.tap('NotifyPlugin', this.onDone.bind(this)); } onDone(rawWebpackStats: Stats) { const { time } = rawWebpackStats.toJson({ timings: true }); const action = this.firstRun ? 'building' : 'updating'; console.log('==> ' + chalk.green(`Webpack finished ${action} ${this.name} in ${time}ms`)); this.firstRun = false; } } ================================================ FILE: config/webpack.ts ================================================ import { InjectManifest } from '@aaroon/workbox-rspack-plugin'; import filterWebpackStats from '@bundle-stats/plugin-webpack-filter'; import { type Configuration, rspack } from '@rspack/core'; import ReactRefreshPlugin from '@rspack/plugin-react-refresh'; import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; import GenerateJsonPlugin from 'generate-json-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import SondaWebpackPlugin from 'sonda/webpack'; import { TsCheckerRspackPlugin } from 'ts-checker-rspack-plugin'; import { StatsWriterPlugin } from 'webpack-stats-plugin'; import NotifyPlugin from './notify-webpack-plugin.ts'; import browserslist from 'browserslist'; import { execSync } from 'child_process'; import fs from 'fs'; import svgToMiniDataURI from 'mini-svg-data-uri'; import { resolve } from 'node:path'; import zlib from 'zlib'; import csp from './content-security-policy.ts'; import { makeFeatureFlags } from './feature-flags.ts'; const ASSET_NAME_PATTERN = 'static/[name]-[contenthash:6][ext]'; import packageJson from '../package.json' with { type: 'json' }; import createWebAppManifest from './manifest-webapp.ts'; import splash from '../icons/splash.json' with { type: 'json' }; // https://stackoverflow.com/questions/69584268/what-is-the-type-of-the-webpack-config-function-when-it-comes-to-typescript type CLIValues = boolean | string; type EnvValues = Record>; interface Env extends EnvValues { release: boolean; beta: boolean; dev: boolean; pr: boolean; name: 'release' | 'beta' | 'dev' | 'pr'; } type Argv = Record; export interface WebpackConfigurationGenerator { (env?: Env, argv?: Argv): Configuration | Promise; } export default (env: Env) => { env.name = Object.keys(env)[0] as Env['name']; (['release', 'beta', 'dev', 'pr'] as const).forEach((e) => { // set booleans based on env env[e] = Boolean(env[e]); if (env[e]) { env.name = e; } }); if (env.dev && env.WEBPACK_SERVE && (!fs.existsSync('key.pem') || !fs.existsSync('cert.pem'))) { console.log('Generating certificate'); execSync('mkcert create-ca --validity 825'); execSync('mkcert create-cert --validity 825 --key key.pem --cert cert.pem'); } let version = env.dev ? packageJson.version.toString() : process.env.VERSION; if (!env.dev) { console.log('Building DIM version ' + version); } const buildTime = Date.now(); const publicPath = process.env.PUBLIC_PATH ?? '/'; const featureFlags = makeFeatureFlags(env); const contentSecurityPolicy = csp(env.name, featureFlags, version); const analyticsProperty = env.release ? 'G-1PW23SGMHN' : 'G-MYWW38Z3LR'; const jsFilenamePattern = env.dev ? '[name]-[fullhash].js' : '[name]-[contenthash:8].js'; const cssFilenamePattern = env.dev ? '[name]-[fullhash].css' : '[name]-[contenthash:8].css'; const lightningCssLoader = { loader: 'builtin:lightningcss-loader', /** @type {import('@rspack/core').LightningcssLoaderOptions} */ options: { targets: packageJson.browserslist, }, }; const config: Configuration = { mode: env.dev ? ('development' as const) : ('production' as const), entry: { main: './src/Index.tsx', browsercheck: './src/browsercheck.js', earlyErrorReport: './src/earlyErrorReport.js', authReturn: './src/authReturn.ts', backup: './src/backup.ts', }, // https://github.com/webpack/webpack-dev-server/issues/2758 // target: env.dev ? 'web' : 'browserslist', target: 'browserslist', output: { path: resolve('./dist'), publicPath, filename: jsFilenamePattern, chunkFilename: jsFilenamePattern, assetModuleFilename: ASSET_NAME_PATTERN, hashFunction: 'xxhash64', }, // Dev server devServer: env.dev ? { host: process.env.DOCKER ? '0.0.0.0' : 'localhost', allowedHosts: 'all', server: { type: 'https', options: { key: fs.readFileSync('key.pem'), // Private keys in PEM format. cert: fs.readFileSync('cert.pem'), // Cert chains in PEM format. }, }, devMiddleware: { stats: 'errors-only', }, client: { overlay: false, logging: 'none', // we don't need to see build errors in the console log }, historyApiFallback: true, hot: 'only', liveReload: false, headers: (req) => { // This mirrors what's in .htaccess - headers for html paths, COEP for JS. const headers: Record = req.url?.match(/^[^.]+$/) ? { 'Content-Security-Policy': contentSecurityPolicy, // credentialless is only supported by chrome but require-corp blocks Bungie.net messages // Disabled for now as it blocks Google fonts //'Cross-Origin-Embedder-Policy': 'credentialless', //'Cross-Origin-Opener-Policy': 'same-origin', } : req.url?.match(/\.js$/) ? { // credentialless is only supported by chrome but require-corp blocks Bungie.net messages //'Cross-Origin-Embedder-Policy': 'require-corp', } : {}; return headers; }, } : undefined, // Bail and fail hard on first error bail: !env.dev, stats: env.dev ? 'minimal' : 'normal', devtool: 'source-map', performance: { // Don't warn about too-large chunks hints: false, }, optimization: { // We always want the chunk name, otherwise it's just numbers // chunkIds: 'named', // Extract the runtime into a separate chunk. runtimeChunk: 'single', splitChunks: { chunks(chunk) { return chunk.name !== 'browsercheck' && chunk.name !== 'earlyErrorReport'; }, automaticNameDelimiter: '-', }, minimizer: [ new rspack.SwcJsMinimizerRspackPlugin({ minimizerOptions: { ecma: 2020, module: true, format: { comments: false, }, compress: { passes: 3, toplevel: true, unsafe: true, unsafe_math: true, unsafe_proto: true, pure_getters: true, pure_funcs: [ 'JSON.parse', 'Object.values', 'Object.keys', 'Object.groupBy', 'Object.fromEntries', 'Map.groupBy', 'Map', 'Set', 'BigInt', ], }, mangle: { toplevel: true }, }, extractComments: false, }), new rspack.LightningCssMinimizerRspackPlugin({ minimizerOptions: { targets: packageJson.browserslist, }, }), ], }, module: { parser: { javascript: { exportsPresence: 'error', }, }, rules: [ { test: /\.js$/, exclude: [/node_modules/, /browsercheck\.js$/], use: [ { loader: 'babel-loader', options: { cacheDirectory: true, }, }, ], }, { // Optimize SVGs - mostly for destiny-icons. test: /\.svg$/, exclude: /data\/webfonts\//, resourceQuery: { not: [/react/] }, type: 'asset', generator: { dataUrl: (content: any) => svgToMiniDataURI(content.toString()), }, parser: { dataUrlCondition: { maxSize: 5 * 1024, // only inline if less than 5kb }, }, use: env.dev ? [] : [ { loader: 'svgo-loader', }, ], }, // Allow importing SVGs as React components if *.svg?react { test: /\.svg$/i, issuer: /\.[jt]sx?$/, resourceQuery: /react/, // only create react component if *.svg?react use: [ { loader: '@svgr/webpack', options: { memo: true, svgProps: { fill: 'currentColor' }, }, }, ], }, { test: /\.(jpg|gif|a?png|eot|ttf|woff(2)?)(\?v=\d+\.\d+\.\d+)?/, type: 'asset', parser: { dataUrlCondition: { maxSize: 5 * 1024, // only inline if less than 5kb }, }, }, // *.m.scss will have CSS Modules support { test: /\.m\.scss$/, use: [ env.dev ? 'style-loader' : rspack.CssExtractRspackPlugin.loader, { loader: '@bhollis/css-modules-typescript-loader', options: { mode: process.env.CI ? 'verify' : 'emit', }, }, { loader: 'css-loader', options: { modules: { localIdentName: !env.release ? '[name]_[local]-[contenthash:base64:8]' : '[contenthash:base64:8]', exportLocalsConvention: 'camelCaseOnly', }, importLoaders: 2, }, }, lightningCssLoader, { loader: 'sass-loader', options: { sassOptions: { quietDeps: true } } }, ], type: 'javascript/auto', }, // Regular *.scss are global { test: /\.scss$/, exclude: /\.m\.scss$/, use: [ env.dev ? 'style-loader' : rspack.CssExtractRspackPlugin.loader, 'css-loader', lightningCssLoader, { loader: 'sass-loader', options: { sassOptions: { quietDeps: true } } }, ], type: 'javascript/auto', }, { test: /\.css$/, use: [ env.dev ? 'style-loader' : rspack.CssExtractRspackPlugin.loader, 'css-loader', lightningCssLoader, ], type: 'javascript/auto', }, // All files with a '.ts' or '.tsx' extension will be handled by 'babel-loader'. { test: /\.tsx?$/, exclude: [/testing/, /\.test\.ts$/], use: [ { loader: 'babel-loader', options: { cacheDirectory: true, }, }, env.dev ? null : { loader: 'ts-loader', }, ].filter((l) => l !== null), }, // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. { enforce: 'pre', test: /\.jsx?$/, loader: 'source-map-loader', }, { test: /\.json/, include: /(src(\/|\\)locale)|(i18n\.json)/, type: 'asset/resource', generator: { filename: '[name]-[contenthash:8][ext]', }, }, { // Force webfonts to be separate files instead of having small ones inlined into CSS test: /data\/webfonts\//, type: 'asset/resource', }, { type: 'javascript/auto', test: /\.wasm/, }, { test: /CHANGELOG\.md$/, use: [ { loader: 'html-loader', }, { loader: 'markdown-loader', }, ], }, ], noParse: /manifests/, }, resolve: { extensions: ['.js', '.json', '.ts', '.tsx', '.jsx'], // Install aliases from tsconfig tsConfig: resolve('./tsconfig.json'), alias: { 'textarea-caret': resolve('./src/app/utils/textarea-caret'), }, fallback: { http: false, https: false, http2: false, util: false, zlib: false, browser: false, process: false, os: false, constants: false, fs: false, path: false, }, }, plugins: [], }; const plugins: any[] = [ new rspack.IgnorePlugin({ resourceRegExp: /caniuse-lite\/data\/regions/ }), new NotifyPlugin('DIM', !env.dev), new rspack.CssExtractRspackPlugin({ filename: cssFilenamePattern, chunkFilename: cssFilenamePattern, ignoreOrder: true, }), // TODO: prerender? new HtmlWebpackPlugin({ inject: false, filename: 'index.html', template: 'src/index.html', chunks: ['earlyErrorReport', 'main', 'browsercheck'], templateParameters: { version, date: new Date(buildTime).toString(), splash, analyticsProperty, publicPath, }, minify: env.dev ? false : { collapseWhitespace: true, keepClosingSlash: true, removeComments: false, removeRedundantAttributes: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, useShortDoctype: true, }, }), new HtmlWebpackPlugin({ inject: true, filename: 'return.html', template: 'src/return.html', chunks: ['authReturn'], }), new HtmlWebpackPlugin({ inject: true, filename: 'backup.html', template: 'src/backup.html', chunks: ['backup'], }), new HtmlWebpackPlugin({ inject: false, filename: '404.html', template: 'src/404.html', }), // Generate the .htaccess file (kind of an abuse of HtmlWebpack plugin just for templating) new HtmlWebpackPlugin({ filename: '.htaccess', template: 'src/htaccess', inject: false, minify: false, templateParameters: { publicPath: publicPath.replace('/', ''), csp: contentSecurityPolicy, }, }), // Generate a version info JSON file we can poll. We could theoretically add more info here too. new GenerateJsonPlugin('./version.json', { version, buildTime, }), // The web app manifest controls how our app looks when installed. new GenerateJsonPlugin('./manifest-webapp.json', createWebAppManifest(publicPath)), new rspack.CopyRspackPlugin({ patterns: [ // Only copy the manifests out of the data folder. Everything else we import directly into the bundle. { from: './src/data/d1/manifests', to: 'data/d1/manifests' }, { from: `./icons/${env.name}/` }, { from: `./icons/splash`, to: 'splash/' }, { from: `./icons/screenshots`, to: 'screenshots/' }, { from: './src/safari-pinned-tab.svg' }, { from: './src/nuke.php' }, { from: './src/robots.txt' }, ], }), new rspack.DefinePlugin({ $DIM_VERSION: JSON.stringify(version), $DIM_FLAVOR: JSON.stringify(env.name), $DIM_BUILD_DATE: JSON.stringify(buildTime), // These are set from the GitHub secrets $DIM_WEB_API_KEY: JSON.stringify(process.env.WEB_API_KEY), $DIM_WEB_CLIENT_ID: JSON.stringify(process.env.WEB_OAUTH_CLIENT_ID), $DIM_WEB_CLIENT_SECRET: JSON.stringify(process.env.WEB_OAUTH_CLIENT_SECRET), $DIM_API_KEY: JSON.stringify(process.env.DIM_API_KEY), $ANALYTICS_PROPERTY: JSON.stringify(analyticsProperty), $PUBLIC_PATH: JSON.stringify(publicPath), $BROWSERS: JSON.stringify(browserslist(packageJson.browserslist)), // Feature flags! ...Object.fromEntries( Object.entries(featureFlags).map(([key, value]) => [ `$featureFlags.${key}`, JSON.stringify(value), ]), ), }), ]; if (env.dev) { // In dev we use babel to compile TS, and fork off a separate typechecker plugins.push(new TsCheckerRspackPlugin()); plugins.push(new ReactRefreshPlugin({ overlay: false })); } else { // env.beta and env.release plugins.push( new StatsWriterPlugin({ filename: '../webpack-stats.json', stats: { assets: true, entrypoints: true, chunks: true, modules: true, excludeAssets: [ /data\/d1\/manifests\/d1-manifest-..(-br)?.json(.br|.gz)?/, /^(?!en).+.json/, /webpack-stats.json/, /screenshots\//, /\.br$/, ], }, transform: (webpackStats) => { const filteredSource = filterWebpackStats(webpackStats); return JSON.stringify(filteredSource); }, }), new SondaWebpackPlugin({ format: 'html', outputDir: 'sonda-report', open: false, deep: true, sources: true, gzip: false, brotli: false, exclude: [/\.br$/, /\.gz$/, /\/manifests\//, /webpack-stats\.json/], }), new rspack.CopyRspackPlugin({ patterns: [ { from: `./config/.well-known/android-config${env.release ? '' : '.beta'}.json`, to: '.well-known/assetlinks.json', }, { from: `./config/.well-known/apple-config.json`, to: '.well-known/apple-app-site-association', toType: 'file', }, ], }), new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['node_modules/.cache'], }), // Tell React we're in Production mode new rspack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), 'process.env': JSON.stringify({ NODE_ENV: 'production' }), }), // Generate a service worker new InjectManifest({ include: [/\.(html|js|css|woff2|json|wasm)$/, /static\/(?!fa-).*\.(a?png|gif|jpg|svg)$/], exclude: [ /version\.json/, // Ignore both the webapp manifest and the d1-manifest files /data\/d1\/manifests/, /manifest-webapp/, // Android and iOS manifest /\.well-known/, /screenshots\//, ], swSrc: './src/service-worker.ts', swDest: 'service-worker.js', }), ); // Skip brotli compression for PR builds if (!process.env.PR_BUILD) { plugins.push( // Brotli-compress all assets. We used to gzip too but everything supports brotli now new CompressionPlugin({ filename: '[path][base].br', algorithm: 'brotliCompress', exclude: /data\/d1\/manifests/, // Skip .woff and .woff2, they're already well compressed test: /\.js$|\.css$|\.html$|\.json$|\.map$|\.ttf$|\.eot$|\.svg$|\.wasm$/, compressionOptions: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11, }, minRatio: Infinity, }), ); } } config.plugins = plugins; return config; }; ================================================ FILE: crowdin.yml ================================================ "project_id_env": "CROWDIN_PROJECT_ID" "api_token_env": "CROWDIN_PERSONAL_TOKEN" "base_path": "." "files": [ { "source": "src/locale/en.json", "translation": "src/locale/%android_code%.json", "type": "i18next_json" } ] ================================================ FILE: cspell.json ================================================ { "version": "0.2", "language": "en", "dictionaries": [ "workspace", "workspace-bungie-names", "workspace-usernames", "workspace-programming" ], "dictionaryDefinitions": [ { "name": "workspace", "path": "./config/cspell/dim-dict.txt", "description": "DIM Workspace Dictionary", "addWords": true, "scope": "workspace" }, { "name": "workspace-bungie-names", "path": "./config/cspell/bungie-dict.txt", "description": "DIM Workspace Dictionary - Bungie", "addWords": true, "scope": "workspace" }, { "name": "workspace-usernames", "path": "./config/cspell/dim-username-dict.txt", "description": "DIM Workspace Dictionary - Usernames", "addWords": true, "scope": "workspace" }, { "name": "workspace-programming", "path": "./config/cspell/programming-dict.txt", "description": "DIM Workspace Dictionary - Programming Syntax", "addWords": true, "scope": "workspace" } ], "allowCompoundWords": true, "flagWords": [ "langauge" ], "ignorePaths": [ ".vscode/extensions.json", ".github/workflows/**", "node_modules/", "dist/", "icons/", "manifest-cache/", "destiny-icons/**", "icons/build_icons.js", "config/content-security-policy.js", "src/browsercheck-utils.js", "src/data/", "src/images/", "src/locale/de.json", "src/locale/es.json", "src/locale/esMX.json", "src/locale/fr.json", "src/locale/it.json", "src/locale/ja.json", "src/locale/ko.json", "src/locale/pl.json", "src/locale/ptBR.json", "src/locale/ru.json", "src/locale/zhCHS.json", "src/locale/zhCHT.json", "src/htaccess", ".eslintrc", "babel.config.js", "cspell.json", "jest.config.js", "package.json", "tsconfig.json", "pnpm-lock.yaml", ".gitignore", "webpack-stats.json", ".stylelintrc" ], "words": [ "unenhanced" ] } ================================================ FILE: docker-compose.yml ================================================ version: '3' services: webpack: container_name: dim-webpack image: node:24 ports: - 8080:8080 working_dir: /usr/src/app command: > bash -c "git config --global --add safe.directory /usr/src/app && corepack enable && pnpm install --frozen-lockfile --prefer-offline && pnpm start" environment: - NODE_ENV=development - DOCKER=true restart: 'no' volumes: - .:/usr/src/app - ./config/:/usr/src/app - node_modules:/usr/src/app/node_modules volumes: node_modules: {} networks: dim: name: dim ================================================ FILE: docs/CHANGELOG.md ================================================ ## Next * Restyle Collections Armor to match Universal Ornaments. * d2foundry.gg is back, and so are our links to it. ## 8.122.0 (2026-04-26) ## 8.121.0 (2026-04-19) ## 8.120.0 (2026-04-12) ## 8.119.0 (2026-04-05) * Restyled Stat trackers on Records page and added their gilding requirements * The warning tooltip for mismatched perk hashes in wishlists will now show the perk name in addition to the perk hash. * Removed links to d2foundry.gg which has sadly shut down. * Fixed tooltips showing unnecessarily due to a bug in iOS 26. ## 8.118.0 (2026-03-29) ## 8.117.0 (2026-03-22) * Add Territorial Profit quest and other milestone-based quest steps to Progress tab ## 8.116.0 (2026-03-15) ## 8.115.0 (2026-03-08) ## 8.114.0 (2026-03-01) ## 8.113.0 (2026-02-22) ## 8.112.0 (2026-02-15) ## 8.111.0 (2026-02-08) ## 8.110.0 (2026-02-01) ## 8.109.0 (2026-01-25) ## 8.108.1 (2026-01-23) * Fix Loadout Optimizer being unable to plan Artifice mods for Exotic armor with a locked Artifice mod slot. * Fix scrolling not working on Loadout Optimizer on Chrome 144 for Android. ## 8.108.0 (2026-01-18) ## 8.107.0 (2026-01-11) ## 8.106.0 (2026-01-04) * Add setting to toggle display of ornaments and view ornaments when hovering over items * Limit ornament icon swapping to armor only ## 8.105.0 (2025-12-28) * Fix `exactperk/perkname` matching against a perk's type, instead of just its name. * Added the Synthweave bounty counter to Ada-1's bounty descriptions ## 8.104.0 (2025-12-21) * Fix the masterwork socket on crafted and enhanced weapons incorrectly showing a masterwork tier number ## 8.103.1 (2025-12-16) ## 8.103.0 (2025-12-14) * Added heat-related weapon stats. * Improved mobile view for loadouts. ## 8.102.0 (2025-12-07) * DIM should load faster even when it is having trouble loading DIM API data. * Festival masks will now show up in the Organizer. * Tiered Engrams in postmaster will now match `is:engrams` * Fix crash in Loadout Optimizer when changing characters. * Include more ingame loadout identification info in Triage loadouts list. * Fixed the well-rested XP calculation being too low for the first 5 levels of the season pass. * Added a toggle to display vaulted items underneath equipped items in the desktop view. * Fixed the default shader saying it cannot roll. ## 8.101.1 (2025-12-02) * Fixed a crash on the Records page caused by Seasonal Challenges being removed from the game. ## 8.101.0 (2025-11-30) * Show weapon frame info on item icons. * Allow sorting inventory by weapon frame * support newline escape sequences (\n) in notes ## 8.100.0 (2025-11-23) * Fix search/Compare issues with item names containing quotes. ## 8.99.0 (2025-11-16) ## 8.98.1 (2025-11-11) * Display 5 tier pips (instead of none) for bugged Call to Arms Tier 6 holofoil weapons, so there are no misunderstandings. ## 8.98.0 (2025-11-09) ## 8.97.0 (2025-11-02) * Fix being able to remove vendor items individually from Compare. ## 8.96.0 (2025-10-26) * Loadout Optimizer allows Festival of the Lost masks to complete a set bonus. You'll still need to plug the right mod in. ## 8.95.0 (2025-10-19) * Show a simple ETA countdown for long Loadout Optimizer jobs. * Added inventory sorting by armor archetype or special modslot. ## 8.94.0 (2025-10-12) * Change Organizer to display Tertiary Stat, and Tuning Stats with stat names instead of stat hashes in armor.csv export * Allowed stripping Combat Flair mods with the Strip Sockets feature. * Added `tunedstat:unfocused` filter to match items whose tuning stat is not one of the focused stats. * Removed max light loadout from the Loadouts page because some folks are bothered by it. ## 8.93.0 (2025-10-05) * Fixed an issue where loadouts might assign tuning mods in a different order than they were shown in Loadout Optimizer, resulting in different stats. * Added "Clear all unselected" button to compare menu. * Added the stat archetype (or mod slot) to the top right corner of armor item tiles. * Restore progress bars on incomplete D1 items. * Fixed stat bars in Compare getting squished when the icon size setting is large. * Restored the setting for how many spaces to clear in Farming Mode. * Removed the "new item" dot and tracking. This has been gone in Beta for a while now, and the recommendation is to use the Item Feed and tagging to keep tabs on your loot as it drops. * Fixed a case where some old searches could not be deleted/unsaved. * Added `is:artifice` filter to find Artifice armor. * Keep in-game loadouts visible while filtering loadouts ## 8.92.0 (2025-09-28) * Reduced the height of the automatic Max Light loadout on the Loadouts page. * Fix Loadout names being forced to uppercase. * Add supplied set bonuses to loadouts on the Loadouts page. * Fixed "Wrong Stat Minimums" showing up on loadouts without stat constraints. ## 8.91.0 (2025-09-21) * Updated item tiles to more closely match in-game tiles, with higher quality images. * Remove armor energy capacity from the item tile. It's just not that interesting these days. * Added Max power loadout to Loadouts tab * Items in loadout optimizer sets will now reflect changes like lock/unlock or being masterworked in-game. * Hide artifact power on its tile since artifact power has been removed. * Fix tuner mods being automatically assigned in Optimizer even when auto-mods was switched off. * Removed item Tier pips from engrams. ## 8.90.1 (2025-09-18) * Fixed some cases where the enhanced version of perks would not match wishlists that specified the unenhanced version. * Replaced empty or mismatched mod slot icons with ones that match the activity they're used in. * Fixed a crash using the Streamdeck plugin. * `tag:none` no longer selects untaggable objects like materials and abilities ## 8.90.0 (2025-09-14) * Updated tuned stat icon. * `dupe:perks` and `dupe:traits` will ignore perks' enhancement status. * Fixed icons displaying too large on the gear power tooltip. * Loadout Optimizer result sets now show which set bonuses they activate. * Added `tunedstat:primary`, `tunedstat:secondary`, and `tunedstat:tertiary`. * Distinguish between special and primary ammo sidearms/pulse rifles in `dupe:traits` and `dupe:perks` comparison. ## 8.89.1 (2025-09-09) * In Compare and Organizer, there are now separate columns/rows for archetype and perks. * Added armor masterwork tier, tertiary stat, and tuning stat columns to Organizer and CSV output. * Reorganized weapons columns in Organizer a bit. * Added `dupe:traits` for finding weapons with duplicate traits. ## 8.89.0 (2025-09-07) * Reduced how much items are dimmed out in the Item Feed when they don't match the current search. * BETA: Option to compare by base masterworked stats in Compare feature. This allows a fair comparison between Armor 2.0 and Armor 3.0. * Added a setting to control how many CPU cores can be used by Loadout Optimizer/Analyzer. * Reorganized the Settings page. * Loadout Optimizer no longer excludes pieces with the requested Set Bonus, even if their stats are terrible. * Fix wishlists not properly matching some new enhanced perks * The perk list vs. grid setting is now saved independently for mobile and desktop views. * Un-deprecated the `is:infusionfodder` filter. * Removed redundant holofoil overlay. * Compare/Organizer sorting now takes Tuning Mods and Artifice armor into account when sorting Totals or Custom Stats. * Totals and Custom stats in Compare/Organizer have an indicator when Tuners or Artifice mods can contribute. * Invalid wish list rolls are now shown in Armory with a tooltip that explains them. * Loadout analyzer is more precise about calling out invalid search queries vs. loadouts whose search query excludes some of its armor. * Added wishlist title/description and link to source to the Armory page. ## 8.88.0 (2025-08-31) * Tuning mods can be chosen manually in the loadout editor, and will be assigned to compatible items when the loadout is applied. The equipped loadout and any snapshotted in-game loadouts will retain their tuning mods. * Loadout Optimizer will no longer collapse sets with items that have the exact same stats. Now you'll see a separate set for each copy. * Combine the set bonus tooltips for items of the same set in character status * Adjust enhanced perk arrow when perk name takes up more than 1 line * Loadout Optimizer now automatically assigns Tier 5 tuning mods where available. This can make major differences in what stats you can achieve! * Loadout Optimizer will now utilize multiple CPU cores. * Bulk locking/unlocking items will skip over items that cannot be locked. * `is:locked` and `is:unlocked` searches will never match items that cannot be locked. * Finishers are no longer lockable (Bungie doesn't allow it) * In Compare and Organizer, when you hold shift and click a column to change its sorting, we now remove the sort entirely on the third click. * Fixed a case where some old searches could not be unsaved. Remember that you can also *delete* searches from the Search History page or by clicking the X in the autocomplete dropdown. * Improve autocomplete for the new `dupe:` filter. * Loadout Optimizer and Compare will only show vendor items that you can actually buy. * Fixed Progress tab season pass counter double counting levels 101-110 ## 8.87.0 (2025-08-24) * Fix Xur showing exotic catalysts you have already acquired while "Only show uncollected items" is enabled. * Add `source:kepler` * `is:modded` can match items other than armor * `year:8` now correctly matches Edge of Fate items. * Fixes well rested XP counter from counting ranks 101-110 as 1 level instead of 5 * Fixed the display of Seasonal Progress and Well-Rested on mobile. * Include Vanguard Arms Rewards in the items considered in Loadout Optimizer * Sped up loadout analyzer when loadouts and inventory haven't changed. * Deprecated `is:dupelower` filter. * Deprecated `is:infusionfodder` filter. * Deprecated `is:wishlistdupe` filter. Use `is:dupe is:wishlist` to find dupes with a wishlist match. * Deprecated `is:crafteddupe` filter. Use `is:dupe is:patternunlocked` to find dupes you may replace with a crafted item. * A new `dupe:` filter has been added, which can find duplicates of combined factors. * Try `dupe:archetype+tertiarystat` to find duplicate armors with the same armor Archetype *and* tertiary stat. * The `dupe:` filter can look for lower stats *within* a group of similar items. * Try `dupe:setbonus+statlower` to look inside each armor set for pieces with worse stats. * Try `dupe:setbonus+stats` to look for identical rolls on identical armor. * Check out Filters Help at the bottom of the search dropdown, for more dupe filter keywords. * In the future, the `dupe:` filter will replace `is:dupeperks`, `is:statdupe`, `is:statlower`, `is:customstatlower`. * `dupe:customstatlower` will more accurately narrow matches, checking **all* applicable custom stats against items. `is:customstatlower` is not recommended. * Add a button to Compare to find all Armor 3.0 with the same 3 non-zero stats, even if they have a different Archetype. * The loading spinner will always spin when loading vendors. ## 8.86.1 (2025-08-19) * Fix Organizer shift-click filter behavior for perks. * Fixed confusing Tuned Stat symbol placement in the Organizer. ## 8.86.0 (2025-08-17) * Added Activatedness information to Set Bonuses on armor held by a character. * Builds in Loadout Optimizer are now sorted by enabled stats, then each enabled stat in order, then total stats (including disabled stats). Before, they were not sorted by total stats, so if you had some stats disabled you could get very low-stat builds near the top. * Added `is:statdupe` to find armor with the same base stats. * Fixed a bug where removing set bonuses by un-clicking them from the set bonus picker could result in a loadout that couldn't be saved. * Removed outdated power caps info from the Progress page * Tuning mod stat changes are reflected in the stat bars in the item popup. * Removed redundant stat effect text in mod tooltip. * Added holofoil column to organizer * Worked around a bug in light.gg that caused some links from DIM's Armory to log you out of light.gg. * Filtered down the list of reputation ranks shown on the Progress page, and included the Crucible reward rank. * Solstice challenges should appear on the Progress page now. * Moved season pass rank info to the Ranks section in Progress. * Fixed "Well Rested" perk detection, moved it to the Ranks section in Progress. ## 8.85.0 (2025-08-10) * Improved the responsiveness of draggable lists (Loadout Optimizer stat constraints, inventory sort order, etc.) * Fixed an issue where the loadout analysis on the Loadouts page treated Armor 2.0 and Armor 3.0 versions of exotics separately. * BETA ONLY: The new-item dot, `is:new` search, and new-item Organizer column have been removed. You can use the Item Feed to keep track of new drops. * Adjust text spacing for item popup headers when tier/season banner are present * Added an `is:holofoil` search. * Add the bonus to all stats for tier 10 masterwork on new weapons. * Item icons in the item feed will dim if the items do not match the current search. * Added a scrollbar to the item feed * Fix showing default ornament icon for new weapons in Armory * Added `tunedstat:` filter for finding Tier 5 items with specific tuners. * Highlighted Archetype and Tuned stats in the item popup. * The Balanced Tuning mod now correctly applies +1 only to the three lowest armor stats * Remove repeated stat info in perk descriptions * Fixed Assume Masterwork for tiered weapons in Compare * Show the armor masterwork socket for tiered armor * Correctly calculate the masterwork level for tiered armor * Add the ability to specify Set Bonuses in the Loadout Optimizer * Set Bonus names and perks are filterable via `perk:` `exactperk:` or free text search. * `is:statlower` now knows how to compare all the possible stat arrangements of armor with Tier 5 tuning mods, allowing you to find more strictly-worse items. * `is:statlower` now compares armor as if it has been masterworked, so an unmasterworked piece won't be considered worse than a masterworked piece if it would actually be better after masterworking. ## 8.84.0 (2025-08-03) * Fixed an issue where the Loadout Optimizer would consider Armor 3.0 and Armor 2.0 versions of exotics separately. Now, if you select an exotic, all your copies of that exotic will be considered. * Updated the order of search suggestions to prioritize `is:` filters. * Display armor archetypes in Loadout Optimizer. * Support mid-season season pass track change. * Stat range searches (e.g. `stat:rpm:<100`) will no longer match items that don't have that stat. * New Compare button for armor with Archetype-based stats. * Added a toggle to the Loadout Optimizer that will limit eligible armor to only new or featured gear (the gear that gets bonuses for being new). All this does is add and remove `is:featured` from the search bar. * Hid the masterwork upgrade socket in the item popup for new armor. * Added a catalyst icon for the Osteo Striga. * Removed some uninteresting materials from the material counts display. * Added stat ordinality armor filters like `primarystat:super` and `tertiarystat:grenade`. * Add another compare button to highlight Armor 3.0 with the same 3 base stats. * When you open Loadout Optimizer with the "Equipped" loadout, the loadout parameters you select will now be saved as the default for that class. This was already true if you entered Loadout Optimizer by clicking the "Loadout Optimizer" button. Editing an existing loadout does not save the parameters as a default. * When we save Loadout Optimizer defaults, we'll save the min/max setting for each stat now, not just whether it's enabled and what order. * Fix "enhanced" detection to highlight barrels, magazines, etc. ## 8.83.0 (2025-07-27) * DIMmit is back, for all your changelog notifications. * DIM now shows a placeholder and warning when your Silver balance is not available from Bungie.net. * Updated item tier display in the Item Popup. * In addition to weapon barrels/magazines/etc., item stat bars now display contributions from perks that provide them. * Big updates to Material Counts, with more mats and better grouping. Try hovering or clicking the Consumables count under the Vault banner. * `is:featured` now has a synonym `is:newgear` for looking up which items provide reward/combat bonuses, and can be taken into activities for New Gear only. * `is:armor3.0` set up for new Edge of Fate Archetype-based armor, and `is:armor2.0` now excludes 3.0 pieces. * Persistence stat and stat filter added for heavy crossbows. * Catalysts now show their source on the Records page. * Increased the contrast of red numbers against a black background. * Removed empty perks that could appear in the Armory on craftable items. * Added hover highlighting for the power formula and material counts buttons. * Possible Set Bonuses are now displayed in the Armor Popup. ## 8.82.2 (2025-07-22) * Fix an item inspection crash in D1. ## 8.82.1 (2025-07-21) * Bungie.net still shows characters as having one point of Artifact Power. This has been removed. * Fixed the armor stat total "equation" shown in the Item Popup. * Fixed Armor 3.0 Exotics displaying their Archetype instead of their Intrinsic power. * Added `stat:ammogen:` search. ## 8.82.0 (2025-07-20) ## 8.81.4 (2025-07-18) * Added Enhanced arrows to show Enhanced perks in the Item Feed. * Updated armor intrinsic detection to identify Armor 3.0 Archetypes. It should appear now in Organizer/Item Popup/Compare. * Improved cramped spacing of stats display in Loadouts. * Updated Echo of Persistence and other class-conditional fragments to lower the right stat. * Added archetypes in the Item Feed and adjusted multiple-perk display. * Added inventory sorting by item Tier. * Fix masterwork stat numbers in Loadouts/Optimizer. * Fixed a crash in Loadout Optimizer result sets rendering. * Stream Deck updates * Removed progress metrics (no more available) * Added new "append only" mode to append filters to search query from plugin (search action) * Now the state sent to the plugin includes also the counters of inventory items grouped by element id (vault action) * Restored subclass item pick * Added tier to selection/picker action ## 8.81.3 (2025-07-16) * Fixed a crash in Loadout Optimizer when using the "+Artifice" Assume Masterwork option. * Restore season pass info in Progress. * Manually renamed "Immovable Refit" to "Vexcalibur Catalyst". ## 8.81.2 (2025-07-16) * Added tier-level pips to item icons. * Fixed an issue in Loadout Optimizer that could exclude some valid sets when auto stat mods are off. * On the Loadouts page, hashtags are now combined if they only differ by case, and are sorted case-insensitively. * Fixed masterwork bonuses not showing up in orange in the stat bars for new armor. * Fixed some engrams not looking like engrams. * Fixed a number of places where class items did not display stats. ## 8.81.1 (2025-07-15) * Fixed new Armor 3.0 items not showing any stat bars. * Fixed Armor 3.0 masterworks applying points to all stats instead of the lowest 3 stats. * Add an explanatory tooltip to the Anti-Champion icon in the item tooltip, since folks have been confused by it. * Reposition the season badge on the item tile a bit, to better match in-game. * Add the stat icon to stat tooltips to help folks learn them. * Add the season icon to the item popup header. * Add the item tier as a number under the season icon. * Add an inventory sort property for "featured". Waiting on "tier" sorting until we show it on the tile somewhere. * Add Organizer/spreadsheet columns for "featured" and "tier". * Add `tier:` search to find items by tier. * Fixed the missing title for "Seasonal Hub" in the Progress page. * Fixed a crash when using the Stream Deck integration. * Fixed the display of the power icon in the character power calculation. * Show the Ammo Generation stat on weapons. * Prevent a crash in the Loadout Optimizer when auto stat mods is disabled. * Fixed cramped stat min/max fields in the Loadout Optimizer. * Changed the Loadout Optimizer min/max stat fields to not update other numbers until you hit Enter or blur the field. ## 8.81.0 (2025-07-15) * Improved the Loadout Optimizer algorithm to correctly maximize stats now that it's tierless. Also improved its performance. * Fix some interactions with the Loadout Optimizer stat constraint editor. * Add `is:featured` search for the new Featured Items. ## 8.80.0 (2025-07-13) * The Loadout Optimizer has been updated to target exact stats (up to 200) instead of tiers. There are lots more changes to Loadout Optimizer after Edge of Fate has released and we are able to adapt to the new way armor and stats works. * Fixed the D1 Reputation display not showing up on mobile. * Exotic armor ornaments no longer repeat the class name. * Fix an occasional crash in the Compare tool. * Removed `is:hasmod`, `modslot:legacy`, and `holdsmod:` searches as they don't have any use anymore. ## 8.79.0 (2025-07-06) * Added an `is:ininventory` filter that will highlight items that you have at least one copy of. This is meant to be used in the Records and Vendors tabs. * Fixed an issue where community insights on mods are so long that you could not select mods on mobile. ## 8.78.0 (2025-06-29) ## 8.77.0 (2025-06-22) ## 8.76.0 (2025-06-15) ## 8.75.0 (2025-06-08) * Removed the "Refresh Wishlist" button and the suggested remote wishlists when using a local wishlist. y * D1 inventory now shows emotes, and you can add emotes to D1 loadouts. ## 8.73.0 (2025-05-25) ## 8.72.1 (2025-05-19) * Updated season number filter for some items. * `source:riteofthenine` filter added. ## 8.72.0 (2025-05-18) * Postmaster items now have a warning indicator if DIM is not allowed to pull them. * Added an "ammo" column, a "perks grid" column, and a "mods" column to the Organizer. * Slimmed down column labels for some Organizer columns.as ## 8.71.1 (2025-05-12) * Fix misaligned stat numbers in Compare box. ## 8.71.0 (2025-05-11) * Stats in Organizer are now colored the same as in Compare. * Fixed the positioning of the item popup when clicking on artifact mods. * Rite of the Nine shiny weapons now have icon stripes and detect in `is:shiny` filter. ## 8.70.0 (2025-05-04) * Warning banner for Destiny 1 accounts when DIM detects the D1 API is still broken. * Fix for an Armory crash in outdated versions of Firefox. ## 8.69.0 (2025-04-27) * The Armory page will now link to alternate versions of an item if they exist, including reissues, shiny versions, adept versions, etc. * Crafted and enhanced weapons will now show their masterwork stat icon in the same place regular weapons do. ## 8.68.0 (2025-04-20) * `is:shiny` now detects Heresy weapons with the bonus Runneth Over Origin Trait. * Fixed Triage tab generating a very generic search filter for In-Game Loadouts. * Gave In-Game Loadouts an Edit button in Triage tab. * Fixed Enhanced Perks on an Enhanced item showing no selected Perks in the Armory. * Style cleanup for Perks, Weapon Components, Weapon intrinsics/archetypes. If something seems 2 pixels different, you aren't crazy. ## 8.67.0 (2025-04-13) * May have finally fixed the issue where DIM sometimes fails to load live data. * In Compare, you can now sort items by perks, mods, intrinsic, archetype, etc. The mods and perks rows. * In Compare, the masterworked stat is highlighted with an orange dot. * Ghosts show all their mods in Compare. * Removed a duplicate tooltip that showed an item's notes in Compare. * Restored the colored border for completed D1 items. ## 8.66.0 (2025-04-06) ## 8.65.0 (2025-03-30) ## 8.64.0 (2025-03-23) ## 8.63.1 (2025-03-19) * Fix a bug in Chrome for Android 134 and newer where the space reserved for the virtual keyboard would not be reclaimed after the keyboard is dismissed. * When you update DIM, it will no longer reload other tabs if you have a sheet up or are on the Loadout Optimizer. DIM may fail to load some pages until you refresh, though. * Fixed an issue where vigorous scrolling in a sheet (like the mod picker) can dismiss the sheet and its parent, losing progress in things like the loadout editor. * Attempt to prevent an issue where DIM loads fresh data from Bungie but doesn't update the view of inventory. ## 8.63.0 (2025-03-16) * Fixed a bug where the Organizer wouldn't show all the items if your screen was very tall or zoomed out. * Added a sortable name label to the Compare view. ## 8.62.0 (2025-03-09) ## 8.61.0 (2025-03-02) * `is:harmonizable` fixed to ignore bugged weapons with a Deepsight option that shouldn't be there. * `is:accountmaxpower` added to show your highest Power Level gear, determining the level of new drops. * `is:origintrait` added to find weapons with Origin Traits. * Fixed styling missing sometimes on the tooltip for Artifact Power. ## 8.60.0 (2025-02-23) * Added `source:sundereddoctrine` for the new dungeon. * Updated the color scale used for stats in Compare - there is now greater separation between the worst (red) and the best (green, with blue for the best stat). ## 8.59.1 (2025-02-16) * Added Pathfinder reward info. ## 8.59.0 (2025-02-16) * Fix a bug that could prevent removing wishlists if the wishlist no longer exists. * Fix D1 showing a yellow border around completed items. ## 8.58.0 (2025-02-09) * Energy upgrade tooltips now show the material cost to upgrade. ## 8.57.3 (2025-02-05) * Also fixed armor not showing up in Organizer due to the Bungie data bug that had all armor as "unknown" class instead of Hunter/Titan/Warlock. ## 8.57.2 (2025-02-05) * Worked around a bug in the Bungie data that had all armor as "unknown" class instead of Hunter/Titan/Warlock. ## 8.57.1 (2025-02-03) * DIM Sync data is now loaded incrementally, instead of being completely refreshed every time. This should result in faster updates, but otherwise nothing should be different. If you notice things are out of sync, you can click the "Reload remote data from DIM sync" button in Settings, but please let us know if you needed to do that. ## 8.57.0 (2025-02-02) * DIM now coordinates in a limited way between different tabs/windows. Only one tab will load data at a time, and when it does, all other tabs will refresh immediately. Item moves are also reflected immediately across tabs. This should help prevent tabs from getting out of sync with each other. ## 8.56.0 (2025-01-26) * Removed the 15 second timer on being able to check for new data from Bungie.net. ## 8.55.0 (2025-01-19) * Restored a workaround for laggy dragging in Chrome on Windows when using some high-DPI and/or Logitech mice. * DIM now recognizes exotic weapons that grant intrinsic breaker abilities through a perk. * Logging out now properly "forgets" the page you were on, so when you log in again it doesn't try to go back to that page. * Loadout names and vendor names in the sidebar are no longer uppercased. * Double-clicking on items in the search results page will pull them to your active character. * The text in the search history page is selectable. ## 8.54.0 (2025-01-12) ## 8.53.0 (2025-01-05) * Added `is:enhancementready` search that finds weapons which have reached level thresholds to enhance perks * For crafted weapons, this looks at the level thresholds of the enhanced versions of the weapon's current perks * For enhanced weapons, this looks at whether the next tier of enhancement is selectable on the weapon * Updated `enhancedperk` search to allow `is:enhancedperk` to find any weapons with already-enhanced perk columns * Farming mode will no longer make room for Ghosts. ## 8.52.0 (2024-12-22) * Minimum browser version for DIM has been raised to Chrome 126+ (or equivalent Chromium-based browsers) and iOS 16.4+. DIM may not load at all on older browsers. Firefox and Desktop Safari are still only supported on their most recent two versions. Note that Firefox 115 ESR is not supported, but may work if you're stuck on Windows 7. ## 8.51.0 (2024-12-15) * Added a wishlist refresh button in Settings to help with wishlist development. Note that GitHub can take upwards of 10min to actually reflect your changes. Refreshing won't speed up GitHub. * Having a broken wish list in your settings will no longer prevent removing other wish lists. ## 8.50.1 (2024-12-12) * Notes now appear in the tooltips on item tiles. * Fixed vendor items showing wishlist thumbs up icons when they didn't match a wishlist roll. * Fixed a bug that could cause DIM to infinitely redirect to invalid pages. ## 8.50.0 (2024-12-08) * Stat bars in the Item Popup now show more detailed information about contributing factors. Stat bar tooltips are easier to hover/hold-tap, and list weapon parts and stat contributions. * Some backlogged translations have been added to the app. * Destiny 2's item/game definitions are now more spread out in browser storage, to prevent Firefox from hitting a storage limit. * The "Traits" column for weapons in the Organizer now separates perks into two columns. * You can no longer tag catalysts for sale from vendors. * Vendor items will no longer complain about missing sockets. ## 8.49.0 (2024-12-01) * Corrected a misleading error banner and improved handling when DIM needs a fresh Bungie.net login. * Fixed some issues with search saving and DIM Syncing. ## 8.48.0 (2024-11-24) * Fixed a crash happening with a Guardian Games stat tracker. ## 8.47.1 (2024-11-18) * Fix gigantic vault engrams in single-character mode. ## 8.47.0 (2024-11-17) * #Tag suggestions now use your most popular existing capitalization. * Fixed character engram inventory wrapping on mobile. * Fixed autocomplete not doing its job after a colon. * Fixed checkboxes not clickable in the item sorting editor. ## 8.46.0 (2024-11-10) ## 8.45.1 (2024-11-04) * Fixed an issue with Destiny 1 inventory not loading. * Fixed an issue with some hover text not appearing on PC. * Fixed an issue with some exotics not appearing in Collectibles. * Fixed an issue where invalid searches looked saved if they were close to a saved search. * Progress > Ranks * Now highlights maxed ranks. * Competitive Division no longer shows it can be reset. ## 8.45.0 (2024-11-03) * If you click "Edit Copy" on a loadout, then click "Optimize Armor", then save the loadout from Loadout Optimizer, it will now save the copy, not the original loadout. * Tonics in the Tonic Capsule now show what their rewards or what artifact perk they affect is. * Added Clan Reputation, Engram Ensiders, and Xûr Rank to the Progress page. * Slimmed down Bungie's Destiny database to fit within Firefox's storage limits. * Stream Deck selection now relies on buttons, not drag and drop. * Masks are now counted as helmets properly in Compare. ## 8.44.0 (2024-10-27) * Allow dragging Item Feed items to any slot, removing the need to scroll to a specific item slot to drag them to a character. They will automatically go to the correct slot on the given character (in the same way Postmaster items function). ## 8.43.0 (2024-10-20) ## 8.42.0 (2024-10-13) ## 8.41.1 (2024-10-09) * Show the new pathfinders on the Progress page. * Add Accessories (Tonic Capsule) to the inventory screen. ## 8.41.0 (2024-10-08) * Updated for Episode: Revenant. * Make room for always-on scrollbars in Organizer to avoid extra weird scrolling. * Fix vendor finishers offering the "Lock" action, and D1 vendor items offering "tag". ## 8.40.0 (2024-10-06) ## 8.39.0 (2024-09-29) * Add `breaker:intrinsic` search that highlights items that have an intrinsic breaker ability (i.e. not granted by the seasonal artifact). * Breaker type granted by the seasonal artifact now has a green box around it, reminiscent of the artifact mod that grants it. ## 8.38.0 (2024-09-22) * Renamed `is:class` to `is:subclass`. * Fixed D1 item category searches such as `is:primary` and `is:horn`. Now they're up to parity with the D2 categories. ## 8.37.0 (2024-09-15) ## 8.36.1 (2024-09-12) * Fixed stat bar display for mods that conditionally apply stats. * Fix breaker type showing up on D1 items, and some crashes in D1 inventories. * `is:dupeperks` no longer compares armor from different classes. * Updated the list of breaker-granting artifact mods. ## 8.36.0 (2024-09-08) * In the item picker, you can long-press or shift-click an item to see its item details. A regular click still pulls that item. * `breaker:` searches now match items that can have that breaker type granted by this season's artifact (whether or not the correct artifact mods are enabled). The effective breaker type from artifact mods also now shows up on item tiles and in the Armory. * Add Enhancement tier to weapon level bar. * Update `enhanced` search keyword to allow range of values (0/1/2/3). Old `is:enhanced` behavior is now `enhanced:3`. * Update `is:enhanceable` search keyword to exclude Tier 3 Enhanced items. * Added `is:dupeperks` search that shows items that either are a duplicate of another item's perks, or a subset of another item's perks (taking into account which column perks appear in). * Added a Loadouts CSV export, accessible from the Settings page. * Improved how conditional stats for perks are calculated, including fixups for exotic catalyst stats, Enhanced Bipod, and more. * The notes text area now has a maximum editable size of 1024 characters, up from 120. * Added a tooltip with information about the selected super on the Loadouts page. * Clicking on the subclass on the loadouts page will open its item popup, with the correct aspects/fragments previewed. ## 8.35.1 (2024-09-01) * Fix the "track record" button not appearing on hover. ## 8.35.0 (2024-09-01) * Fix Organizer sorting behavior for notes, tags, and wishlist notes so that empty values sort along with other values. * In Compare, you can now shift-click on stats to sort by multiple stats at once (e.g. sort by recovery, then by resilience). * Hover state only applies on supported devices. ## 8.34.1 (2024-08-27) * Exotic class item perks will now show up in Compare suggestions * Compare view's suggestion buttons will now use the leftmost item's perks instead of an arbitrary item if the initial compare item is removed ## 8.34.0 (2024-08-25) ## 8.33.1 (2024-08-20) * Fixed the symbol picker displaying in the wrong part of the screen. ## 8.33.0 (2024-08-18) * Fixed the `:solar:` icon not showing up in the symbol picker. * Shift-clicking on a cell full of perks in Organizer, but not on a specific perk within that cell, will now add all the perks to the search, instead of adding an invalid search term. ## 8.32.0 (2024-08-11) * Fixed character sorting on the Loadouts page. * Dropdown no longer flickers on Firefox. * Thumbs-up icon is no longer near-invisible on the Europa theme. * Replaced Crafted icon with Enhanced icon for enhanced weapons. * Added setting to separate armor on different lines by class. * Replaced red background with dotted circle for perks that no longer roll on weapons. ## 8.31.0 (2024-08-04) * Improved item move logic to do a better job of making room for transfers between characters. * Fixed single-character mode hiding cosmetic items (ghosts, etc) that are equipped on other characters. Now they are properly shown as being in the "vault". * The Planetary Piston Hammer item now shows the number of charges. ## 8.30.0 (2024-07-28) * Remove damage mods and empty memento sockets from item popups. * A bunch of changes to Vault Organizer: * Sorting now uses a local-sensitive comparator by default. * You can sort perk columns - they sort by the name of the first perk, and then the name of the second perk, and so on. This should help with exotic class items. * Broke out armor intrinsics, cosmetics (shaders/ornaments), and weapon origin traits into their own columns. * Now that everything has its own column, the plain "perks" column is now restricted to perks/mods that don't appear in other columns. * For the deepsight harmonizer column, replaced the checkmark with the deepsight harmonizer icon. * Fixed shift-clicking on breaker to fill in a breaker: search. * Widened the "Enabled Columns" menu to multiple columns so it no longer has to scroll. * Perks columns fit their contents instead of having a hardcoded width. * The item type selector no longer scrolls away horizontally. * Selecting "Weapons" now shows all weapons by default. Feel free compare sidearms to rocket launchers if that's your thing. ## 8.29.0 (2024-07-21) * Fixed a case where invalid loadouts would be repeatedly rejected by DIM Sync. Now they'll be rejected once and be removed. * Re-added Xûr vendor armor to Loadout Optimizer. * Fix light.gg and D2Foundry links to avoid certain circumstances where perks weren't selected (or, where D2Foundry crashed). * Armor intrinsics once again show up in Organizer's perks column. ## 8.28.0 (2024-07-14) * Fixed a case where recently saved tags or loadouts might not appear if DIM Sync is down, even though they were still saved. ## 8.27.0 (2024-07-07) * Fixed the order of pathfinder objectives. * Fixed `modslot:artifice` matching every exotic. * Fix loadout apply trying to socket the empty artifice plug. * Make the prismatic symbol pink. ## 8.26.0 (2024-06-30) * The progress page shows the Ritual Pathfinder and Pale Heart Pathfinder. * Improved how we detect which rewards and challenges are available for Milestones on the Progress page. * Prismatic subclasses now show the currently-equipped super overlayed on them. * Restored wish-list-ability to all weapons, and actually made exotic class items wishlistable. * Manually filled in the possible perks that exotic class items can roll with in the Armory page. * A new `is:wishlistable` search highlights items that can be added to a wish list. * Made the vendor-item icon on Loadout Optimizer items a bit brighter. * Fully removed the concept of sunset weapons. The `is:sunset` search no longer does anything. * Added `modslot:salvationsedge` search. * Exotic class items now show all of their intrinsic perks. * Show the intrinsic perk for Ergo Sum and crafted exotics in Item Feed. * Fixed a case where multiple custom stats would overflow in the Item Feed. * Removed the stats line from class items in the Item Feed. * Fixed missing yellow stat bars from masterworked weapons. * Fixed some bizarre behavior in the symbols picker. ## 8.25.0 (2024-06-23) * Greatly expanded the set of symbols available for use in loadout names/notes and item notes. * Fixed the calculation of stat effects from enhanced stats. * The compare sheet now highlights which of the quick-filter buttons is currently active. * Changed the way we rate-limit Bungie.net calls, which may result in snappier item moves and loadout application. * Added a "compare" button to item feed tiles, since it's such a common action when evaluating new gear. * In the item feed, perks in the same column now have a bar on the left to indicate they are together. * Engram bonuses in the Milestones section are now relative to your "drop power", not your "character power". * The name of the nightfall and crucible labs playlist is included in their Milestone titles. * Manually corrected the engram power level for several Milestones. * Worked around a bug in the Bungie data that showed duplicate perks in some weapons. * Any item with randomized perks can now be wishlisted, which includes random-perk armor. ## 8.24.0 (2024-06-16) * Fix issue where light level displayed in the Loadouts views was calculated using all weapons and armor in the loadout, instead of just the weapons and armor to be equipped. * Add `light:` filter to the Loadouts search. Only works with loadouts that equip an item for all weapon and armor slots. * DIM now calculates Account Power level, which uses all items from all characters, and determines their "current power level" for the purposes of new item drops. * This number can be found in the header for Vault * Details can be found by clicking any character's gear Power level, below their header/dropdown. * Cleaned up armor upgrade slots and inactive Artifice slots showing up in the Item Popup. * Loadout Optimizer no longer tries to pair Exotics and Class Item Exotics in the same loadout. * Fixed issue where Enhanced BRAVE Mountaintop did not appear to be masterworked. * Fixed issue where Enhanced weapons could have perks that showed as not rollable. * `is:crafted` matches only crafted weapons, not Enhanced weapons. * Max stack size of consumables is now shown in the Item Popup when viewing their details. * Add some new materials and remove some old, from material/consumable counts in the Vault header. ## 8.23.0 (2024-06-09) * `is:enhanceable` and `is:enhanced` filters for non-crafted weapons whose perks can be enhanced. * Cleaned up extra weapon upgrade slots showing up in the Item Popup. * Shiny BRAVE weapons now have corner stripes. * Preliminary support for Exotic Artifice armor in the Loadout Optimizer * DIM can now assign these mods and automatically suggest them to according to your preferences. * A `+ Artifice` option allows DIM to treat regular Exotic Armor as Artifice, letting us plan out the best stat tiers for you. * Sheets no longer adjust up when the horizontal scrollbar is visible * `is:light` and `is:dark` filters for finding light (arc, solar, and void) and dark (stasis and strand) damage weapons. ## 8.22.0 (2024-06-02) * Pressing "Escape" when an input in a sheet is focused now closes the sheet. ## 8.21.1 (2024-05-27) * Fixed an issue where DIM clients might not see search history when using only local settings storage. ## 8.21.0 (2024-05-26) * The search suggestions dropdown now shows more results, based on the size of your screen. * Added an overload to the `inloadout:` search which allows searching items based on how many loadouts they are in, for example `inloadout:>2`. * Loadout searches now save in your search history and can be saved. * Added `is:fashiononly` and `is:modsonly` search keywords to loadouts search. * Pages such as "About" and "Settings" now respect device safe areas when the device is in landscape mode. * You can now edit a copy of a Loadout directly, with no risk of overwriting the existing loadout. * Fixed some bounties showing as "Arc" that were not, in fact, Arc. * Fixed the too-narrow width of the sidebar on the Loadout Optimizer on app.destinyitemmanager.com. * Fixed distorted icons for owned mods in collections. * Due to a change in how Bungie.net works, DIM now loads vendor information one at a time, which may mean it takes longer to see accurate vendor items. * Minor clarifications to the privacy policy. ## 8.20.0 (2024-05-19) ## 8.19.2 (2024-05-15) ## 8.19.1 (2024-05-12) * Loadouts now have their own search language that you can use from the Loadouts page or character emblem dropdown. This works the same as item search, but the search keywords are different and loadout-specific. We'll add more search keywords over time. * When you change language, DIM immediately loads the item database for that language - you no longer need to reload. * DIM now loads new item database after content updates without requiring a reload, as long as there's an item in your inventory from that new content. * Fixed the icon for "Material Counts" in the vault dropdown on mobile. * Removed some useless search suggestions like `exactname:gauntlets`. * Fixed some crashes when Google Translate is enabled. Please don't use Google Translate on DIM though, use our language settings. * Added a grace period before DIM stops auto-refreshing because you're not playing. This should prevent DIM from giving up on auto refresh when you change characters. * Added some new protections against losing tags when Bungie.net is misbehaving, though it still may not be able to handle some weirdness. * Fixed the filter help not showing up in some circumstances. ## 8.19.0 (2024-05-05) * Minimum browser version for DIM has been raised to Chrome 109+ (or equivalent Chromium-based browsers) and iOS 16+. * Add a warning for Samsung Internet users to explain why dark mode is making DIM too dark. ## 8.18.1 (2024-04-30) * Fix vault tile display on mobile. ## 8.18.0 (2024-04-28) * Restore per-stat quality ratings to D1 armor popups. ## 8.17.0 (2024-04-21) * Fixed max stat constraints sometimes being not shown in Loadout parameters. * `is:shiny` filter to find limited-edition BRAVE weapons. ## 8.16.0 (2024-04-14) * Loadouts with only armor mods, or only fashion (shaders & ornaments), now display a symbol in the equip dropdown, and can be filtered from among other loadouts. * BRAVE bounties now display correct rewards instead of all possible rewards. ## 8.15.0 (2024-04-07) * The Item Popup now correctly shows stat contributions from universal ornaments. * Item slots on characters now look a bit more normal when no item at all is equipped. ## 8.14.0 (2024-03-31) * Long equipped character Titles no longer push the Power Level out of view. ## 8.13.0 (2024-03-24) ## 8.12.0 (2024-03-17) ## 8.11.1 (2024-03-13) * Updates for new game content, including Wild Style detection as a breech-loaded grenade launcher. ## 8.11.0 (2024-03-10) ## 8.10.0 (2024-03-03) ## 8.9.0 (2024-02-25) ## 8.8.0 (2024-02-18) ## 8.7.0 (2024-02-11) * Loadout Optimizer now considers exotics sold by Mara Sov for Wish Tokens and includes them in suggested builds. ## 8.6.0 (2024-02-04) ## 8.5.0 (2024-01-28) ## 8.4.0 (2024-01-21) * Added an `exactname` filter to match items by their exact name, such that `exactname:truth` won't find Truthteller. * Updated Game2Give 2023 banner. ## 8.3.0 (2024-01-14) * Loadout Optimizer once again allows you to set a maximum stat tier, by Shift-clicking on a stat number (this is the only way to do it, for now). Unlike the previous stat max setting, this does not exclude builds that meet all your other requirements but need to put points in a maxed stat, resulting in more optimal builds overall. * Show more information about quest lines, even if steps are classified. ## 8.2.0 (2024-01-07) * Support the newest version of the DIM StreamDeck plugin. ## 8.1.0 (2023-12-31) * In the mod picker, we now show just "Stackable" or "Unstackable" instead of the full requirements for the mod (like its exact stackability or what raid it belongs to). * Several improvements to how the mod picker and subclass editor look and work, in hopes of making it easier to use. * Individual wish lists can be toggled on and off and the Just Another Team wishlist is available as a suggested option. * Catalysts can be searched for on the Records page using search terms that would match their associated item. * Clicking a catalyst on the Records page shows the item popup for its associated collections item. * Loadout Optimizer's "Pin Items" and "Excluded Items" sections have been slightly redesigned, and now both have clear-all buttons. * You can choose to show weapon groups in the vault inline instead of row-by-row. * The Seasonal Artifact details page now shows how many points used and resets. ## 7.99.0 (2023-12-17) ## 7.98.0 (2023-12-10) * You can no longer select multiple copies of mods that are unstackable (they do not provide a benefit when there are multiple copies of them). ## 7.97.1 (2023-12-04) * Fixed infinite reload / "Operation is insecure" issue introduced in the last release. ## 7.97.0 (2023-12-03) * Fixed error showing titles after Season of the Wish launched. * Updated data for new season. * Loadout optimizer stat constraints now have a clear button, randomize option, and sync from equipped option. * Tooltips will reliably disappear when you move your mouse off their triggering element, even if you move the mouse onto the tooltip. * Fixed the `is:smg` search. ## 7.96.0 (2023-11-26) * Added `is:vendor` search that is useful for excluding vendor items from Loadout Optimizer (enter `-is:vendor` in the search box). * You are now prevented from selecting multiple copies of fragments or aspects in the subclass selector. * Slightly improved Loadout Optimizer's algorithm for finding optimal sets. * Single-character mode now shows the postmaster items and unclaimed engrams from other characters. * The records page has a menu on the mobile version for easily jumping to the right section. ## 7.95.0 (2023-11-19) * Added an `exactperk:` search that matches a perk name exactly. No more mixing up "Frenzy" with "Feeding Frenzy". * Fixed a bug where in-game loadouts would be marked as matching a DIM loadout incorrectly. ## 7.94.1 (2023-11-13) * Fixed an edge case where Loadout Optimizer's "Strict Upgrades Only" mode from clicking Optimize Armor on the Loadouts page could result in too few sets. ## 7.94.0 (2023-11-12) * There is now an option in settings to group items in your vault by Item Type, Rarity, Ammo Type, Tag, or Damage Type. * On the Loadouts page, DIM now runs the Loadout Optimizer in the background to find out which of your loadouts could have better stats by swapping armor. * Loadout optimizer has a new stat tier editor which allows you to set your minimum stat tiers more intuitively. It is no longer possible to explicitly set maximum stat tiers - make sure you've added your subclass and mod configuration, set stat order, and ignore stats you don't want DIM to optimize, and you'll get the best possible sets. * In the loadout editor, replacing a missing item works even if you have 10 items in that slot. * Links in notes can no longer result in spurious hashtags. * Moving items from a search can once again move consumables/materials. * In Loadout Optimizer and the Loadout Editor, you can now choose a subclass with a single click. * Revamped the Exotic Armor selector in Loadout Optimizer. * Added an inline explanation of the Assume Masterwork option in Loadout Optimizer. * Vendor items are always included in Loadout Optimizer - the setting has been removed. * There is now a per-loadout option to include the effects of "Font of ..." mods' stats as if they were active or not. This helps the Loadout Optimzier make the right choices. * In Loadout Optimizer and the Loadout Editor, the selected subclass' super is now folded into the subclass icon rather than being shown separately. ## 7.93.0 (2023-11-05) * The Loadouts page will now analyze your Loadouts in more depth, show filter pills for analysis findings, and note them in individual Loadouts too. * Examples are Loadouts where the mods don't fit on the armor, Loadouts that rely on seasonal mods, or Loadouts where armor needs to be upgraded to accommodate mods or reach target stats. * Loadout optimizer will now use an effective energy of 9 for items that it is not assuming masterworked stats for. Before, it used an effective energy of 7, but enhancement prisms are easier to come by these days. * Fixed a case where vendor items could show as owned when they were not. * Fixed the platform icon not showing for Destiny accounts that were only associated with a single platform. * Fixed accidentally showing the kill tracker perk column on the item popup. * Fixed subclass mod sockets size on the in-game loadout details popup. ### Beta Only * A preview of a new stat constraint editing widget for Loadout Optimizer! Let us know how you like it and how it helps (or hurts) your ability to make top tier builds. * The Loadouts page will note "Better Stats Available" if we've found that a Loadout could use different armor or stat mods to reach strictly higher stat tiers. ## 7.92.0 (2023-10-29) * Fixed a bug that could delete recent tags/notes when loading DIM on a different device than the one where you set the notes. * Added a hotkey (T) to switch between the overview and triage tabs on the item popup. * The item popup remembers which tab you were last on. * Incomplete seals are now shown greyed out on the Records page. * Gilded seals show their gilding count on the Records page. * The Loadout Optimizer is better at calculating the max possible tier given your chosen stat constraints. * Fix FotL pages sometimes being shown as all owned. * Fixed the D1 organizer page to display perks nicely. * Fixed the Organizer not showing the armor CSV download button. * Organizer will now show exotic catalysts and empty catalyst sockets if a catalyst exists. * Organizer will no longer duplicate the exotic perk between the archetype column and the traits column. * CSV export will no longer include all kill tracker options, as they're very unreliable. * CSV export will no longer include some junk like armor upgrade sockets ## 7.91.1 (2023-10-23) * Fixed search menu showing behind items in some sheets. ## 7.91.0 (2023-10-22) * Added `source:ghostsofthedeep` search term. * Fixed some cases where hitting "Esc" would not close a nested sheet. * In Loadout Optimizer, prevent opening an empty mod picker when clicking general mod slots when auto stat mods are enabled. * Removed outdated reference to Google Drive in our privacy policy - DIM has not used Google Drive for storage for many years. * Fixed the search field in the character menu not having a background on the default theme. * If your language is set to Japanese, Korean, or Traditional Chinese, you may notice that fonts display smaller since upgrading to Chrome/Edge 118. [This is a change in Chrome](https://discord.com/channels/316217202766512130/1052623849197404241/1164636684646887434), and it makes the fonts in DIM (and everywhere else!) look the same as they do for other languages. Consider zooming the page or adjusting the item tile size if the fonts are now too small for you. * Fixed a bug where classified items would show "undefined" as their power level. * Removed link to Destiny Tracker from the Armory page. ## 7.90.0 (2023-10-15) * Removed tooltip from the text portion of exotic perks / archetypes since the text already includes those details. ## 7.89.0 (2023-10-08) * "Hide completed triumphs" on the Records page now also hides sections and seals where all triumphs have been completed, not only the triumphs themselves. * The Vendors page should now more reliably indicate whether armor sold by Ada-1 is unlocked in collections. * Comparing a legendary armor piece now starts the Compare view with similar armor based on intrinsic perk and activity mod slot. * Removed links to DIM's inactive Mastodon account. ## 7.88.0 (2023-10-01) * Added a Universal Ornaments section to the Records page showing which legendary armor pieces you have unlocked as Transmog ornaments and which ones you could turn into ornaments. * "Only show uncollected items" now correctly identifies some shaders that it missed before. * Removed Stadia from the list of accounts shown in the accounts list, and moved the Cross-Save primary platform to the front of the list. * Fixed a bug that prevented DIM from loading when offline. * Fix tooltips getting stuck open if you scroll in Compare while they're shown. * The mod selection menu now shows you how many of each type of mod you've chosen, and what the limit is. * Removed links to Twitter, and the Twitter embedded timelines, because Twitter no longer allows un-logged-in users from viewing feeds. BungieHelp info now comes from the unofficial mirror to Mastodon. * Fixed D1 Farming Mode to no longer try to move your equipped items, and to no longer move emblems at all. * Fixed the color of the title bar when DIM is installed to the dock from Safari in macOS Sonoma. * Cleaned up the list of materials shown on Rahool's vendor section. * Removed the loadout optimizer progress popup and replaced it with an inline progress indicator. * Added some debugging information in case DIM fails very early in its startup. ## 7.87.0 (2023-09-24) * Fixed the "Any Class" Loadout toggle not removing class-specific items. * The tabs (Overview/Triage) in the item popup no longer scroll with their contents. * Hide the duplicate activity socket on some ghosts. * Loadouts now show which problems they have (e.g. deprecated mods) on the loadout itself, not just in the filter pills. * Improved the drag-to-dismiss behavior of sheets on mobile. There's more work to do there though. * The pattern progress bar in the item popup now shows a harmonizer icon when a deepsight harmonizer could be used on that item to unlock pattern progress. ## 7.86.0 (2023-09-17) * Restored `is:dupelower` to prioritize power when choosing lower dupes. ## 7.85.0 (2023-09-10) * Adding a subclass to a Loadout or selecting a subclass in Loadout Optimizer will now copy all currently equipped Aspects and Fragments too. * The sort order for loadout names has been changed to better respect different languages, and to understand numbers in names. It should now match the way you see files sorted in File Explorer / Finder. ## 7.84.1 (2023-09-06) * Fixed the character menu scrolling the page to the top. * The "Sort triumphs by completion" toggle on the Records page now maintains the order of triumphs with identical completion progress. ## 7.84.0 (2023-09-03) * The order of vendor items should now much more accurately match the in-game order. * Filters and toggles on the Vendors page now consider focusing/decoding subvendors. E.g. the "Only show uncollected items" toggle will now show focusing subvendors if they allow focusing items you don't have collected. * Loadout editor menus now have a "Sync from equipped" option that replaces the loadout's items with your equipped items. The "Fill in using equipped" option is also disabled when there's no spaces to fill. * When loadouts are sorted by edit time, they are now grouped under headers showing which season they were last edited in. * The character menu no longer displays behind sheets. * "Fill in using non-equipped" will no longer attempt to add items for the wrong character class. * The Loadout Optimizer now uses the same editors as the Loadout Editor for subclass and mods, and has all the same options. ## 7.83.1 (2023-08-29) * Fixed another case where you might not get bounced to the login page when you need to re-login. * Fixed the search autocomplete menu showing behind search results on mobile. * Unsightly long text in the mod picker now wraps instead of escaping its box. * Fixed a few minor visual issues in sheets. ## 7.83.0 (2023-08-27) * You can choose between a number of different themes for DIM's interface in settings. * The Vendors page now has a toggle to hide all items sold for Silver. * Clicking on sub-vendors on the vendors page now opens them in a sheet, instead of taking you to a new page. * Vendor reputation is now displayed with all the same info as the ranks on the Progress page. * The "strip sockets" tool now has an option to remove only discounted-cost mods. * Fixed a bug where Loadout Optimizer would sometimes interpret a search query too literally and require that all items match it, even if a slot doesn't have any items that match the query. * Removed the ability to favorite finishers - this functionality has been removed from the game. * Fixed the color of search bars in sheets. * Fixed the ordering of popups, sheets, and tooltips so they won't display behind things anymore. * Fixed loadouts including last season's artifact unlocks. * Remove transmat effects from Rahool's currencies list. * Fixed display of character headers on mobile. * While In-Game Loadouts are disabled, their section of the Loadouts page will not appear. ## 7.82.1 (2023-08-22) ## 7.82.0 (2023-08-20) * Again fixed unsaving previously saved, now invalid search queries. * The "Titles" section on the Records page now shows the corresponding title instead of the seal name (e.g. Dredgen and Rivensbane instead of "Gambit" and "Raids"). * Added vendor engrams to the materials tooltip/sheet accessible through or near the vault emblem. * The "Search History" table can now be sorted by its columns (times used, last used) just like the Organizer. * DIM now detects the Clarity browser extension and recommends uninstalling it. Clarity is no longer developed by its authors and it causes excessive system resource usage. * Emblem backgrounds in the character headers should be a bit more crisp and won't scale as weirdly. ## 7.81.0 (2023-08-13) ## 7.80.0 (2023-08-06) * Fixed "Fill in using equipped" in a Loadout's subclass section failing to copy Aspects and Fragments. ## 7.79.0 (2023-07-30) * You can now search for Emotes and Ghost Projections on the Records page. * Added button to sort triumphs by completion. * Greatly expanded the "Randomize Loadout" feature. You can now randomize a Loadout's subclass and its configuration, weapons, armor, cosmetics, and armor mods. * Randomize them individually through the three dots in a Loadout section. * Randomize the entire Loadout using the "Randomize" button at the bottom of the Loadout drawer. * The existing "Randomize Loadout" button to immediately generate and apply a random Loadout now allows you to choose which parts of your current loadout to randomize * If you have an active search query, weapons and armor will be restricted to those matching the query. * DIM should automatically log you out if you need to log back in manually at Bungie.net, rather than just not working. * The "Optimize Armor" button on loadouts changes to "Pick Armor" when you don't have a complete armor set. * In Armory and Collections, un-rollable perks are sorted to the bottom, and we no longer show enhanced options for uncraftable perks. * Tier 1 Powerful Exotic engrams are now counted as Powerful rewards on the Progress page. * The Quests section on the Progress page now has filtering pills that match the quest categories in game (e.g. Exotics, Lightfall, The Past). * Artifact unlocks on loadouts no longer show a "1" in the corner. ## 7.78.0 (2023-07-23) * The "Clear other items" setting in Loadouts has been split into a separate option for clearing weapons and clearing armor. ### Beta Only * We've now got some experimental new themes for DIM - you can choose one in settings. These aren't final designs but they show off what can be changed. ## 7.77.3 (2023-07-18) ## 7.77.2 (2023-07-17) * Slow down updates to the Bungie Day Giving Festival Banner. ## 7.77.1 (2023-07-17) * Moved the Bungie Day Giving Festival Banner. ## 7.77.0 (2023-07-16) * Fixed Harmonic mods ex. "Harmonic Siphon" from having no description * DIM now considers breech-loaded (special) Grenade Launchers and Heavy Grenade Launchers completely separate item types. This means Special Grenade Launchers now have their own Organizer tab, Triage for Heavy Grenade Launchers will no longer show "similar" Special Grenade Launchers, and Compare will not include them when comparing Heavy Grenade Launchers. * When opening the Compare view from the Triage tab of a Vendor item, this Vendor item will now be included in the compared items. * New versions of the Last Wish weapons now appear in collections. * Added Bungie Day Giving Festival Banner. ## 7.76.0 (2023-07-09) * Fixed Fashion Loadouts being unable to store an Ornament for the Titan exotic Loreley Splendor Helm. ## 7.75.0 (2023-07-02) * Organizer's "Loadouts" column now sorts items by the number of Loadouts using them. * Added `memento:none` filter to highlight weapons with an empty memento socket. * `deepsight:harmonizable` highlights weapons where Deepsight Resonance can be activated using a Deepsight Harmonizer. ## 7.74.0 (2023-06-25) * You may now include vendors' items in loadout optimizer, in case they have a better roll available than what you have. ## 7.73.0 (2023-06-18) * DIM should no longer show a popup to enable DIM Sync before you've logged in. * Fixed drag and drop on Android. * Fixed scroll bar behaving weirdly on the sidebar of certain pages. ## 7.72.0 (2023-06-11) * Fixed showing the item under your finger while you're dragging it on iOS/iPadOS. As a reminder, on touchscreen devices you need to press the item for a little bit to "pick it up". And as a reminder for everyone, any time you see an item in DIM, pretty much wherever it is, you can drag it around to move the item or add it to a Loadout you're editing. This works from the Inventory, Item Feed, Loadouts screen, etc. * The Loadouts page now has a filter pill to find Loadouts with empty Fragment sockets. * The popup shown on the refresh button when Bungie.net is down no longer has buttons you can't click, and no longer exceeds the width of the screen on mobile. * DIM will not update itself in the background if you're in the middle of editing a loadout or doing many other tasks. * Removed references to Reddit. * Improved performance of viewing lots of loadouts, especially on iOS. The Loadouts page should no longer hang on load on iOS. * The tab title includes the name of the current page you're on. * Fixed the keyboard automatically appearing on the iOS App Store version. * Removed support for old Loadout Optimizer share links. * Fixed showing catalyst perk descriptions. ## 7.71.0 (2023-06-04) * Added Community Insights for the impact of various stat tiers on ability cooldowns, etc. This takes into account your current subclass config and equipped exotic. For loadouts, it uses the subclass config and exotic that are saved in the loadout to display details. This information comes from the Clarity database, and like all Community Insights is sourced from lots of manual investigation. * Automatic stat mods in Loadout Optimizer have graduated from Beta! We now remember this setting, and we ignore any manually chosen stat mods when auto stat mods are on. Enabling auto stat mods allows Loadout Optimizer to automatically assign stat mods to potential loadouts in order to hit the stats you've requested, in the priority order that you've chosen. * Fixed an issue where DIM might not properly force you to re-login with Bungie.net, and would instead continually throw errors trying to talk to Bungie.net. * The loadout dropdown in the "Compare Loadouts" sheet from Loadout Optimizer can no longer be taller than the screen. * Added `is:iningameloadout` search to find items that are in an in-game loadout. * In-game loadouts now appear above the "Max Power" loadout in the loadouts menu. * Fixed an issue with farming mode where it would show a bunch of error notifications. * `is:inloadout:` searches now autocomplete hashtags in loadout names and descriptions. * Fixed plugging Harmonic Resonance mods when using a Strand subclass. * You can now drag and drop subclasses into the loadout editor, including from other loadouts. * When DIM is installed as a PWA on desktop, you can now choose to hide the title bar. * Removed loadout sharing buttons from Loadout Optimizer. You can share from the Loadouts screen. * Hid the Artifact Unlocks section from loadouts until Bungie.net starts returning artifact info again. * Improved highlighting and selection styles for item perks. * Improved layout for mods in the Compare drawer - they stay in a line now. * Vendors will now show your current count of Engrams and other resources needed for focusing in their currencies section. * Added `is:focusable` search to find items that can be focused at a vendor. * Fixed DIM not showing Leviathan's Breath catalyst progress in the item popup. * Cleaned up the design of Loadout Optimizer stats, mod picker, exotic picker, and subclass editor. ## 7.70.0 (2023-05-28) * Fixed an issue where equipping classified titles (e.g. Ghoul), or ornaments would crash DIM. * Fixed the sizing and spacing of abilities in the subclass picker. * Fixed the display of the "Fishing Tackle" item to show current values and not show an ugly placeholder icon. * Updated information used to detect which season an item is from, after changes in the Bungie.net data since Season of the Deep. * Improved the hover indication for the search field buttons. * Fixed tracking crafted date for loadouts - they were not saving crafted date for items as intended, and were thus losing crafted items when they got reshaped. ## 7.69.0 (2023-05-21) ## 7.68.0 (2023-05-14) * Item tiles for armor on the Vendors page will now show their stat total instead of power level. * Refreshing your profile data no longer blocks item moves. * DIM now correctly handles mods that have mutually exclusive rules - e.g. you can't have multiple finisher mods on your class item. ## 7.67.0 (2023-05-07) * Fixed an issue where sometimes the stat bonuses shown on perks was wrong. ## 7.66.0 (2023-04-30) * Changed the wording when you need to visit a postmaster to pull an item. * Added an `is:adept` search filter. This allows you to find weapons which can equip Adept mods. * `:` and `-` are now allowed in hashtags. ## 7.65.1 (2023-04-28) * Enhanced adept weapons from Root of Nightmares should now show with correct stats. ## 7.65.0 (2023-04-23) * Fixed Adept Draw Time marking the draw time stat as negatively affected (red) instead of positively affected (blue). * Loadout Optimizer and Loadouts now consistently allow you to choose not yet unlocked Fragments and Aspects. Previously this was only working for some characters. * Fixed an issue where the DIM Loadout apply notification was sometimes not showing that it performed changes to subclass abilities. ## 7.64.1 (2023-04-16) * In-Game Loadouts are now represented as save slots. Click them for details on which items and selections they contain. * Change icon/name/color, save as a DIM loadout, clear the save slot, and more, from the dropdown menu or the slot's details popup. * The overview In-Game Loadouts strip now shows whether each slot matches a DIM loadout, is equipped, or is equippable. * Loadouts have a Prepare Equip button, to move items to a character and ensure clicking the in-game equip button succeeds. * If you're in orbit or a social space, or offline, the Equip button can move items appropriately, then apply the in-game loadout. * Fixed issue where selected mods would not scroll on mobile in the mod sheet. * Items can now be dragged and dropped from within the loadout edit screen. * Loadout Optimizer and Loadouts now include Armor Charge-based "Font of ..." mods in stat calculations. For example, if you are using the Font of Focus armor mod, Loadout Optimizer will assume the +30 points to Discipline when holding Armor Charge are active and not waste stat points on exceeding T10 Discipline. ## 7.64.0 (2023-04-09) * Updated DIM's Smart Moves logic for how to choose which items to move when a bucket is full. * The bulk note tool has gotten a major upgrade and now allows adding and removing to existing notes. * Undo and redo in loadout editor and loadout optimizer have the keyboard shortcuts you'd expect. * Added some extra information to the randomize popup to explain how to use it with searches. * Fixed the "L" hotkey inadvertantly working in Compare and when items should not be lockable. * Added new hotkeys for opening Armory (A) and Infusion Fuel Finder (I) from the item popup. * Added a "Compare" button to the Organizer to allow focused comparison of selected items. * Added hotkeys to organizer - bulk tag, note, compare, or move selected items easily. * Added a new "N" hotkey for editing notes on an item. ## 7.63.3 (2023-04-06) * Fixed equipping in game loadouts * DIM now tries to keep your device from sleeping while an item is moving or a loadout is applying. ## 7.63.2 (2023-04-03) ## 7.63.1 (2023-04-03) ## 7.63.0 (2023-04-02) * The "Show Mod Placement" sheet will now show the required armor energy capacity upgrades to make all mods fit (and the total upgrade costs). * Loadout Optimizer sets will now show energy capacity bars below armor pieces, similar to the "Show Mod Placement" sheet. * Loadouts created before Lightfall with deprecated stat mods now have their stat mods restored. * Mods in the loadout mods picker are now more logically ordered by matching the in-game order. * Autocompletion in the search bar now succeeds for terms with umlauts even if you didn't enter any (suggests "jötunn" when typing "jot..."). * Fixed the `modslot:any`/`modslot:none` filters. * Fixed an issue where some subclass fragments and armor mods would be missing descriptions in Loadout Optimizer and the Loadout editor. * The minor boosts to all stats that enhanced crafted and masterworked adept weapons have are now ignored in Organizer's "Masterwork Stat" column and by the `masterwork:statname` filter. Only the primary +10 boost is considered. * Sharing build settings directly from Loadout Optimizer will now also include subclass configuration. * DIM now saves Artifact configuration in Loadouts. Note that DIM cannot reconfigure your artifact automatically, but you can use this information to keep track of which artifact unlocks are important for a Loadout. * Search autocomplete should be smarter, with the ability to complete item and perk names even when you type multiple words. * In-game loadouts' icon, color, and name can now be changed through DIM. * You can create an in-game loadout from your currently equipped items through DIM. * Added quick clear buttons to each section of the loadout editor. ## 7.62.0 (2023-03-26) * Items in the "Focused Decoding" and "Legacy Gear" screens within the Vendors page will now correctly show collection and inventory checkmarks. * The current power caps and power floor are now displayed on the Milestones section of the Progress screen. * Display the current and max Postmaster count at all times. * Armor mods descriptions now include their stacking behavior. * Added tooltips for the loadout optimizer settings shown on saved loadouts. * The "mod assignment" screen displays better on mobile. * The icons for weapon slots have been changed to reflect how they work in game. What was the "kinetic" slot now shows kinetic, stasis, and strand icons while the "energy" slot shows solar, arc, and void icons. * On mobile, you can no longer accidentally scroll the whole page while viewing search results. * Clicking "Manage Loadouts" from the character menu will bring you to the Loadouts screen for that character instead of your active character. ## 7.61.0 (2023-03-19) * Hashtags for items and loadouts can now contain emoji. * Removed # from loadout filter pills. * Overloaded range filters (e.g. season:>outlaw) now autocomplete. * Stat effects for mods/aspects in the mod picker are now both more accurate and more attractive. * Fixed the color of Strand movement and class abilities on Loadouts screen. * Fixed an issue where DIM Sync data might not be available when Bungie.net is down. * Added `source:rootofnightmares`/`source:ron` and `modslot:rootofnightmares` searches. * DIM now correctly allows you to unsave previously saved queries that later became invalid. * Fixed the `is:curated` filter never matching weapons without an equipped kill tracker. ## 7.60.1 (2023-03-14) * Custom stat fixes * Fixed the `stat`/`basestat` filters for weapon stats. * Fixed custom stat columns unchecking themselves in Organizer. * Loadout Optimizer improvements: * The tooltip for stat mods now explains when a mod was picked automatically. * Mins/Maxes displayed in the stat tier picker now better match the stat rangees found in results. ## 7.60.0 (2023-03-12) * Fixed deepsight border showing up for weapons whose pattern has already been unlocked. * DIM now correctly handles reduced mod costs via artifact unlocks. * Support added for named and multiple custom total stats. Sort and judge your your armor pieces by multiple situations, like a PVE and PVP stat. Sort by these values in Compare and Organizer, and search by them with stat filters like `stat:pve:>40`. * Fixed powerful and pinnacle reward calculations. ## 7.59.0 (2023-03-05) ## 7.58.1 (2023-03-02) * DIM supports displaying and equipping in-game loadouts. * Triage tab is now available outside of DIM Beta. This feature provides information to help quickly compare and judge a new (or old) item. * Whether am armor piece is high or low among your others, or is completely better or worse than another. * How many other similar weapons you have, and weapon Wishlist status. * Whether an item is included in loadouts, and which. * Bright Dust and XP have been added to the filter pills on bounties and seasonal challenges. * `is:statlower` knows about the new artifice armor rules and will consider the artifice +3 stat boost in a single stat when comparing against other armor. * Sorting in the Organizer is a bit more reliable. * DIM should be more resistant to being logged out during API maintenance. * Loadout Optimizer will now automatically use Artifice mod slots to improve build stats, and the arrows point the right way. * The tooltip for enhanced intrinsics or adept masterworks will now only show the stat boosts actually relevant to the item. * The materials popup has been updated for Lightfall. * Deepsight weapons once again appear with a red border. The deepsight search terms have been collapsed into just `is:deepsight` as there is no longer deepsight progress on items. * Removed useless energy indicators on armor. ### Beta Only * Loadout Optimizer's toggle to include required stat mods has been changed to optimize all builds using as many stat mods as possible. This is a consequence of the artifice changes. ## 7.58.0 (2023-02-26) * The `inloadout` filter now finds hashtags in Loadout notes. * Support for non-English hashtags. * Added a popup on crafted weapons that shows all their kill tracker stats at once. * Switched D2Gunsmith link to D2Foundry. ## 7.57.0 (2023-02-19) * Add `is:retiredperk` search that highlights items that have a perk which can no longer drop for that item. * You can now click a Loadout name in Organizer's Loadouts column to quickly bring up this loadout for editing. * When hovering over subclass Aspects in Loadouts and Loadout Optimizer, the tooltip will now show the number of Fragment slots granted. * You can now bring up the Armory page for a weapon directly from the search bar by typing a weapon name there and clicking the corresponding entry. * Improved the logic for choosing what item to equip when de-equipping an item. DIM will now generally avoid equipping exotics as replacements, and will pay attention to the type of item and your tags. ## 7.56.0 (2023-02-12) * Fixed the Compare tool for items with quotation marks in their name. ## 7.55.0 (2023-02-05) ## 7.54.0 (2023-01-29) ## 7.53.0 (2023-01-22) * On the Records and Armory pages, perks only shown on the collections/"curated" roll will now correctly be marked as unavailable on randomly rolled versions. * Added a `crafteddupe` search filter. This allows you to find duplicate weapons where at least one of the duplicates is crafted. * Added shaped date to the organizer, and a shaped overlay to more easily pick out shaped weapons. * DIM will remember where you were linked to when you log in - you no longer have to log in then open that loadout link again. * Bounties and seasonal challenges now show their base XP value (before any bonuses). This is community sourced data which may not remain accurate with subsequent game updates. ## 7.52.0 (2023-01-15) * Loadout hashtags are now auto-completed in the Loadout name and notes fields. Type `#` to suggest tags used in other Loadouts. * Destiny symbols are now available in Loadout names and notes, and item notes. Type `:` for symbol suggestions or use the symbols picker in the text fields. * The "Sync item lock state with tag" setting now excludes crafted weapons, as DIM would otherwise re-lock crafted weapons during reshaping. * In accordance with all standard armor mods being unlocked in-game, DIM now also considers these mods unlocked. ## 7.51.0 (2023-01-08) * If you add hashtags to your loadouts' names or notes, DIM will show buttons for quickly filtering down to loadouts that include that hashtag. * Fixed a bug where the "Show Older Items" button in the Item Feed would not permanently show all old items. * The Armor and Weapons CSV export in Organizer and Settings now includes a Loadouts column. * Fixed universal ornament unlock detection. * Opening the Armory view from a Vendor focusing item now shows the correct weapon with all available perks, not a dummy item. ## 7.50.3 (2023-01-04) ## 7.50.2 (2023-01-04) ## 7.50.1 (2023-01-03) * Removed the "2x" tag on Crucible rank. ## 7.50.0 (2023-01-01) * DIM now loads a saved copy of your inventory even when it is offline or Bungie.net is down. The saved copy is whatever information Bungie.net last successfully provided on that device. * The refresh button now has a tooltip showing how recently DIM was able to load your inventory from Bungie.net. This can help identify when DIM's view is out of date, relative to the in-game state. * If DIM Sync is down, the Export Backup button will save a copy of your local data instead of just failing. * DIM can now automatically sync an item's log state to its tag - favorite, keep, and archive tags auto lock the item, and junk or infuse tags unlock the item. This option needs to be enabled in settings, and when it's on the item tile will no longer show the lock icon for tagged items. * Crafted items will no longer lose their tags/notes or be missing from loadouts after being reshaped. This only affects items that are newly tagged or added to loadouts - crafted weapons that were already tagged or in loadouts will not be preserved when reshaping them. * Worked around an issue where class item mods from the fourth artifact column would be missing for some players. ### Beta Only * If you add hashtags to your loadouts' names or notes, DIM will show buttons for quickly filtering down to loadouts that include that hashtag. ## 7.49.0 (2022-12-25) * The filter help menu item is now keyboard accessible. * Fixed a bug where opening a loadout link could result in the loadout reopening later. * DIM should be better at ignoring when Bungie.net sends back outdated inventory data. ## 7.48.0 (2022-12-18) * Using the "Import Loadout" button on the Loadouts page, you can now paste loadout share links (like `dim.gg` links or links generated by other community sites) to open these loadouts directly in DIM. * This should make it easier to open shared loadouts where you're using DIM instead of opening those loadouts in a new browser tab every time. * Added a "Clear Feed" button to the Item Feed. ## 7.47.0 (2022-12-11) ## 7.46.1 (2022-12-07) * Fix an error preventing Collections from being displayed. ## 7.46.0 (2022-12-04) ## 7.45.0 (2022-11-27) ## 7.44.1 (2022-11-22) * A Rising Tide community event: Updates for new declassified items, and support for new dynamic values in the titles of Items and Vendor Categories. ## 7.44.0 (2022-11-20) * When using the Compare tool with weapons, enabling the "Assume Masterworked" toggle will show weapon stats as if their masterwork was upgraded to T10. ## 7.43.0 (2022-11-13) * Gilding Triumphs for Seals are now denoted with a background, darker colors, and label text. * Loadout Optimizer now has Undo/Redo buttons covering all configuration options. * When Loadout Optimizer can't find any builds, it will now recommend configuration changes that could allow it to find builds. ## 7.42.3 (2022-11-10) * Telesto has been reprimanded. ## 7.42.2 (2022-11-09) ## 7.42.1 (2022-11-09) * Fixed an issue where DIM Sync data (loadouts, tags, etc) could appear missing for 10 minutes after loading DIM. ## 7.42.0 (2022-11-06) * Applying a Loadout with subclass configuration should now avoid pointless reordering of Aspects and Fragments in their slots. * When selecting a subclass in Loadout Optimizer, it will now start configured with your currently equipped super and abilities (but not aspects or fragments). * Fixed Compare drawer closing when clicking the button to compare all of a certain weapon type. * The Materials menu now includes Transmog currencies (Synthweave Bolts/Straps/Plates). ## 7.41.0 (2022-10-30) * On first visit, DIM will prompt you to select a platform instead of automatically selecting the most recently played one. Also, DIM will no longer fall back to your D1 account when Bungie.net is down. * Invalid search queries are now detected more reliably and DIM will not show search results if the query is invalid. * Loadout Optimizer will now remember stat priorities and enabled stats per Guardian class. ## 7.40.0 (2022-10-23) * Catalyst progress shows up in the item popup for exotic weapons that still need their catalyst finished. * Firefox users should notice fewer cases where their data is out of sync with the game. * DIM will warn you if you have DIM Sync off and try to save Loadouts or Tags that could be lost without DIM Sync. ## 7.39.1 (2022-10-18) * You can now undo and redo changes to loadouts while editing them. * Fix for an error displaying new vendor inventories when definitions are still old. * Fix the Progress page's event section to properly detect the new Festival of the Lost event card. * Removed a now-unnecessary workaround for incorrect subclass ability colors. ## 7.39.0 (2022-10-16) * Added `is:armorintrinsic` to find Artifice Armor, armor with seasonal perks, etc. * Compare suggestion buttons now offer comparison to similar armor intrinsics. * Added perks to Light.gg links. See your weapon's popularity rating without having to reselect its perks. * Vendor items now show pattern unlock progress. * Removed the "streak" boxes from Trials rank. * Added browser info on the About page ## 7.38.0 (2022-10-09) ### Beta Only * Added an experimental Loadout Optimizer setting that automatically adds +10 and +5 stat mods to hit specified stat minimums. ## 7.37.0 (2022-10-02) * Add `foundry` search term. Try `foundry:hakke` for all your items brought to you by Hakke. ## 7.36.0 (2022-09-25) ## 7.35.0 (2022-09-18) * Fixed an issue where emblems that were not transferrable across characters were being shown in the loadout drawer. * DIM now identifies more intrinsic breakers, added `breaker:any` ## 7.34.0 (2022-09-11) * Season of Plunder Star Chart upgrades are now shown in the right order on the Vendors page. ## 7.33.0 (2022-09-04) * Progress page now correctly classifies the Star Chart weekly challenge as a powerful reward source instead of a pinnacle. * Visual adjustments to power level tooltips. * Loadout Optimizer is now aware of King's Fall mods. * Deprecated mods no longer appear in the Seasonal Artifact preview. * Made an experimental change to how we sequence Bungie.net API calls that may make their performance more consistent. ## 7.32.0 (2022-08-28) * If the DIM API is down and you have pending updates, DIM will load correctly instead of spinning forever. We also do a better job of keeping changes you make while the API is down. * If the DIM API is not returning some info (e.g. searches), we'll fall back to your locally cached data instead of wiping it out. * Updating/overwriting a Loadout using Loadout Optimizer's "Compare Loadout" button will now correctly remove the placeholders for armor equipped in the Loadout that no longer exists. * The item sort for Weapon Damage Type and Armor Element Type are now separate. * Epic Games accounts should display properly in the menu. * The loadout name editor will no longer offer system autocomplete. * Fixed the subclass colors for arc subclass mods. ## 7.31.1 (2022-08-23) ## 7.31.0 (2022-08-21) * Fixed Loadouts trying to clear Solstice sockets and Strip Sockets trying to remove Festival of the Lost helmet ornaments. * Tooltips have been adjusted further. They now have more spacing around content, rounded corners and improved contrast. ## 7.30.0 (2022-08-14) * Tooltips have been redesigned: * They now use a darker color scheme that fits in better with the rest of DIM. * Perk and mod tooltips for enhanced weapon traits and Exotic catalysts have unique styles to help them stand out. * The energy cost of armor mods is displayed within tooltips. * Fixed an issue where the energy meter on Ghosts was not displaying the amount of energy that had been used by inserted mods. * Solar class ability and jump icons have had their colors adjusted to match other solar abilities (we couldn't handle it anymore). ## 7.29.1 (2022-08-07) * Fix a bug where you couldn't edit a search query from the middle. ## 7.29.0 (2022-08-07) * Fixed Armory perk grid showing arbitrary wish list thumbs, and fixed Collections offering wish list notes for unrelated weapons. * Collections items will now be recognized as craftable. Try the search filter `is:craftable -is:patternunlocked` on the Records page to list craftable weapons you still need to unlock the pattern for, and click the weapons to see your pattern progress. * When prioritizing where to place other Arc armor mods, DIM Loadout Mod assignment will now try to activate the secondary perks of all types of Arc Charged With Light mods. * Fixed the "Remove other mods" toggle in Loadouts resetting when saving the Loadout as "Any Class". * Fixed missing element icons in the Triage pane. * Added a "Strip Sockets" search action to remove shaders, ornaments, weapon, armor, and artifact mods. This is available from the advanced actions dropdown to the right of the search field. Search for targeted items first, then choose what to remove. * Eliminated an unnecessary 10 second pause when loading DIM if the DIM Sync service is down. * Fixed search filter string disappearing when rotating or majorly resizing the DIM window. * Integration for the [DIM Stream Deck extension](https://dim-stream-deck.netlify.app/) is now available outside DIM Beta. * Fixed an issue with saving/syncing the Farming Mode slot count setting. * Fixed a crash and improved the accuracy of the Loadout Optimizer's mod assignment behavior. ### Beta Only * Added warnings about potential data loss when you save tags, notes, and loadouts but have DIM Sync off. * Added an info bar when DIM Sync is not able to talk to the server. ## 7.28.0 (2022-07-31) * Hid Solstice armor rerolling sockets from Loadout Optimizer too. ## 7.27.0 (2022-07-24) ## 7.26.1 (2022-07-23) * Added Solstice event challenges to the Progress page. ## 7.26.0 (2022-07-17) * Worked around a Bungie.net API bug where Vanguard reset count was reported under Strange Favor (Dares of Eternity) instead. * DIM now has direct support for the [DIM Stream Deck extension](https://dim-stream-deck.netlify.app/). If you have a Stream Deck you can install this plugin and then enable the connection from DIM's settings to control DIM from your Stream Deck. Please note that the plugin is neither written by nor supported by the DIM team. ## 7.25.0 (2022-07-10) ## 7.24.0 (2022-07-03) * Weapon perks now include community-sourced weapon and armor perk descriptions courtesy of [Clarity](https://d2clarity.page.link/websiteDIM) and [Pip1n's Destiny Data Compendium](https://docs.google.com/spreadsheets/d/1WaxvbLx7UoSZaBqdFr1u32F2uWVLo-CJunJB4nlGUE4/htmlview?pru=AAABe9E7ngw*TxEsfbPsk5ukmr0FbZfK8w#). These can be disabled in settings. * DIM will now auto refresh while you're playing the game. You'll see a green dot when DIM notices you're online - if you're online and it doesn't notice, try refreshing manually by clicking the refresh icon or hitting the R key. * If you have a title equipped on your character, it will replace your character's race in the character headers. * Fixed a crash when trying to assign deprecated Combat Style mods. * The "Move other items away" loadout toggle no longer clears ghosts, ships, or sparrows. * Added filter for enhanced perks. ### Beta Only * We have enabled experimental direct support for the [DIM Stream Deck extension](https://dim-stream-deck.netlify.app/). If you have a Stream Deck you can install this plugin and then enable the connection from DIM's settings to control DIM from your Stream Deck. Please note that the plugin is neither written by nor supported by the DIM team. **If you had installed the old Stream Deck Chrome extension, you need to uninstall it, or DIM will act weird (popups closing, etc).** ## 7.23.2 (2022-06-29) * Fixed an issue where fashion mods would not display in loadouts. * Fixed the element icon displaying below the energy number in Compare. * Somewhat worked around an issue with Bungie.net where on refresh you would see an older version of your inventory. * Fixed the crafted weapon level progress bar going missing with some Operating System languages. * Perk and mod tooltips should contain fewer duplicate lines of text. * Exotic catalyst requirements are now hidden on tooltips if the catalyst is complete. * Fixed an issue where stat modifications from Exotic catalysts were being displayed when the catalyst was incomplete. ### Beta Only * Community-sourced perk descriptions have been made more visually distinct. ## 7.23.1 (2022-06-27) * Fix missing icons in the subclass and mod menus. ## 7.23.0 (2022-06-26) * The links on the top of the page will now show for narrower screens. All links are always available in the menu. * Improved performance of switching characters and opening item picker or search results on iOS. Something had gotten slower with Safari in one of the recent iOS updates, so we had to do a lot of work to get back to a responsive UI. * Fixed the tooltip in the mod assignment page not showing the correct energy usage. ## 7.22.0 (2022-06-19) * Fixed a rare edge case where Loadout Optimizer would miss certain valid elemental mod assignments with locked armor energy types. * When moving multiple items, DIM will transfer them in a more consistent order e.g. Kinetic weapons are moved before Heavy weapons, helmets before chest armor etc. * Fixed Organizer redundantly showing enhanced weapon intrinsics in multiple columns. * Vendor items once again show wish list thumbsup icons. * Weapon attunement and leveling progress now shows a single digit of additional precision. ## 7.21.0 (2022-06-12) * The [DIM User Guide](https://guide.dim.gg) has moved back to GitHub from Fandom, so you can read about DIM without intrusive ads. * When making automatic moves, DIM will always avoid filling in your last open Consumables slot. An item can still be manually moved into your character's pockets as the 50th consumable. * Loadout Optimizer will now suggest class items with an elemental affinity matching the mods even when allowing changes to elemental affinity. * Fixed an issue where the item popup could appear partly offscreen. * Items sorted by tag will re-sort themselves immediately after their tag changes. * DIM now loads full inventory information on load and doesn't require an inventory refresh for certain info including crafting status. ### Beta Only * Weapon perks now include community-sourced weapon and armor perk descriptions courtesy of [Clarity](https://d2clarity.page.link/websiteDIM) and [Pip1n's Destiny Data Compendium](https://docs.google.com/spreadsheets/d/1WaxvbLx7UoSZaBqdFr1u32F2uWVLo-CJunJB4nlGUE4/htmlview?pru=AAABe9E7ngw*TxEsfbPsk5ukmr0FbZfK8w#). These can be disabled in settings. ## 7.20.1 (2022-06-06) * Fixed some items showing the wrong popup. ## 7.20.0 (2022-06-05) * The top level comment of a saved search filter is now displayed separately from the filter query. * Support for new loot: `source:duality` and `source:haunted`. * Little clearer warning when you have hidden a major section of your inventory. * Moved the currencies (glimmer, legendary shards, etc) from the "Armor" tab to the "Inventory" tab on mobile, and also included them in the material counts sheet (accessible from Vault header dropdown). ## 7.19.0 (2022-05-29) * Enhanced intrinsics on crafted weapons are now treated as a masterwork internally. As a result, you can use e.g. `is:crafted -masterwork:any` to find crafted weapons without an enhanced intrinsic. The golden border additionally requires two enhanced traits, just like in-game. * Resonant Element search filters such as `deepsight:ruinous` have been removed as these currencies are now deprecated. * Selected Super ability is now displayed on Solar subclass icons. * Features around managing crafting patterns: * Items that have a pattern to unlock will show the progress to that pattern in the item popup - even on items that do not have deepsight resonance. * Items that can be attuned to make progress in unlocking a pattern have a little triangle on the bottom right of their tile to set them apart. * Search filter `deepsight:pattern` finds those items. * The search `is:patternunlocked` finds items where the pattern for that item has already been unlocked (whether or not that item is crafted). * Don't forget that `is:craftable` highlights any items that can be crafted. * Fixed Triage tab's similar items search for slug Shotguns. ## 7.18.1 (2022-05-24) * Added seasonal info for Season of the Haunted and fixed some bugs with new items. * Loadouts with a Solar subclass will automatically be upgraded to Solar 3.0. * Show Airborne Effectiveness stat on weapons. ## 7.18.0 (2022-05-22) * In Loadout Optimizer, the option to lock Masterworked armor to its current element has been replaced with an option to lock the element on armor equipped in other DIM Loadouts. * The Witch Queen had reduced the cost of changing the element on a fully masterworked armor piece to 10,000-20,000 Glimmer and one Upgrade Module, making it cheaper than changing the element on a not fully masterworked armor piece. * Selecting this option means Loadout Optimizer will suggest changes to armor elements as needed but avoid breaking other Loadouts where mod assignments rely on particular elements. * Clicking the "Optimize Armor" button in a Loadout to open Loadout Optimizer excludes this Loadout from consideration because you're actually looking to make changes to this Loadout. * Loadouts list opened from Vault emblem now won't erroneously warn that Loadouts with subclasses or emblems are missing items. ## 7.17.0 (2022-05-15) * Fixed Organizer not showing some legendary armor intrinsic perks. * Fixed a glitch in Loadout Optimizer where legendary armor intrinsic perks could be clicked to lock that piece as an exotic. * Fixed double zeroes on armor in Compare. * Fixed bad stat coloring in Compare when stats are more than 100 points apart (this only really affected power level). * Popups and tooltips are a bit snappier. * The close button in the Armory view (click an item's title) no longer overlaps the scrollbar. * Inventory size stat no longer shows on any item - it used to show on Bows only. ## 7.16.1 (2022-05-09) * Fix "lower is better" stats not being masterworked gold in the item popup. ## 7.16.0 (2022-05-08) * Stat bonuses granted to crafted weapons by an enhanced intrinsic are now distinguished in the stat bars similarly to masterwork effects. * Make sure DIM displays the scoring thresholds on the Shoot To Score quest. * The recoil direction stat has been tweaked to show a much wider spread as the recoil stat value decreases. ## 7.15.0 (2022-05-01) ## 7.14.1 (2022-04-26) * Reverted Deepsight workaround, so weapon attunement displays correctly. ### Beta Only * Enabled the Triage tab of the item popup. Find some information here to help decide if an item is worth keeping. Let us know what helps and what could help more! ## 7.14.0 (2022-04-24) * Work around an issue where Bungie.net is not highlighting completed Deepsight weapons. ## 7.13.0 (2022-04-17) * If an armor piece doesn't have enough mod slots to fit the requested mods (e.g. three resist mods but no artifice chest piece), DIM will notice this earlier and show them as unassigned in the Show Mod Placement menu. * Added text labels to "icon-only" columns (lock icon, power icon, etc.) in dropdowns on the Organizer page. Only show label in dropdowns, columns show icon only. * Echo of Persistence Void Fragment now indicates that it has a stat penalty depending on the Guardian class. * We no longer auto-refresh inventory if you "overfill" a bucket, as refreshing too quickly was returning out-of-date info from Bungie.net and making items appear to "revert" to an earlier location. Make sure to refresh manually if DIM is getting out of sync with the game state. * Using the Mod Picker to edit loadout mods should now correctly show all picked mods. * Selecting a different weapon masterwork tier for previewing should now correctly preview the final value of the changed stat in the masterwork picker. * Fixed a case where the "Gift of the Lighthouse" item might be in your inventory but not show up in DIM. Allowed some items with missing names to appear in your inventory. ## 7.12.0 (2022-04-10) * If a wish list contains only non-enhanced perks, DIM will mark a roll as matching if it has the Enhanced versions of those perks. * Fixed a rare edge case where Loadout Optimizer would not consider legendary armor if you own an exotic with strictly better stats. * Glaive symbol now shows up in bounties, challenges, etc. * `is:extraperk` filter finds weapons with additional toggleable perks, from pinnacle activities and Umbral Focusing. * Fixed perk grouping for some perk-only wish lists. * Armory wish list view now shows perks, magazines, barrels, etc. in a similar order to the in-game view. * Re-added the D2Gunsmith link to the weapons armory page. * `memento:any`, `memento:nightfall` etc. filters find crafted weapons with a memento inserted. ## 7.11.0 (2022-04-03) * The Item Popup's header now opens the Armory view when clicked, and has some cursor/link styling as a reminder. * Deprecated Black Armory Radiance slots are now hidden, to make space for other weapon data. * Material Counts tooltip now fits onscreen better on desktop. On mobile, it's available under the banner dropdown of the Vault inventory page. * Wishlist combinations now collapse themselves into manageable groups in the Armory view. * Enhanced Elemental Capacitor no longer adds all its stat bonuses to weapons on which it's selected. * Fynch rank is now showing the correct number on the Vendors page. * Fixed loadouts with Void 3.0 subclasses accidentally including empty fragment or aspect sockets. * Fixed loadouts failing to remove mods from some armor or inadvertently changing the Aeon sect mod. * Invalid search terms no longer cause the entire search to match every item. * Searches do better with quoted strings, and allow for escaping quotes in strings (e.g. `"My \"Cool\" Loadout"`) * Item moves are better about allowing a move if you really have space on a character, even if DIM hasn't refreshed its view of inventory. That said, DIM will always work best when its view of your inventory is up to date, so continue to refresh data after deleting items in game. DIM will now refresh automatically if we "overfill" a bucket because clearly we're out of date in that circumstance. * Mod Picker will now properly register Shadowkeep Nightmare Mods as activity mods. * Selected Super ability is now displayed on Void and Stasis subclass icons. * Mod position selector avoids invalid sockets a little better. ## 7.10.0 (2022-03-27) * Dragging horizontally on items in Compare will scroll the list - even on iOS. * Mobile users can now access Material Counts under the banner dropdown of the Vault inventory page. * In the Armory and Collection views, craftable weapons now show their required Weapon Level in their tooltip. * DIM should no longer get visually mangled by Android's auto-dark-mode. * Fixed an incorrect item count in non-English inventory searches. * Try a little harder to re-fetch item definitions data, if Bungie.net sends back an invalid response. * Searches that can't be saved (because they're too long, or invalid) won't show a save ⭐️ button. * Search filters can contain comments. Only the top level comment gets saved. e.g. `/* My Cool Search */ is:handcannon perkname:firefly`. * Loadouts * The loadout search field has been moved to the top of the loadout menu, which should prevent iOS from going crazy. Filtering loadouts hides the other buttons as well. * Sharing a loadout now shows an explanation of what's being shared. * Fixed the loadout drawer not opening when "+ Create Loadout" is selected from the vault. * Fixed "Fill from Equipped" going a little overboard on what it tried to add to the loadout, and spamming notifications. ## 7.9.0 (2022-03-20) * When loading your inventory, DIM now alerts you if your items might be misplaced, affecting your drops' Power Level. * New inventory sorting options. Check [Settings](/settings) to view and rearrange your sort strategy. * Reverse the order of any individual sorting method. * Sort items by whether they are crafted, and whether they have Deepsight Attunement available. * Fix organizer stats header alignment * Added Vow of the Disciple raid mods to Loadout Optimizer and search filters. * Deepsight weapons' attunement progress is now shown on the item popup. Tap and hold, or hover the progress bar to see extractable Resonant Elements. * Fixed some weird spacing in the item popup perk list when a gun could but doesn't have an origin perk. * The Progress page properly distinguishes between +1 and +2 pinnacles. ## 7.8.3 (2022-03-15) * Fixed loadout search filter to include notes ## 7.8.2 (2022-03-14) ## 7.8.1 (2022-03-14) ## 7.8.1 (2022-03-14) * Fixed D1 loadout editor not appearing. * Fixed loadout editor not disappearing after saving/deleting. ## 7.8.1 (2022-03-13) * Assume armor masterwork and lock armor energy options will now be saved correctly when saving a loadout from the Loadout Optimizer and loaded correctly when Optimizing Armor. * Obsolete consumable mods hidden in the Vault are now detected. They should show up on the Inventory page, and DIM should count vault space more accurately. * Prevent iOS from popping up the keyboard automatically so often. * Prevent crafting socket from showing up in the Armory. * Clearer, prettier Enhanced Perk icons. * Raid crafting materials are now included in the currency counter. Tap and hold, or hover, the consumables count in the vault header to check them. * Many fixes for how classified items show up, and how they count toward the power level of each Guardian class. Can't see these fixes now, but maybe next time there's a new Raid. * New search support for `source:vow` (Vow of the Disciple) and `source:grasp` (Grasp of Avarice) and `season:16`. ## 7.8.0 (2022-03-06) ### Changes * The "Pull From Postmaster" button no longer requires a second tap to confirm. For those who dislike this button, it may be removed entirely via a setting in the Settings page. * Removed D2Gunsmith link from the item details popup while they work on revamping the site for all the new changes. * Removed the `level:` filter for D2 accounts, as Guardians no longer have a Level and items no longer require one. * Season of the Risen War Table Upgrades are now in the right order and show their acquired status. * Loadout Optimizer Mod picker will now correctly update when switching between mod slots without closing Mod Picker. * Loadout Optimizer now correctly takes Echo of Persistence's class-specific stat reductions into account when generating sets. * The "Kinetic Slot" icon in Compare sheet now looks different from the "Kinetic Damage" icon. * Added `catalyst:` filter which accepts the following parameters `missing`, `complete`, and `incomplete`. ### Features * `is:wishlistunknown` highlights items that have no rolls in the currently loaded wishlist. * When you have 10 or more loadouts, a search box will appear in the Inventory page loadout dropdown, allowing you to search names just like on the Loadouts page. * The Item Feed is available on both desktop and mobile. It shows your gear in the order it dropped, and gives you quick controls to tag incoming loot. Click on the item tile to get the full item popup. * Item Feed also got better at identifying relevant weapon perks. * Tagging an item from the Item Feed also marks it as not-new. * Items can be dragged out of the feed into inventory locations (or into the loadout editor). * We have brand new Loadout Editor! Check it out from the character menu or the Loadouts page. * The layout mirrors the Loadout page's new design which has clear areas for different types of items. Each section also has a menu of additional actions like re-syncing from your currently equipped items, or clearing out a whole section. * As part of this change, we're removing support for "multi-class" loadouts. Loadouts will either be tied to one class, or can be toggled to "Any Class". "Any Class" loadouts cannot contain Subclass, Armor, or Fashion. If you edit an existing "Any Class" loadout and save it, those items will be removed unless you turn off "Any Class". * Double-click items to toggle between equipped and unequipped instead of single clicking. We'll be continuing to improve how you choose items and specify whether they're equipped in the future. * A new setting allows you to clear out all other mods from your armor when applying a loadout. This works even if you've chosen no mods in your loadout, so you can make a "Reset mods" loadout. * With this new design we have space to add even more loadout editing tools over the next few seasons. * The loadout editor stays open if you navigate to the Inventory or Loadouts screen while it's already open. * The new Loadout Editor is not available for D1. ### Witch Queen updates * Crafted and Deepsight weapons are now more in line with how they look in-game. * Old loadouts containing void subclasses will upgrade automatically to the new Void 3.0 version, instead of telling you the loadout is missing an item. * Enhanced perks are now visually distinct in the Item Popup. * The Organizer page now includes a selector for Glaives. * Glaives now show their Shield Duration stat. * New search filters: * `deepsight:complete` and `deepsight:incomplete` to check the status of weapons' Deepsight attunement. * `deepsight:ruinous`, `deepsight:adroit`, `deepsight:mutable` and `deepsight:energetic` to identify Deepsight Resonance weapons that can provide specific Resonant Elements. * `is:craftable` for any weapons which could be crafted at the Relic. * `weaponlevel:` to filter by a crafted weapon's level. * `is:glaive` ... finds Glaives! ## 7.7.0 (2022-02-28) * Increased the strings we search through when filtering by mods/perks. * Crafted weapons' levels and level progress are now shown on the item popup. * Added `is:crafted` and `is:deepsight` filters. * Crafting materials are now included in the currency counter. Tap and hold, or hover, the consumables count in the vault header to check them. * Fixed a bug where "Use Equipped" would not update fashion in existing loadout. ## 7.6.0 (2022-02-21) * Fix applying D1 loadouts. * `inloadout:` filter now matches partial loadout names -- use `inloadout:"pvp"` for items in loadouts where "pvp" is in the loadout's name. * If your loadout includes ornaments, items are shown as if they had the loadout applied in the loadout page and loadout editor. * You can now change the Aeon sect mod through the item popup. * You can now edit your equipped Emotes from DIM. You can't add them to loadouts... yet. * Fix issue where Loadout Optimizer armor upgrade settings were not being migrated from existing loadouts. * Clan Banners are no longer shown in DIM. * Weapon compare sheet now includes a button to compare with other legendary weapons of the same category, excluding exotics. * Armor in collections now displays its collections stat roll. * Fix issues with button text wrapping in some languages. * Fix potential element blurriness in Edge browser. * Fix for Loadout Optimizer suggesting armor with insufficient energy. * Fix a clash between `power:1234` and `is:power` filters. * Loadout Optimizer is now a little more thorough in preventing an item from being both pinned and excluded. ### Witch Queen updates * There's a good chance crafted items will display correctly in DIM. No promises though. * Prepare Records page for a new section featuring craftable items. ### Beta Only * Loadout Editor * Fix issue where subclasses were counted as general items when dropping into a loadout or filling general from equipped. * Allow removal of a single mod through the editor display. ## 7.5.1 (2022-02-14) ### Beta Only * We're testing a brand new Loadout Editor. Check it out from the character menu or the Loadouts page. * The layout mirrors the Loadout page's new design which has clear areas for different types of items. Each section also has a menu of additional actions like re-syncing from your currently equipped items, or clearing out a whole section. * As part of this change, we're removing support for "multi-class" loadouts. Loadouts will either be tied to one class, or can be toggled to "Any Class". "Any Class" loadouts cannot contain Subclass, Armor, or Fashion. If you edit an existing "Any Class" loadout and save it, those items will be removed unless you turn off "Any Class". * Double-click items to toggle between equipped and unequipped instead of single clicking. We'll be continuing to improve how you choose items and specify whether they're equipped in the future. * A new setting allows you to clear out all other mods from your armor when applying a loadout. This works even if you've chosen no mods in your loadout, so you can make a "Reset mods" loadout. * With this new design we have space to add even more loadout editing tools over the next few seasons. * The loadout editor stays open if you navigate to the Inventory or Loadouts screen while it's already open. * The new Loadout Editor is not available for D1. ## 7.5.0 (2022-02-13) * Collect Postmaster now requires an additional click to confirm. * Transferring ships via search query should now reliably transfer all selected items. * Filters Help now groups stat comparison operators for a more compact page. * Milestones are grouped by how much power bonus their rewards can provide. * On the Loadouts page, you can now drag existing items on the page, into the current Loadout Editor, just like you can on the Inventory page. Use it to grab a couple of your favorite pieces from another loadout! * Loadout armor stat tiers now include the total tier. * Changed the Loadout Optimizer's Armor Upgrade options for Assume Masterwork and Lock Element options. All armor will now have an assumed minimum energy capacity of 7. The new settings have the following options, * Assumed Masterwork * None - Armor will use their current stats. * Legendary - Only legendary armor will have assumed masterwork stats and energy capacity * All - Legendary and exotic armor will have masterwork stats and energy capacity * Lock Element * None - No armor will have its element locked * Masterworked - Only armor that is already masterworked will have their element locked * All - All armor will have element locked ## 7.4.0 (2022-02-06) * Masterwork picker now only shows higher tiers of the current masterwork and full masterworks compatible with the weapon type. * Sharing a build from the Loadouts page or Loadout Optimizer now uses our dim.gg links which are easier to share and show a preview. * If you prefer reduced motion (in your operating system preferences), sheets like the compare and loadout dialogs now appear and disappear instantly. * Clearer feedback when uploading a wishlist file. * Expanded Organizer categories to account for Fusions and LFRs in unusual weapon slots. * Visual fixes for Organizer categories and Vendor page toggles. ## 7.3.0 (2022-01-30) * Organizer drill-down buttons now show a more accurate armor count. * Delete Loadout button now looks more warning-ish, and asks for confirmation without using a popup. * DIM will now try to recover from a state where the browser has a corrupted storage database. * DIM will now try to avoid overwriting shaders you don't own and thus couldn't apply back. * Removing subclass from loadout will now enable "Add Equipped" button. * "Add Equipped" button will no longer cause multiple items in the same slot to be listed as equipped. * Widened and reorganized the Loadouts menu. * Pull from Postmaster (and its lesser known cousin, Make room for Postmaster) are removed in favor of the button next to your Postmaster items. * Randomize loadout is now at the end of the list of loadouts. ## 7.2.0 (2022-01-23) * Weapons CSV download now includes a Zoom stat column. * Shaders, ornaments, and mods can now be searched in their choosers. * Trials passages now show the number of rounds won and the progress of completion is now tied to the number of wins. ## 7.1.0 (2022-01-16) * Applying a loadout *without* fashion will no longer remove shaders and ornaments from your armor. * The shader picker now filters invalid shaders more consistently and won't call shaders "mods". * Fixed Records page sometimes duplicating Triumphs or Seals section while missing Collections. * When provided multiple wish lists, Settings page now shows info about all loaded wish lists, not just the first one. * Compare Drawer should no longer refuse valid requests to add an item to comparison. ## v6 CHANGELOG * v6 CHANGELOG available [here](https://github.com/DestinyItemManager/DIM/blob/master/docs/OLD_CHANGELOG/OLD_CHANGELOG_6.X.X.md) ================================================ FILE: docs/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at github@destinyitemmanager.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: docs/COMMUNITY_CURATIONS.md ================================================ Moved to https://github.com/DestinyItemManager/DIM/wiki/Creating-Wish-Lists ================================================ FILE: docs/CONTRIBUTING.md ================================================ First, thank you for contributing to DIM! We're a community-driven project and we appreciate improvements, large and small. Here are some tips to make sure your Pull Request (PR) can be merged smoothly: 1. If you want to add a feature or make some change to DIM, consider [filing an issue](https://github.com/DestinyItemManager/DIM/issues/new) describing your idea first. This will give the DIM community a chance to discuss the idea, offer suggestions and pointers, and make sure what you're thinking of fits with the style and direction of DIM. If you want a more free-form chat, [join our Discord](https://discordapp.com/invite/UK2GWC7). 1. Resist the temptation to change more than one thing in your PR. Keeping PRs focused on a single change makes them much easier to review and accept. If you want to change multiple things, or clean up/refactor the code, make a new branch and submit those changes as a separate PR. 1. All of our code is written in [TypeScript](https://typescriptlang.org) and uses React to build UI components. 1. Be sure to run `pnpm fix` before submitting your PR - it'll catch most style problems and make things much easier to merge. 1. Don't forget to add a description of your change to the PR description prefixed with "Changelog: " so it'll be included in the release notes! 1. Use of AI coding tools is not prohibited, but if we wanted to use AI to solve some problem, we could easily do it ourselves. We do not need people to feed our backlog into Claude. ## Developer Quick start 1. [Install Pre-requisites](#pre-requisites) 1. [Clone](#clone-the-repo) 1. [Start Dev Server](#start-dev-server) 1. [Get your own API key](#get-your-own-api-key) 1. [Enter API credentials](#enter-api-credentials) ### Pre-requisites **Note:** It's often easier to use a package manager like [Homebrew](https://brew.sh/) for Mac, or [Chocolatey](https://docs.chocolatey.org/en-us/choco/setup) for Windows, and install the prerequisites through them. * Homebrew: `brew install git nodejs corepack visual-studio-code` * Chocolatey: `choco install git nodejs-lts corepack vscode` #### Manual Install * Install [Git](https://git-scm.com/downloads) * Install [NodeJS](https://nodejs.org/) * It is highly recommended to use [VSCode](https://code.visualstudio.com/) to work on DIM. When you open DIM in VSCode, accept the recommended plugins it suggests (find them manually by searching "@recommended" in the Extensions window). * On Windows, restart your system after installing everything. ### Enable Corepack Corepack manages the version of pnpm, the package manager used by DIM. It comes with NodeJS and will automatically get the right version of pnpm for you. * [`corepack enable`](https://github.com/nodejs/corepack#how-to-install) ### Clone the repo To locally **run a copy** of DIM, you can simply clone the code repository: ```sh git clone https://github.com/DestinyItemManager/DIM.git ``` To **contribute changes to the project**, you'll want to: 1. Make an account on GitHub 1. [Create an SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) and [add it to your account](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) 1. Fork DIM to make your own copy of the repository 1. Clone the forked repository to your local machine 1. Edit the local files 1. Commit and push your code changes to your fork 1. Create a Pull Request from your forked repository to the original upstream repository, allowing DIM maintainers to accept and merge your changes More detailed information on these steps is [here](https://docs.github.com/en/get-started/quickstart/contributing-to-projects). ### Start Dev Server Once you have cloned the repository or a fork of the repository to your local machine, in the root directory: * Run `pnpm install` * If `pnpm` isn't installed, run `corepack enable` and try again, or [install it manually](https://pnpm.io/installation). * If `corepack enable` fails, try running it with Administrator privileges. * If you're using PowerShell on Windows, you may need to run `Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted` to allow pnpm to run. * Linux-based developers will need to install `build-essential` (`sudo apt-get install -y build-essential`) prior to running `pnpm install`. * Run `pnpm start` On Windows machines, this will also install SnoreToast to provide notifications for parts of the development process, like when a build completes. ### Get your own API key: 1. Go to [Bungie's Developer Portal](https://www.bungie.net/en/Application) (you will have to be signed in) 1. Click `Create New App` 1. Enter any application name 1. Enter `https://github.com/YourGithubUsername/DIM` under website 1. For `Oauth Client type`, select `Confidential` 1. Set your redirect url to `https://localhost:8080/return.html` (or whatever the IP or hostname is of your dev server) 1. Select all scopes _except_ the Administrate Groups/Clans 1. Enter `https://localhost:8080` as the `Origin Header` 1. Check the box to agree to the Terms of Use and click Create New App ### Enter API Credentials This step will need to be done each time you clear your browser cache. You will be automatically redirected to a screen to enter these credentials if the app can't load them from local storage when it starts. 1. Open your browser and navigate to https://localhost:8080 (your browser will likely give a security warning, it is safe to continue anyways, see Overview below for details) 1. Copy your API-key, Oauth Client_id, and OAuth client_secret from bungie.net into DIM developer settings panel when it is loaded 1. Below the section for Bungie API credentials, follow the instructions to generate a DIM API key ### Development **Overview** The `pnpm start` step will create a hot-loading web server and a TLS cert/key pair. You will access your local development site by visiting https://localhost:8080. You will likely get a security warning about the certificate not being trusted. This is because it's a self-signed cert generated dynamically for your environment and is not signed by a recognized authority. Dismiss/advance past this warning to view your local DIM application. **Check code Style** * `pnpm fix` will tell you if you're following the DIM code style (and automatically fix what it can). Check out the [docs]() folder for more tips. **Translation** * We use [i18next](https://github.com/i18next/i18next) for all our translated strings, so if you want to translate something that's currently English-only, take a look at that. Usually it's as simple as replacing some text with `{t('KEY')}` and then defining KEY in the `config\i18n.json` file. * `pnpm i18n` will add, sort, and prune `src/locale/en.json`. You should never manually edit `src/locale/en.json`. Some keys are obfuscated by code and will need to be added as comments into the code such as `// t('LoadoutBuilder.ObfuscatedKey1')`. If you have any questions, ping @delphiactual via GitHub, Slack, or Discord. ### Android Debugging Assuming you have an Android phone: 1. Enable Developer Mode 2. Enable USB debugging 3. In Chrome, visit chrome://inspect/#devices and connect to the device. 4. Enable port forwarding for port 8080. 5. Visit https://localhost:8080/developer on your laptop and copy the "Open this link in another browser" link address. 6. Back in the inspect devices screen, paste that URL into the "Open tab with URL" box to open it on your Android device. 7. Save the API keys on the Android device and log in as normal. 8. Back in the inspect devices screen, click "inspect" under the tab you want to debug. ================================================ FILE: docs/Docker.md ================================================ # Docker Development Quick Start ### Pre-Requisites To get started with Docker, follow the steps below. If you already have Docker installed on your machine, you may skip to step 2. If you already have docker-compose installed on your machine, you may skip to step 3. 1. Install Docker ([https://www.docker.com/get-started](https://www.docker.com/get-started)) 2. Install docker-compose ([https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/)) 3. Getting an API key in [CONTRIBUTING.md](CONTRIBUTING.md#get-your-own-api-key) ### Managing DIM Docker Containers Run the following commands from the DIM cloned directory on your machine: * `docker-compose up` to start the docker containers and build the dist files * `docker-compose up -d` to start the containers and build the dist files in detached mode * `docker-compose stop` to stop detached mode * `docker-compose down` to stop the container and purge its containers and networks * `ctrl+c` to stop > Building dist files will take a while on the first startup while yarn installs dependencies and webpack builds DIM. ### Tips and Troubleshooting You can get an interactive terminal with `docker-compose exec webpack bash`. This is very useful for troubleshooting issues that arise inside of the container when it's spun up. ## Commit hook won't run If you `git commit` inside the container, you may see one of the following warnings: ```txt Can't find Husky, skipping pre-commit hook You can reinstall it using `npm install husky --save-dev` or delete this hook ``` ```txt Can't find Husky, skipping prepare-commit-msg hook You can reinstall it using `npm install husky --save-dev` or delete this hook ``` These can be fixed with the suggested commands. ## No editor on system When using `git commit`, you may encounter: - `error: cannot run editor: No such file or directory` - `error: unable to start editor 'editor'` This means that `git` cannot launch a text-editor for you to customize your commit-message with. A quick workaround is to use the `-m` flag, ex: ```sh git commit -m "This is my commit message" ``` For an interactive editor, try `nano` or `vim`: ```sh apt-get update apt-get install nano ``` ## Installing new packages If you need to install new node dependencies, you should run those commands from inside the container via: ```sh docker-compose exec webpack bash ``` Once inside, use `pnpm add NameOfTheDependency` as normal. ## Container terminates unexpectedly On Windows, you may see the error: ```sh dim-webpack | error Command failed with exit code 137. ``` This may indicate that the Docker VM which hosts containers has run out of memory, and a higher setting is needed. ================================================ FILE: docs/OLD_CHANGELOG/OLD_CHANGELOG_3.X.X.md ================================================ ## v4 CHANGELOG * v4 CHANGELOG available [here](/docs/OLD_CHANGELOG/OLD_CHANGELOG_4.X.X.md) ## 3.17.1 * Fixed a bug with the display of the amount selection controls in the move popup for stackable items. * Localization updates * Moved the "VCR" controls for stackable item amount selection to their own row. ## 3.17.0 * Fixed the perk selection in Loadout Builder. #1453 * Integrated Trials-centric weapon reviews (and the ability to rate your own gear (and make comments about your gear)). Done in conjunction with destinytracker.com. * Fixed the logic for artifact bonuses to compute the right number. #1477 * Restore some missing images from our build system changes. * Don't allow engrams to be tagged. #1478 * Add home screen icons (and Safari tab icons, and Windows tile icons) for the website. * Fixed "is:locked" filters to be consistent for engrams. #1489 * The Beta website is now updated automatically for every PR. * If you're not logged in to the website, we show the login screen. * Better error messages for when you have the wrong platform selected, plus the error doesn't cover the platform selector. * Improved website compatibility with Firefox, Safari, and Edge. * Many style fixes for Safari. * Drag and drop is now supported on touch devices. Press and hold an item to drag it. #1499 * Armsday packages can no longer be dragged. #1512 * Add tags and notes to items! This has been in Beta forever but now it's official. Hit ? to see the keyboard shortcuts, and use "tag:" searches to find your tagged gear. * Remove Materials Exchange from the beta. * Vendors now show where they are, and are sorted better. All the cryptarchs now appear. Engrams waiting to be decrypted aren't shown in the vendor screen. * Experimental iOS 9 Mobile Safari compatibility. May be removed in the future. * Style updates to clean up DIM's look and make sure more screen space is being used for items. * Gained the ability for us to fill in classified items, even if Bungie hasn't unclassified them. You still can't transfer them though. * The "Hide Unfiltered Items while Filtering" preference now applies to vendor gear too. #1528 * When moving stacks of items through the popup, there are now buttons to max out the amount, and add and remove up to even stacks of items. * Xur should disappear on Sundays again. ## 3.16.1 * Significantly increased the storage limit for tags and notes. It's still possible to go over (especially with long notes) but it should happen far less frequently - and it should notify you when it happens. ## 3.16.0 * Removed farming option to keep greens since they're disassembled by default now. * Added stat search, for example: "stat:rof:>= 22" * Fixed formatting for search loadouts when the search terms contain angle brackets. * A new "Make room for Postmaster items" auto layout will clear out enough space on your character to pick up all the stuff you've accumulated at the Postmaster. * Vendor items now explain what you need to do to get them. * Xur looks like the other vendors, and correctly displays both heavies now. * Compare tool styling updates. * Compare tool shows attack/defense. * In the compare tool, stats that are the same across all items are white instead of blue. * There's now a picture of each item in the compare tool. * Clicking the title of an item in the compare tool will scroll to that item and "pop" it so you know which one it is. * Armor and items that don't match the equipping character will once again transfer in loadouts. You can still put multiple subclasses of the same damage type in a loadout. * Empty space around talent grids has been eliminated. * Memory of Felwinter's stat bar no longer overflows its container. ## 3.15.0 * Permit the same damage type of subclass in loadouts (#1067) * Update record books to properly display time instead of a large number. (#1051) * Moving an item into a full vault but an empty bucket (such as full General but the vault contains no Consumables) now works. * Stacks of items are properly accounted for. They'll now combine as things are moved to make space - previously even a stack of 1 consumable would count as taking up the whole slot and would prevent a move of 2 more of that consumable. * We now catch errors trying to move aside items and retry with a different item. You should see fewer failed moves! * "Thrashing" in farming mode is fixed. When farming mode can't proceed (because moving anything off the character would result in something else being moved back on, because you're out of space), we now show a friendly info message. This message is throttled to show up no more than once a minute. * Fixed a bug where a full vault would prevent farming mode from moving things to other characters. * The move aside logic strongly prefers putting things on characters other than the original item's owner. This makes it much easier to move a bunch of stuff off of a character without other things bouncing right back in. * Prefer putting engrams in the vault and not taking them out when choosing items to move aside. * Farming mode now makes room to pick up artifacts, materials, and consumables. * When making space in the "General" category or in Materials/Consumables buckets, we'll choose to move aside an item that can be combined with another stack somewhere without increasing the total number of stacks. This trends towards consolidation and can help free up a full vault, as well as getting rid of stray stacks. * We swapped in "special ammo synth" and "primary ammo synth" instead of "motes of light" and "strange coins" for the farming mode quick gather buttons. They seemed more useful in the heat of battle. * When dequipping an item, we try harder to find a good item to equip in its place. We also prefer replacing exotics with other exotics, and correctly handle The Life Exotic perk. * Lots of new translations and localized strings. * Vendors update when you reach a new level in their associated faction, or when you change faction alignment. * Fixed a too-small perk selection box in the loadout builder, and properly handle when vendors are selling Memory of Felwinter. ## 3.14.1 (2016-12-06) * Internationalization updates. * Fix for Loadout Class Type bug. ## 3.14.0 * Compare Weapons and Armor side-by-side. * Added `is:sublime` filter * Added detailed information to the Trials of Osiris popup card. * Added more detection for item years. * The collapse button now no longer takes up the whole bucket height. * Fixed marking which characters had access to vendor items. * Fix tracking new items when the new-item shine is disabled. * Added option to Farming Mode to not move weapons and armor to make space for engrams. * About and Support pages are now translatable. * Improved error handling and error messages. * Vendors are collapsible. * All vendor items (including duplicates with different rolls) will now show up. * Added more translations. * If you have more than one Memory of Felwinter, they are all excluded from loadout builder. * Export correct quality rating for items in CSV. ## 3.13.0 (2016-10-31) * The vendors page is back. It'll show all available vendors. It's now a lot faster, and combines vendor inventory across your characters. Consumables and Bounties are now shown. Item stats and quality will hopefully show up on 11/8. * Loadout builder has option to load from equipped items. * Added option to farm green engrams or not. * When moving consumable stacks, you can now choose to fill up one stack's worth. * Don't sort bounties (the API does not currently provide the in-game order.) * Fix max-light rounding. * Fix a bug in the new filters for source. * Fix incognito mode launching * More i18n. * Classified items in the vault are now counted and shown. * DIM is faster! * Memory of Felwinter is now excluded from loadout builder by default. ## 3.11.1 (2016-10-04) * Fixed an issue with farming mode where users without motes, 3oC, coins, or heavy could not use farming mode. * Fixed an issue where classified items would not show up in the UI. ## 3.11.0 (2016-10-04) ##### New * Added Quick Move items to farming mode. * Farming mode now also moves glimmer items to vault. * Added `is:inloadout` filter * New filters: is:light, is:hasLight, is:weapon, is:armor, is:cosmetic, is:equipment, is:equippable, is:postmaster, is:inpostmaster, is:equipped, is:transferable, is:movable. * New filters for items based on where they come from: is:year3, is:fwc, is:do, is:nm, is:speaker, is:variks, is:shipwright, is:vanguard, is:osiris, is:xur, is:shaxx, is:cq, is:eris, is:vanilla, is:trials, is:ib, is:qw, is:cd, is:srl, is:vog, is:ce, is:ttk, is:kf, is:roi, is:wotm, is:poe, is:coe, is:af. * Added debug mode (ctrl+alt+shift+d) to view an item in the move-popup dialog. * Added max light value to max light button in dropdown. * Major loadout builder performance enhancements. * Support rare (blue) items in loadout builder. ##### Tweaks * Consumables and materials are now sorted by category. * All other items in the General Bucket are sorted by Rarity. * Move ornaments in between materials and emblems. * Link to wiki for stat quality in the move-popup box. * Full item details are shown in the move popup by default (they can still be turned off in settings). ##### Bugfixes * Prevent double click to move item if loadout dialog is open. * [#889](https://github.com/DestinyItemManager/DIM/issues/889) Fixed stats for Iron Banner and Trials of Osiris items. * Fix infusion finder preview item not changing as you choose different fuel items. Also filter out year 1 items. * Fix some green boots that would show up with a gold border. * A bunch of consumables that can't be moved by the API (Treasure Keys, Splicer Keys, Wormsinger Runes, etc) now show up as non-transferable in DIM. * Husk of the Pit will no longer be equipped by the Item Leveling loadout. * Fixed equipping loadouts onto the current character from Loadout Builder. * The default shader no longer counts as a duplicate item. * DIM no longer tries to equip exotic faction class items where your character isn't aligned with the right faction. * Fixed more cases where your loadouts wouldn't be applied because you already had an exotic equipped. * Elemental Icons moved to bottom left to not cover the expansion symbol. * Loadout builder no longer shows duplicate sets. * Fix equip loadout builder equip to current character. ## 3.10.6 (2016-09-23) * The DestinyTracker link in the item popup header now includes your perk rolls and selected perk. Share your roll easily! * Fixed moving consumables in loadouts. Before, you would frequently get errors applying a loadout that included consumables. We also have a friendlier, more informative error message when you don't have enough of a consumable to fulfill your loadout. * Fixed a bug where when moving stacks of items, the stack would disappear. * The progress bar around the reputation diamonds is now more accurate. * Enabled item quality. * Item Quality is enabled by default for new installs. * A new Record Books row in Progress has your Rise of Iron record book. * Searches now work for all characters and the vault again. * Can equip loadouts onto the current character from Loadout Builder. * Added ability to feature toggle items between Beta + Release. ## 3.10.5 * Added Ornaments. ## 3.10.4 * We handle manifest download/cache errors better, by deleting the cached file and letting you retry. * Date armor ratings end is on 9/20/2016 @ 2AM Pacific. * Fixed issues with broken images by downloading from Bungie.net with https. * Loadouts for multi-platform users will now save selected and equipped items for both platforms. Previously, when switching platforms, loadouts would remove items from the loadout for the opposite platform. ## 3.10.3 * Fixed a "move-canceled" message showing up sometimes when applying loadouts. * Bugged items like Iron Shell no longer attempt to compute quality. They'll fix themselves when Bungie fixes them. * Fixed "Aim assist" stat not showing up in CSV (and no stats showing up if your language wasn't English). * We now catch manifest updates that don't update the manifest version - if you see broken images, try reloading DIM and it should pick up new info. * Worked around a bug in the manifest data where Ornament nodes show up twice. * DIM won't allow you to move rare Masks, because that'll destroy them. * The "Random" auto loadout can now be un-done from the loadout menu. * For non-variable items (emblems, shaders, ships, etc) in a loadout, DIM will use whichever copy is already on a character if it can, rather than moving a specific instance from another character. ## 3.10.2 (2016-09-10) * Fixed error building talent grid for Hawkmoon. * Don't attempt to build record books when advisors are not loaded. * Dragged items now include their border and light level again. * New-item overlays have been restored (enable in settings). * Re-enable record book progress. * Better handle errors when record book info isn't available. * Show an error message if the manifest doesn't load. * Fix an error when equipping loadouts. * DIM usage tips will only show up once per session now. You can bring back previously hidden tips with a button in the settings page. ## 3.10.0 * Add ability to create loadouts by selecting sets of perks. * [#823](https://github.com/DestinyItemManager/DIM/issues/823) Added 'current' property to stores. * The DIM extension is now much smaller. * DIM can now display item information in all supported Destiny languages. Choose your language in the settings then reload DIM. * We now automatically pick up Destiny data updates, so DIM should work after patches without needing an update. * The Reputation section should match the in-game logos better now. * Disable new item overlays due to a bug. ## 3.9.2 * [#812](https://github.com/DestinyItemManager/DIM/issues/812) Removed rare masks from the items table used by the random item loadout. ## 3.9.1 * [#801](https://github.com/DestinyItemManager/DIM/issues/801) Resolved error with vendor page character sorting. * [#792](https://github.com/DestinyItemManager/DIM/pull/792) Warning if user clicks on perks to notify them that they can only be changed in game. * [#795](https://github.com/DestinyItemManager/DIM/pull/795) Updated strange coin icon for Xur. ## 3.9.0 * New glimmer-based filters, is:glimmeritem, is:glimmerboost, is:glimmersupply * Add option for new item and its popup to be hidden * Add ability to exclude items from loadout builder. * Expand/collapse sections in DIM. * Double clicking an item will equip it on the current character. 2x click on equipped, dequips. * Show current vendor items being sold. * Move popup won't pop up under the header anymore. * If you have an open loadout, and you click "Create loadout", it switches to the new loadout now instead of leaving the previous loadout open. * DIM is once again faster. * The loadout editor won't stay visible when you change platforms. * Fixed a lot of bugs that would show all your items as new. * New-ness of items persists across reloads and syncs across your Chrome profile. * New button to clear all new items. Keyboard shortcut is "x". * Help dialog for keyboard shortcuts. Triggered with "?". * When you have two characters of the same class, applying a loadout with a subclass will work all the time now. * Item class requirements are part of the header ("Hunter Helmet") instead of in the stats area. * You can search for the opposite of "is:" filters with "not:" filters. For example, "is:helmet not:hunter quality:>90". * Clicking away from the Xur dialog will close any open item popups. * Fixed an issue where you could not equip a loadout that included an exotic item when you already had an exotic equipped that was not going to be replaced by the loadout. * Better handling of items with "The Life Exotic" perk. * New aliases for rarity filters (is:white, is:green, is:blue, is:purple, is:yellow). * An alternate option for the "Gather Engrams" loadout can exclude gathering exotic engrams. * Removed popup notification for new items. * #798 Keyword searches will now scan perk descriptions. * #799 Randomize equipped items for current character. Don't look at us if you have to play a match using Thorn. ## 3.8.3 * Fix move popup not closing when drag-moving an item. * Added ability to and filters for track or untrack quests and bounties. * Fix issue where some sets would be missing from the loadout builder. * Fixed #660 where postmaster items would not appear in the Postmaster section of DIM, ie Sterling Treasure after the reset. * Fixed #697 where loadouts will no longer remove the loadouts for the opposite platform. * Fix an issue where loadouts will not show any items, or transfer any items. * Add option to show new item overlay animation ## 3.8.2 * Update filter list to include quality/percentage filters * Add year column to CSV export scripts * When you have filtered items with a search, you can select a new search loadout option in the loadout menu to transfer matching items. * The screen no longer jumps around when clicking on items, and the item details popup should always be visible. * Dialogs should be sized better now. * Fix character order in move popup buttons. * Restored the ability to set a maximum vault size. "Auto" (full width) is still an option, and is the default. * Armor quality is shown in Xur, loadouts, and the infusion dialog if advanced stats is turned on. * "Take" stackables works again. ## 3.8.1 * Added steps to Moments of Triumph popup (and other record books.) * Fixed wobbly refresh icon. * Fixed single item stat percentages. * Fixed armor export script. * Possible fix for loadout builder. ## 3.8.0 * Loadout builder redesign and major performance enchancements. * Items in the postmaster now have quality ratings, can use the infusion fuel finder, show up in the infusion fuel finder, compare against currently equipped items, etc. They behave just like a normal item except you can't move them and they're in a different spot. * The vault width preference has been removed - the vault now always takes up all the remaining space on the screen. * Section headers don't repeat themselves anymore. * Drop zones for items are larger. * Returning from the min-max tool no longer greets you with a blank, item-less screen. * Fixed a bug where loadouts were not properly restricted to the platform they were created for. * Xur's menu item will properly disappear when he leaves for the week. * New items are marked with a "shiny" animation, and there are notifications when new items appear. * The loadout menu may expand to fill the height of the window, but no more. The scrollbar looks nicer too. * Items can now be made larger (or smaller) in settings. Pick the perfect size for your screen! * The item info popup has a new header design. Let us know what you think! * Changing settings is faster. * You can now download your weapon and armor data as spreadsheets for the true data nerds among us. * The settings dialog is less spacious. * Engrams and items in the postmaster can now be locked (and unlocked). * The buttons on the move item popup are now grouped together by character. * When the "Hide Unfiltered Items while Filtering" option is on, things look a lot nicer than they did. * DIM is generally just a little bit snappier, especially when scrolling. * Clicking the icon to open DIM will now switch to an active DIM tab if it's already running. * Bungie.net will open in a new tab as a convenience for expired cookies. * Items in the Postmaster are sorted by the order you got them, so you know what'll get bumped when your postmaster is full. * Clicking the loadout builder button again, or the DIM logo, will take you back to the main screen. * You may now order your characters by the reverse of the most recent, so the most recent character is next to the vault. ## 3.7.4 * Removed the option to hide or show the primary stat of items - it's always shown now. * Add mode selection full/fast for users willing to wait for all best sets. * Loadout menus are now scrollable for users with over 8 custom loadouts on a single character. * Changing the character sort order now applies live, rather than requiring a refresh. * Use most recently logged in player to start with loadout builder. * Search queries will exclude the token `" and "` as some users were including that when chaining multiple filters. * Fix UI issue on move popup dialog that had some numbers expanding outside the dialog. * Consolidate beta icons to the icons folder. ## 3.7.3 * Fix rounding error that prevented some loadout sets from showing up. * Added filter for quality rating, ex - quality:>90 or percentage:<=94 ## 3.7.2 * Always show locked section in loadout builder. * Fix NaN issue in loadout builder. * Fix issues with 'create loadout' button in loadout builder. * For item leveling don't prefer unlevelled equipped items on other characters. * Various Loadout builder bug fixes and performance updates. ## 3.7.1 * Various Loadout builder bug fixes and performance updates. ## 3.7.0 * Added new armor/loadout tier builder. * Fix for all numbers appearing red in comparison view. * Updated to latest stat estimation formula. * Use directive for percentage width. ## 3.6.5 * Fix an issue where warlocks would see loadouts for all the other classes. ## 3.6.2 & 3.6.3 (2016-05-23) * Add warning if the lost items section of the postmaster has 20 items. * Stat bars are more accurately sized. * Add vendor progress * Add prestige level with xp bar under characters to replace normal xp bar after level 40. * It is no longer possible to choose column sizes that cause the vault to disappear. * The Vault now has a character-style header, and can have loadouts applied to it. Full-ness of each vault is displayed below the vault header. * New option to restore all the items that were in your inventory before applying a loadout, rather than just the equipped ones. * You can now undo multiple loadouts, going backwards in time. ## 3.6.1 * Removed the "Only blues" option in the infusion fuel finder, because it wasn't necessary. * Engram searches and the engram loadout features won't mistake Candy Engrams for real engrams. * Items in the Postmaster include their type in the move popup, so they're easier to distinguish. * Sometimes equipping loadouts would fail to equip one of your exotics. No more! * Add an 'is:infusable' search filter. * Add 'is:intellect', 'is:discipline', 'is:strength' search filters for armor. * XP Progress on bar items ## 3.6.0 (2016-05-03) * Bring back the infusion dialog as an Infusion Fuel Finder. It doesn't do as much as it used to, but now it's optimized for quickly finding eligable infusion items. * Fix a bug where hovering over a drop zone with a consumable/material stack and waiting for the message to turn green still wouldn't trigger the partial move dialog. * Added a new "Item Leveling" auto-loadout. This loadout finds items for you to dump XP into. It strongly favors locked items, and won't replace an incomplete item that you have equipped. Otherwise, it goes after items that already have the most XP (closest to completion), preferring exotics and legendaries if they are locked, and rares and legendaries if they're not locked (because you get more materials out of disassembling them that way). * There's a new setting that will show elemental damage icons on your weapons. Elemental damage icons are now always shown in the title of the item popup. * Elder's Sigil won't go above 100% completion for the score portion anymore. * Added roll quality percentage indicator. You can now see how your intellect/discipline/strength stacks up against the maximum stat roll for your armor. * DIM is smarter about what items it chooses to move aside, or to equip in the place of a dequipped item. * Added a new "Gather Engrams" loadout that will pull all engrams to your character. ## 3.5.4 * We won't try to equip an item that is too high-level for your character when dequipping items. * Fix a regression where subclasses wouldn't show up in Loadouts. They're still there, they just show up now! * Fixed another bug that could prevent item popups from showing up. * The vault can now be up to 12 items wide. * Sterling Treasure, Junk Items, and SLR Record Book added to DIM. * Manifest file updated. ## 3.5.3 * Fixed a bug that would prevent the loading of DIM if Spark of Light was in the postmaster. * Fixed a bug that prevented the Xur dialog from rendering. ## 3.5.2 * Fix a bug where item details popups would show above the header. * Fix showing Sterling Treasures in Messages. * Better error handling when Bungie.net is down. * Fix a bug where having items in the postmaster would confuse moves of the same item elsewhere. * Fix a bug where item comparisons no longer worked. * Added support for the classified shader "Walkabout". ## 3.5.1 * The Infusion Calculator has been removed, now that infusions are much more straightforward. * Pressing the "i" key on the keyboard will toggle showing item details in the item popup. * Add a menu item for when Xur is in town. This brings up a panel with Xur's wares, how much everything costs, how many strange coins you have, and lets you show the item details popup plus compare against any version of exotics you might already have to see if there's a better roll. ## 3.5 (2016-04-11) * DIM will now go to great lengths to make sure your transfer will succeed, even if your target's inventory is full, or the vault is full. It does this by moving stuff aside to make space, automatically. * Fixed a bug that would cause applying loadouts to fill up the vault and then fail. * Fixed a bug where DIM would refuse to equip an exotic when dequipping something else, even if the exotic was OK to equip. * When applying a loadout, DIM will now equip and dequip loadout items all at once, in order to speed up applying the loadout. * The search box has a new style. * Item moves and loadouts will now wait for each other, to prevent errors when they would collide. This means if you apply two loadouts, the second will wait for the first to complete before starting. * Item details are now toggled by clicking the "i" icon on the item popup, rather than just by hovering over it. ## 3.4.1 * Bugfix to address an infinite loop while moving emotes. ## 3.4.0 * Moving and equipping items, especially many at a time (loadouts) is faster. * When you save a loadout, it is now scoped to the platform it's created on, rather than applying across accounts. Loadouts created on one account used to show on both accounts, but wouldn't work on the wrong account. * You can now move partial amounts of materials. There's a slider in the move popup, and holding "shift" or hovering over the drop area will pop up a dialog for draggers. You can choose to move more than one stack's worth of an item, up to the total amount on a character. * New commands for materials to consolidate (move them all to this character) and distribute (divide evenly between all characters). * Loadouts can now contain materials and consumables. Add or remove 5 at a time by holding shift while clicking. When the loadout is applied, we'll make sure your character has _at least_ that much of the consumable. * Loadouts can now contain 10 weapons or armor of a single type, not just 9. * When making space for a loadout, we'll prefer putting extra stuff in the vault rather than putting it on other characters. We'll also prefer moving aside non-equipped items of low rarity and light level. * The is:engram search filter actually works. * Fixed an error where DIM would not replace an equipped item with an instance of the same item hash. This would cause an error with loadouts and moving items. [448](https://github.com/DestinyItemManager/DIM/issues/448) * Loadouts can now display more than one line of items, for you mega-loadout lovers. * Items in the loadout editor are sorted according to your sort preference. ## 3.3.3 (2016-03-08) * Infusion calculator performance enhancements * Larger lock icon * Completed segments of Intelligence, Discipline, and Strength are now colored orange. ## 3.3.2 (2016-03-04) * If multiple items in the infusion calculator have the same light, but different XP completion percentage, favor suggesting the item with the least XP for infusion. * Keyword search also searches perks on items. * New search terms for is:engram, is:sword, is:artifact, is:ghost, is:consumable, is:material, etc. * Items can be locked and unlocked by clicking the log icon next to their name. * Display intellect/discipline/strength bars and cooldown for each character * Loadouts have a "Save as New" button which will let you save your modified loadout as a new loadout without changing the loadout you started editing. * Autocomplete for search filters. * Comparing stats for armor now shows red and green better/worse bars correctly. * Fixed showing magazine stat for weapons in the vault. * Fixed infusion material cost for Ghosts and Artifacts (they cost motes of light). * Fix a case where the item properties popup may be cut off above the top of the screen. * Transfer/equip/dequip actions for edge cases will now succeed as expected without errors. * Manifest file update. ## 3.3.1 (2016-02-19) * Updated the manifest file. ## 3.3 (2016-02-15) * Infusion auto calculator is much faster. * Items in the infusion calculator don't grey out when a search is active anymore. * Full cost of infusions is now shown, including exotic shards, weapon parts / armor materials, and glimmer. * Show a better error message when trying to equip an item for the wrong class. Before it would say you weren't experienced enough. * Add a button to the infusion calculator that moves the planned items to your character. * Add a filter to the infusion calculator to limit the search to only rare (blue) items. * The infusion auto calculator runs automatically, and now presents a list of different attack/defense values for you to choose from. Selecting one will show the best path to get to that light level. * The infusion calculator greys out items that are already used or are too low light to use, rather than hiding them. * The item move popup now has an entry for the infusion calculator, to make it easier to find. * Hold Shift and click on items in the infusion calculator to prevent the calculator from using that item. * If you have an exotic class item (with "The Life Exotic" perk) equipped, you can now equip another exotic without having the class item get automatically de-equipped. Previously, this worked only if you equipped the non-class-item exotic first. * Armor, Artifacts, and Ghosts now show the difference in stats with your currently equipped item. Also, magazine/energy between swords and other heavy weapons compares correctly. * The is:complete, is:incomplete, is:upgraded, is:xpincomplete, and is:xpcomplete search keywords all work again, and their meanings have been tweaked so they are all useful. * The talent grid for an item are now shown in the item details, just like in the game, including XP per node. * Subclasses show a talent grid as well! * The item stats comparison will no longer be cleared if DIM reloads items while an item popup is open. * Bounties and quests are now separated, and under their own "Progress" heading. * Bounties, quests, and anything else that can have objectives (like test weapons and runes) now show their objectives and the progress towards them. As a result, completion percentages are also now accurate for those items. * Descriptions are now shown for all items. * Include hidden stats "Aim Assist" and "Equip Speed" for all weapons. You can still see all hidden stats by visiting DTR via the link at the top of item details. * Weapon types are now included in their popup title. * Removed Crimson Days theme. It will return. * Fixed issue at starts up when DIM cannot resolve if the user is logged into Bungie.net. ## 3.2.3 * Updated Crimson Days Theme. * Removed verge.js ## 3.2.2 * Updated Crimson Days Theme. ## 3.2.1 (2016-02-04) * Crimson Days theme. * Weapons and armor now show all activated perks (including scopes, etc), in the same order they are shown in the game. * Only display the "more info" detail icon if there's something to show. * If you try to move an item into a full inventory, we'll reload to see if you've already made space in the game, rather than failing the move immediately. * The Infusion dialog now has a "Maximize Attack/Defense" button that figures out how to get the highest stats with the fewest number of infusions. * You can now create a loadout based on what you've got equipped by selecting "From Equipped" in the "Create Loadout" menu item. * After applying a loadout, a new pseudo-loadout called "Before 'Your Loadout'" appears that will put back the items you had equipped. ## 3.2 * In the "Loadouts" dropdown is a new "Maximize Light" auto-loadout that does what it says, pulling items from all your characters and the vault in order to maximize your character's light. * Lots of performance improvements! Loading DIM, refreshing, moving items, and searching should all be faster. * DIM will now refresh immediately when you switch back to its tab, or come back from screensaver, etc. It won't automatically update when it's in the background anymore. It still periodically updates itself when it is the focused tab. * New "is:year1" and "is:year2" search filters. * Artifacts now have the right class type (hunter, titan, etc). * The reload and settings icons are easier to hit (remember you can also hit "R" to reload. * The move popup closes immediately when you select a move, rather than waiting for the move to start. * New sort option of "rarity, then primary stat". ================================================ FILE: docs/OLD_CHANGELOG/OLD_CHANGELOG_4.X.X.md ================================================ ## v5 CHANGELOG * v5 CHANGELOG available [here](/docs/OLD_CHANGELOG/OLD_CHANGELOG_5.X.X.md) ## 4.77.0 (2018-11-11) * Completed bounties now sort to the bottom of the Pursuits. * Return mods to the compare view. * Item popup background now indicates rarity rather than burn type. * Triumphs are now displayed on the Progress page. * Infusion dialog now separates out duplicate items. * The Progress page now shows progress towards reset. * Added some sources to the search dialog. * source: * edz, titan, nessus, io, mercury, mars, tangled, dreaming * crucible, trials, ironbanner * zavala, ikora, gunsmith, gambit, eververse, shipwright * nm, do, fwc * leviathan, lastwish, sos, eow, prestige, raid * prophecy, nightfall, adventure * In Chrome you can now Install DIM from the hamburger menu and use it as a standalone app. Chrome will support macOS later. ## 4.76.0 (2018-11-04) ## 4.75.0 (2018-10-28) * DIM now supports searching by season, event and year in Destiny 2. * is:season1, is:season2, is:season3, is:season4 * is:dawning, is:crimsondays, is:solstice, is:fotl * Performance improvements ## 4.74.1 (2018-10-21) * We no longer support searching D1 vendor items. * Added support for showing ratings and reviews based on the item roll in Destiny 2. * Fix for missing class names in the loadout builder in Firefox. * Added item search to D2 vendors. * Collections now include the in-game Collections. * D2 Vendors and Progress page now have collapsible sections. * Catalysts are sorted above Ornaments on the Collections page. * Fix a bug that could accidentally erase loadouts. Don't forget you can restore your data from old Google Drive backups from the Settings page. * is:hasmod now includes Backup Mag. * is:ikelos now includes Sleeper Simulant. ## 4.74.0 (2018-10-14) * Added negative search. Prefix any search term with `-` and it will match the opposite. * Added `perk:"* **"` search filter to match any keywords against perks on an item * Added some missing `stat:` * Lock and unlock items matching your current search from the same menu you use for tagging them. * Updated icons across the app. ## 4.73.0 (2018-10-07) * Added `is:heroic` search filter for armor with heroic resistance. * New option to manually sort your characters. * No longer forgetting what perks we recommended. * Fix mods/perks on items - there was a bug that affected both display and searches. * Fix is:hasmod search to include some more mods. * You can now drag items into the loadout drawer. * D2 spreadsheet export (in settings) covers perks now. * You can also export ghosts (with perks) for D1/D2. * Filters can now be combined with "or" to match either filter. For example: "is:shotgun or is:handcannon". ## 4.72.0 (2018-09-30) * Add searches `is:transmat`, `is:armormod`, `is:weaponmod`, and `is:transmat`, and removed D1 `is:primaryweaponengram`, `is:specialweaponengram`, and `is:heavyweaponengram`. * Show daily gambit challenge and daily heroic adventure in milestones. ## 4.71.0 (2018-09-23) * Removed a bunch of help popups. * Added information about unique stacks. * Added `is:maxpower` search to return highest light items. * Added `is:modded` search to return items that have a mod applied. * Bounties with expiration times are now shown, and are sorted in front in order of expiration time. * Added masterwork tier range filter. * Highlight the stat that is boosted by masterwork in item details. * Masterwork mod hover now shows the type/name of masterwork. ## 4.70.2 (2018-09-17) * Fix some instances where DIM wouldn't load. * Fix the About and Backers pages. * Hide classified pursuits. ## 4.70.1 (2018-09-17) ## 4.70.0 (2018-09-16) * Display armor resistance type on item icon and include in search filters. * Giving more weight to ratings with reviews than ratings alone. Also, hiding lone ratings. * Custom loadouts now display below our special auto loadouts. * Added inverse string search for items and perks (prefix with minus sign) * Postmaster is now on top of the screen (but disappears when empty). * Individual inventory buckets are no longer collapsible, but disappear when empty. * D1 vault counts are removed from their section headers. * Fixed an issue where the display would be messed up when colorblind mode is on. * Restored the keyboard shortcut cheat sheet (press ?). * The max light loadout prefers legendaries over rares. * Unclaimed engrams are shown up in the Postmaster section. * Infusion transfer button is now visible on mobile devices. ## 4.69.1 (2018-09-10) * Max power value in 'Maximum Power' loadout is now calculated correctly. ## 4.69.0 (2018-09-09) * Max power updated to 600 for Forsaken owners. * Fixed Year 1 weapons not having an elemental damage type. * Many bugfixes post-Forsaken launch. * Add Infamy rank to progress page. * Bounties now show their rewards on the Progress and Vendors pages. * The Progress page has been cleaned up to better reflect the state of the game since Forsaken. * Pursuits are sorted such that bounties are displayed together. * Add "is:randomroll" search for items that have random rolls. * Added "is:bow" and "is:machinegun" searches. * Remove "is:powermod" and "basepower:" searches. * Masterworks now have a gold border. Previously items with a power mod had a gold border, but there are no more power mods. * Added Bow stats "Draw Time" and "Inventory Size". * Disabled vendorengrams.xyz integration until they are back online. * Review modes - say hello to Gambit (and goodbye to Trials, at least for a little while). * Ratings platform selection changes made easier. * Added Etheric Spiral and Etheric Helix to the list of reputation items. ## 4.68.3 (2018-09-03) ## 4.68.2 (2018-09-03) ## 4.68.1 (2018-09-03) ## 4.68.0 (2018-09-02) * Fixed: Destiny 2 - Sort by character age. * Item popup shows the ammo type of D2 weapons. * New is:primary, is:special, and is:heavy search terms for ammo types. * Add is:tracerifle and is:linearfusionrifle searches. * Added Korean as a language option. * We have a new Shop selling enamel pins and T-shirts. * Ratings system understands random rolls in D2. * Search help added for searching by ## of ratings. ## 4.67.0 (2018-08-26) ## 4.66.0 (2018-08-19) * DIM now refreshes your inventory automatically every 30 seconds, rather than every 5 minutes. * Clicking "transfer items" in the Infusion tool will now always move them to the active character. * The infusion tool will now include locked items as potential infusion targets even if the checkbox isn't checked (it still affects what can be a source item). * If you are at maximum light, DIM now alerts you when vendors are selling maximum light gear and engrams, courtesy of VendorEngrams.xyz. ## 4.65.0 (2018-08-12) ## 4.64.0 (2018-08-05) ## 4.63.0 (2018-07-29) * Fixed a bug that could cause iOS Safari to hang. ## 4.62.0 (2018-07-22) * Xur has been removed from the header in D1. Find him in the Vendors page. ## 4.61.0 (2018-07-15) * Fix a bug that would leave behind stackable items when moving certain loadouts like "Gather Reputation Items". * The is:haspower search works once again. * The is:cosmetic search will now work for Destiny 2. * Added is:prophecy search which will return all prophecy weapons from CoO. * Added is:ikelos search which will return all ikelos weapons from Warmind. ## 4.60.0 (2018-07-08) * Farming mode won't try to move unmovable reputation tokens. * Filters like stat:recovery:=0 now work (they couldn't match stat values of zero before). * Checking with VendorEngrams.xyz to see if 380 drops may be right for you. ## 4.59.0 (2018-07-01) * New iOS app icons when you add to home screen. * Ornaments now show additional reasons why you can't equip them. * The is:inloadout search works once again. * Fix a bug where the item popup could hang iOS Safari in landscape view. * Add a link to lowlines' Destiny map for collecting ghost scannables, latent memories, and sleeper nodes. ## 4.58.0 (2018-06-24) * Factions now show seasonal rank instead of lifetime rank. * Vendors show their faction rank next to their reward engrams. * Factions in the progress page also link to their vendor. * Quest keys in your Pursuits now show their quantity. They're still on the Progress page. ## 4.57.0 (2018-06-17) * Item sizing setting works in Edge. * Lock and unlock won't get "stuck" anymore. ## 4.56.5 (2018-06-11) * Fix for item popups not working ## 4.56.0 (2018-06-10) * Add "is:hasshader" search filter to select all items with shaders applied. * Fixed some bugs in older Safari versions. * Errors on Progress, Collections, and Vendors pages won't take out the whole page anymore, just the section with the error. * Fix bugs where a stray "0" would show up in odd places. * Align Progress columns better for accounts with fewer than 3 characters. ## 4.55.0 (2018-06-03) * Displaying available rating data in spreadsheet export. * Correctly display masterwork plug objectives - check the "Upgrade Masterwork" plug for catalyst updates. * The Collections page now shows progress towards unlocking ornaments. Due to restrictions in the API, it can only show ornaments that go with items you already have. ## 4.54.0 (2018-05-27) * Fix the display of crucible rank points. * Fix faction rank progress bars on D1. * Compare view includes perks and mods for D2 items. ## 4.53.0 (2018-05-20) * Add previews for engrams and other preview-able items. * Display Crucible ranks on the progress page. * Add emotes back to the collections page. * Remove masterwork objectives that never complete. * Fix loading loadouts the first time you open a character menu. * Fix exporting CSV inventories in Firefox. ## 4.52.0 (2018-05-13) * Collection exotics are no longer duplicated. They are also sorted by name. * Updated max power to 380. * Vendors and collections will no longer show items exclusive to platforms other than the current account's platform. * Fix masterworks not showing as masterworks. * Set the max base power depending on which DLC you own. ## 4.51.2 (2018-05-09) * Handle the Warmind API bug better, and provide helpful info on how to fix it. ## 4.51.1 (2018-05-08) * Fix progress page not displaying after the Warmind update. ## 4.51.0 (2018-05-06) * Fix a bug where having mods, shaders, or materials in the postmaster might make it impossible to move any mod/shader/material into or out of the vault. * Add links to Ishtar Collective on items with lore. ## 4.50.0 (2018-04-30) * The settings page now shows how much of your local storage quota is being used by DIM (if your browser supports it). * Add search filters based on character location on dim (is:inleftchar / inmiddlechar / inrightchar) and for vault (is:invault) and current/last logged character (incurrentchar), that is marked with a yellow triangle. * Fixed a bug where the "Restore Old Versions" tool wouldn't actually let you see and restore old versions. ## 4.49.1 (2018-04-23) * Fix loadouts. ## 4.49.0 (2018-04-22) * The DIM changelog popup has moved to a "What's New" page along with Bungie.net alerts and our Twitter feed. We also moved the "Update DIM" popup to the "What's New" link. * Fix moving mods and shaders from the postmaster. * Remove "Take" button from stackables in the postmaster. * The Collections page now has a link to DestinySets.com. ## 4.48.0 (2018-04-15) * You can specify game modes for reading and making ratings and reviews. * Full General Vault, Mods, and Shaders buckets are highlighted in red. * Adding DIM to your home screen on iOS was broken for iOS 11.3. It's fixed now! ## 4.47.0 (2018-04-09) ## 4.46.0 (2018-04-02) * Added a page to browse and restore old revisions of Google Drive data. * Emblems now show a preview of their nameplate in the item details popup. * New Vendors page shows all the items you can buy from various vendors. * New Collections page shows your exotics, emotes, and emblems kiosks. * Engram previews from the faction display and vendors pages show what could be in an engram. * Keyword search now includes item descriptions and socket perk names and descriptions. ## 4.45.0 (2018-03-26) * Searching mods and perks in D2 now searches non-selected perks as well. * Perks are in the correct order again (instead of the selected one being first always). * Non-purchasable vendor items are displayed better. * Storage settings break out loadouts and tags/notes between D1 and D2 items. * A new revisions page allows you to restore old versions of settings from Google Drive. * Emblems show a preview of the nameplate graphic. * Fix "is:dupelower" to only affect Weapons/Armor * Add armor stats to the "stat:" filter (in D2 only) * Add ":=" comparison to the text complete tooltip ## 4.44.0 (2018-03-19) * Fixed the "recommended perk" being wrong very often. * Improved the display of perks, shaders, and mods on items. Improved the popup details for those items as well - this includes ornament unlock progress. * Stackable items like mods and shaders have less chance of being left behind during search transfers. * Put back "Make Room for Postmaster" in D1 - it was removed accidentally. * Items matching a search are now more highlighted. Removed "Hide Unfiltered Items" setting. ## 4.43.0 (2018-03-12) * Fix some cases where moving stacks of items would fail. * Fix "Gather Reputation Items" from not gathering everything. * More items can be successfully dragged out of the postmaster. ## 4.42.0 (2018-03-05) * Compare tool shows ratings, and handles missing stats better. * Fixed display of masterwork mod and ornaments. * Remove Auras from inventory since they're part of Emblems now. * Fancy new emblems show all their counters correctly. * Improved moving mods, shaders, and consumables via search loadouts. They can now go to any character (not just the active one) and aren't limited to 9 items. * Pausing over a drop zone to trigger the move-amount dialog works every time now, not just the first time. ## 4.41.1 (2018-02-19) * Fix dupelower logic. * Fixed bugs preventing DIM from loading in some browsers. * See previews of the items you'll get from faction packages and Xur from links on the Progress page. ## 4.41.0 (2018-02-19) * Mobile on portrait mode will be able to set the number of inventory columns (the icon size will be resized to accommodate). * You can now check your emblem objectives. * Armor mods show more info. * Destiny 1 transfers are faster. * DIM is better at equipping exotics when you already have exotic ghosts, sparrows, and ships equipped. * Pulling an item from the postmaster updates the list of items quickly now. * Navigation from "About" or "Backers" back to your inventory works. * is:dupelower breaks ties more intelligently. ## 4.40.0 (2018-02-12) ## 4.39.0 (2018-02-05) * Fixed random loadout feature taking you to a blank page. ## 4.38.0 (2018-01-31) * Fixed display of Clan XP milestone. * DIM's logic to automatically move aside items to make room for what you're moving is smarter - it'll leave put things you just moved, and it'll prefer items you've tagged as favorites. * In D2, "Make room for Postmaster" has been replaced with "Collect Postmaster" which pulls all postmaster items we can onto your character. You can still make room by clicking "Space". * Fix pull from postmaster to clear exactly enough space, not too many, but also not too few. * Accounts with no characters will no longer show up in the account dropdown. * Item tagging via keyboard should be a little more international-friendly. Calling the help menu (via shift+/) is too. * Fixed XP required for well-rested perk after the latest Destiny update. ## 4.37.0 (2018-01-29) * Masterwork differentiation between Vanguard / Crucible, highlight of stat being affected by MW. * The "Well Rested" buff now appears as a Milestone on your Progress page. * Nightfall modifiers are shown on the Progress page. * Storage (Google Drive) settings have moved to the Settings page. * You can configure a custom item sorting method from the Settings page. * Improved display of the account selection dropdown. ## 4.36.1 (2018-01-22) * Attempt to fix error on app. * Moving an item from the postmaster will now only clear enough space for that one item. ## 4.36.0 (2018-01-22) * Attempt to fix error on app. ## 4.35.0 (2018-01-22) * The Settings page has been redesigned. * Your character stats now update live when you change armor. * New settings to help distinguish colors for colorblind users. * DIM should load faster. * DIM won't try to transfer Faction tokens anymore. ## 4.34.0 (2018-01-15) * Sorting characters by age should be correct for D2 on PC. * The infusion fuel finder now supports reverse lookups, so you can choose the best thing to infuse a particular item _into_. * Labeled the Infusion Fuel Finder button. * Trace Rifles are highlighted again on is:autorifle search. * Factions that you can't turn in rewards to are now greyed out. We also show the vendor name, and the raw XP values have moved to a tooltip. * The settings page has been cleaned up and moved to its own page. ## 4.33.1 (2018-01-09) * Fix DIM loading on iOS 11.2.2. ## 4.33.0 (2018-01-08) * A brand new Progress page for Destiny 2 displays your milestones, quests, and faction reputation all in one place. That information has been removed from the main inventory screen. * We've changed around the effect for masterworks a bit more. ## 4.32.0 (2018-01-02) * Added hotkey for search and clear (Shift+F). * Masterworks show up with an orange glow like in the game, and gold borders are back to meaning "has power mod". * Mercury reputation items are now handled by farming mode and gather reputation items. * Tweak max base power / max light calculations to be slightly more accurate. * Display D2 subclass talent trees. We can't show which ones are selected/unlocked yet. * Moving items on Android should work better. * Rotating to and from landscape and portrait should be faster. * Fix quest steps showing up in the "haspower" search. * Do a better job of figuring out what's infusable. * Added a reverse lookup to Infusion Fuel Finder. ## 4.31.0 (2017-12-25) * "is:complete" will find completed rare mod stacks in Destiny 2. ## 4.30.0 (2017-12-18) * NEW - Revamped rating algorithm for D2 items. * Fixed a bug trying to maximize power level (and sometimes transfer items) in Destiny 2. * When hovering over an icon, the name and type will be displayed * Allowing more exotic item types to be simultaneously equipped in Destiny 2 * Initial support for masterworks weapons. * Fixed reporting reviews in Destiny 2. * Fixed item filtering in Destiny 2. ## 4.29.0 (2017-12-13) * Added Mercury reputation. * Added Crimson Exotic Hand Canon. ## 4.28.0 (2017-12-11) * NEW - Move items from the postmaster in DIM! ## 4.27.1 (2017-12-05) * Key for perk hints in D2. * Fixed bug loading items with Destiny 2 v1.1.0. ## 4.27.0 (2017-12-04) * Added setting to pick relevant platforms for reviews. * Fix review area not collapsing in popup. * Fix display of option selector on reviews tab when detailed reviews are disabled. ## 4.26.0 (2017-11-27) * Don't show community best rated perk tip if socket's plugged. * is:haslevel/haspower (D1/D2) fix in cheatsheet. * Fix mobile store pager width ## 4.25.1 (2017-11-22) * Added Net Neutrality popup. ## 4.25.0 (2017-11-20) ## 4.24.1 (2017-11-13) ## 4.24.0 (2017-11-13) * Bungie has reduced the throttling delay for moving items, so you may once again move items quickly. ## 4.23.0 (2017-11-06) ## 4.22.0 (2017-10-30) * Add a 'bulk tag' button to the search filter. * Add basepower: filter and is:goldborder filter. * Fix filtering in D1. * Add a button to clear the current search. * Fix moving partial stacks of items. * Fixed "transfer items" in the Infusion Fuel Finder. * Giving hints about the community's favorite plugs on D2 items. ## 4.21.0 (2017-10-23) * Community reviews (for weapons and armor) are in for Destiny 2 inventory. * Charting weapon reviews. * Fixed the shadow under the sticky characters bar on Chrome. * Add an option to farming mode that stashes reputation items in the vault. * Add a new smart loadout to gather reputation items for redemption. * Scroll the loadout drawer on mobile. * Show character level progression under level 20 for D2. * Stacks of three or more rare mods now have a yellow border ## 4.20.1 (2017-10-16) * Fixed an error when trying to space to move items. ## 4.20.0 (2017-10-16) * Sort consumables, mods, and shaders in a more useful way (generally grouping same type together, alphabetical for shaders). * Show the hidden recoil direction stat. * Link to DestinyDB in your language instead of always English. * Updated documentation for search filters. * Fixed logic that makes room for items when your vault is full for D2. ## 4.19.2 (2017-10-11) * Keyword searches now also search on mod subtitles, so `is:modifications helmet void` will bring only Helmet Mods for Void subclass. * Add Iron Banner reputation. ## 4.19.1 (2017-10-10) * Fix landscape orientation not working on mobile. * Fix D1 stats in loadout builder and loadout editor. ## 4.19.0 (2017-10-09) * Added `stack:` to search filters for easier maintenance of modifications. * Add missing type filters for D2 (try `is:modifications`)! * Bring back keyboard shortcuts for tagging (hit ? to see them all). * The "Max Light" calculation is even more accurate now. * Added `PowerMod` column to CSV export indicating whether or not a weapon or piece of armor has a power mod * Support sorting by base power. * Hide "split" and "take" button for D2 consumables. * OK really really fix the vault count. * Fix showing item popup for some D1 items. * Changed how we do Google Drive log-in - it should be smoother on mobile. * Completed objectives will now show as "complete". * Bring back the yellow triangle for current character on mobile. * Updated `is:dupelower` search filter for items to tie break by primary stat. ## 4.18.0 (2017-10-02) * Updated `is:dupelower` search filter for items with the same/no power level. * Fix some issues with Google Drive that might lead to lost data. * Really fix vault counts this time! ## 4.17.0 (2017-09-29) * Fix bug that prevented pinned apps in iOS from authenticating with Bungie.net. ## 4.16.2 (2017-09-29) * Added `is:dupelower` to search filters for easier trashing. * Added missing factions to the reputation section for Faction Rally. * Fix in infusion calculator to correctly consider +5 mod * Fix for CSV export (e.g.: First In, Last Out in 2 columns) ## 4.16.1 (2017-09-26) * Bugfixes for iOS 10.0 - 10.2. ## 4.16.0 (2017-09-25) * Added item type sort to settings group items by type (e.g. all Sniper Rifles together). * Reputation emblems are the same size as items now, however you have item size set. * Shaders show up in an item's mods now. * Transfering search loadouts is more reliable. * Fixed a serious bug with storage that may have deleted your tags and notes. It's fixed now, but hopefully you had a backup... * Highlight mods that increase an item's power with a gold border. New 'is:powermod' search keyword can find them all. * Phone mode should trigger even on really big phones. * More places can be pressed to show a tooltip. * Fixed showing quality for D1 items. * D2 subclasses are diamonds instead of squares. * Max Base Power, Mobility, Resilience, and Recovery are now shown for each character. * Legendary shards have the right icon now. * Fix newly created loadouts showing no items. * Inventory (mods, shaders, and consumables) in your vault now show up separated into the vault, and you can transfer them to and from the vault. * Search keywords are now case-insensitive. * You can now lock and unlock D2 items. * Equipping an exotic emote won't unequip your exotic sparrow and vice versa. * Item popups aren't weirdly tall on Firefox anymore. * Armor stats now match the order in the game. * Infusion calculator now always gives you the full value of your infusion. * Show a warning that your max light may be wrong if you have classified items. * CSV export for D2 weapons and armor is back. * Add text search for mods and perks. * Add "Random Loadout" to D2. You gotta find it though... ## 4.15.0 (2017-09-18) * D2 items with objectives now show them, and quests + milestones are displayed for your characters. * Custom loadouts return for D2. * D2 items now display their perks and mods. * DIM won't log you out if you've been idle too long. * Swipe left or right anywhere on the page in mobile mode to switch characters. * If you have lots of inventory, it won't make the page scroll anymore. * Power level will update when you change equipment again. * Searches will stay searched when you reload info. * Max light loadout won't try to use two exotics. * Farming mode looks better on mobile. * If you're viewing a non-current character in mobile, it won't mess up on reload anymore. * You can tag and write notes on classified items to help remember which they are. * The Infusion Fuel Finder is back for D2. * The "Max Light" calculation is more accurate now. * Mods now show more detail about what they do. ## 4.14.0 (2017-09-14) * Added back in Reputation for D2. * Max Light Loadout, Make Room for Postmaster, Farming Mode, and Search Loadout are all re-enabled for D2. * Classified items can be transferred! * Fixed search filters for D2. * Show hidden stats on D2 items. * D2 inventory (mods, shaders, etc) now take the full width of the screen. ## 4.13.0 (2017-09-09) * DIM will remember whether you last used D2 or D1. * Lots of DIM functionality is back for D2. * We now highlight the perks from high community reviews that you don't have selected. ## 4.12.0 (2017-09-05) * Early Destiny 2 support! We have really basic support for your Destiny 2 characters. Select your D2 account from the dropdown on the right. This support was built before we even got to start playing, so expect some rough edges. * There's a new phone-optimized display for your inventory. See one character at a time, with larger items. Swipe between characters by dragging the character header directly. * Info popups aren't gigantic on mobile anymore. * Fix a case where changes to preferences may not be saved. ## 4.11.0 (2017-09-02) * Fix a case where DIM wouldn't work because auth tokens had expired. ## 4.10.0 (2017-08-26) * You can flag reviews for being offensive or arguing or whatever. Be helpful but also be nice. * Remove the browser compatibility warning for Opera and prerelease Chrome versions. ## 4.9.0 (2017-08-19) * No changes! ## 4.8.0 (2017-08-12) * No changes! ## 4.7.0 (2017-08-05) * Made loadout builder talent grids tiny again. * If you autocomplete the entire filter name and hit enter, it will no longer hang the browser. * Updated the About page and FAQ. * Fixed a case where DIM would fail to load the latest version, or would load to a blank page unless force-reloaded. * Added some helpful info for cases where DIM might fail to load or auth with Bungie.net. * Added a warning when your browser is not supported by DIM. * DIM no longer supports iOS 9. ## 4.6.0 (2017-07-29) * Fix a bug where the popup for Xur items was below Xur's own popup. * Hiding community rating for items with only one (non-highlighted) review. * The first item in the search autocompleter is once again selected automatically. * If you don't have the vault width set to "auto", the inventory is once again centered. ## 4.5.0 (2017-07-22) * Added "reviewcount" filter to filter on the number of reviews on an item. * Fix slight horizontal scroll on inventory view. * On mobile, tapping outside of dialogs and dropdowns to dismiss them now works. * The item detail popup now does a better job of fitting itself onto the screen - it may appear to the left or right of an item now! * Press on a talent grid node to read its description. The same goes for the stats under your character. * Subclasses now have the correct elemental type in their header color. * Drag and drop should be much smoother now. * You can select Destiny 2 accounts from the account dropdown now - but won't do much until Destiny 2 is released and we have a chance to update DIM to support it! ## 4.4.0 (2017-07-15) * New filters for ornaments - is:ornament, is:ornamentmissing, is:ornamentunlocked * Fixed a bug where item data would not respect your language settings. * Weapon reviews now show up immediately, and can be edited. - If you have been less than friendly, now would be a very good time to edit yourself and put a better foot forward. * Sorting reviews to support edits and highlighted reviews. * Logging out now brings you to Bungie's auth page, where you can choose to change account or not. * Fixed "Clear New Items" not working. * Adjusted the UI a bunch to make it work better on mobile. Just a start - there's still a long way to go. * The announcement about DIM being a website won't show more than once per app session. * Google Drive syncing is a bit smoother. * Fixed a case where you couldn't create a new class-specific loadout. * On Firefox, the new-item shines don't extend past the item anymore. * Do a better job of refreshing your authentication credentials - before, we'd sometimes show errors for a few minutes after you'd used DIM for a while. * The filters help page has been localalized. * Separate the light: and level: filters. level now returns items matching required item level, light returns items matching the light level. ## 4.3.0 (2017-07-08) * DIM is now just a website - the extension now just sends you to our website. This gives us one, more cross-platform, place to focus on and enables features we couldn't do with just an extension. Don't forget to import your data from the storage page! * Scrolling should be smoother overall. * Vendor weapons now show reviews. * Add a "sort by name" option for item sorting. * In Google Chrome (and the next version of Firefox), your local DIM data won't be deleted by the browser in low storage situations if you visit DIM frequently. * Ratings will no longer disappear from the item details popup the second time it is shown. * Info popups should do a better job of hiding when you ask them to hide. ## 4.2.4 (2017-07-03) * Work around a Chrome bug that marked the extension as "corrupted". ## 4.2.3 (2017-07-03) * Fix log out button. * Put back the accidentally removed hotkeys for setting tags on items. * Fixed some visual goofs on Firefox. * Fix a case where DIM would never finish loading. ## 4.2.2 (2017-07-02) * Fix DIM being invisible on Firefox * Fix a case where DIM would never finish loading. * Put back the accidentally removed hotkeys for setting tags on items. ## 4.2.1 (2017-07-01) * Actually turn on Google Drive in prod. ## 4.2.0 (2017-07-01) * Exclude all variants of 'Husk of the Pit' from 'Item Leveling' loadout. * Add a new storage page (under the floppy disk icon) for managing your DIM data. Import and export to a file, and set up Google Drive storage to sync across machines (website only). You can import your data from the Chrome extension into the website from this page as well. * The settings page has been cleaned up and reworded. * Added missing Trials emblems and shaders to the is:trials search. * DIM should look more like an app if you add it to your home screen on Android. * DIM will show service alerts from Bungie. ## 4.1.2 (2017-06-25) * Add a "Log Out" button in settings. ## 4.1.1 * Fixed changelog popup too large to close. ## 4.1.0 (2017-06-24) * Fixed the logic for deciding which items can be tagged. * Fix "Make room for postmaster". * Record books have been moved out of the inventory into their own page. Get a better look at your records, collapse old books, and narrow records down to only those left to complete. * Fix changing new-item shine, item quality display, and show elemental damage icon preferences. They should apply immediately now, without a reload.x * Localization updates. * Fixed objective text in the record book floating above stuff. * Fixed displaying record objectives that are time-based as time instead of just a number of seconds. * When pinned to the iOS home screen, DIM now looks more like a regular browser than an app. The upside is you can now actually authorize it when it's pinned! * Loadouts with a complete set of equipped armor now include a stat bar that will tell you the stat tiers of the equipped loadout pieces. * Loadouts with non-equipping items now won't _de-equip_ those items if they're already equipped. #1567 * The count of items in your loadout is now more accurate. * DIM is now better at figuring out which platforms you have Destiny accounts on. * DIM is faster! * Added Age of Triumph filters is:aot and is:triumph * Add gunsmith filter is:gunsmith * Updated filters to remove common items for specific filters (e.g. is:wotm no longer shows exotic items from xur, engrams, and planetary materials) * Loadout Builder's equip button now operates on the selected character, not your last-played character. * Loadout Builder no longer has equip and create loadout buttons for loadouts that include vendor items. * Loadout Builder is faster. * DIM has a new logo! * Elemental damage color has been moved to a triangle in the upper-left corner of your weapon. * See community weapon ratings in DIM, and submit your own! Weapon ratings can be turned on in Settings, and will show up on your individual weapons as well as in the details popup. You can submit your own reviews - each review is specific to the weapon roll you're looking at, so you know whether you've got the god roll. ## v3 CHANGELOG * v3 CHANGELOG available [here](/docs/OLD_CHANGELOG/OLD_CHANGELOG_3.X.X.md) ================================================ FILE: docs/OLD_CHANGELOG/OLD_CHANGELOG_5.X.X.md ================================================ ## v6 CHANGELOG * v6 CHANGELOG available [here](/docs/OLD_CHANGELOG/OLD_CHANGELOG_6.X.X.md) ## 5.74.0 (2020-03-15) * Updated item metadata for Season of the Worthy! Mod slots, sources, ghost perks, etc. * Fixed a bug where using the colorblindness filters interfered with drag and drop. * Restyled vendor engrams icons to make them clearer. * Quest steps now use their questline name as a title, and the quest step as subtitle, just like in game. * Reorganized some of the search keyword suggestions. * Add "holdsmod:" search to find armor compatible with a certain season's mods. * Behind-the-scenes changes to support upcoming DIM Sync storage feature. ## 5.73.0 (2020-03-08) * You can now restrict what items get chosen by the random loadout feature by having an active search first. Try typing "tag:favorite" or "is:pulserifle" and then choosing Randomize. * Improved drag and drop performance on some browsers. * Removed the Factions section from the Progress page. You can still see faction rank on the Vendors page. ## 5.72.0 (2020-03-01) * Worked around a long-standing Bungie.net bug where items would change lock state when moved. One caveat is that DIM will always preserve the lock state as it sees it, so if you've locked/unlocked in game and haven't refreshed DIM, it may revert your lock. ## 5.71.0 (2020-02-23) * Removed "Gather Reputation Tokens" feature. You can do the same thing with an "is:reptoken" search. * Changing language now properly updates the UI language and prompts to reload. * Update search filters to include 'is:hasornament' and 'is:ornamented' * Filter autocomplete should now work in increments, and suggest a wider variety of filters. * Filter autocomplete should now work in increments, and suggest a wider variety of filters * Farming mode now uses the same logic as regular item moves to choose your lowest-value item to send to the vault when a bucket is full. Favorite/keep your items and they'll stay put! * Removed the option to move tokens to the vault in farming mode. ## 5.70.0 (2020-02-16) * Removed community reviews and ratings functionality. It may return in the future, but it was broken since Shadowkeep. * Updated Search suggestions to sort "armor" above "armor2.0" * Fixed ghosts not being draggable in the Loadout Optimizer. * Fixed the Infusion tool not showing all possible items. ## 5.69.0 (2020-02-09) ## 5.68.0 (2020-02-02) * `wishlistnotes` autocompletes in the search filters now. ## 5.67.0 (2020-01-26) ## 5.66.0 (2020-01-19) ## 5.65.0 (2020-01-12) * Setting added for DIM to grab wish lists from external source (defaults to voltron.txt). Choose "Clear Wish List" to remove the default. * Avoid a bug where users who logged in via Stadia would get caught in a login loop. If you are having trouble with login, try using a non-Stadia login linked to your Bungie.net account. * Remove "Store" buttons from Finishers, Clan Banners, and Seasonal Artifacts. * Add links to YouTube tutorials for Search and Loadout Optimizer. ## 5.64.0 (2020-01-05) * Integrating with vendorengrams.xyz to find at-level vendor drops. * Wish lists - trash list icon works with ratings turned off. ## 5.63.0 (2019-12-29) ## 5.62.0 (2019-12-22) ## 5.61.1 (2019-12-17) * Auto refresh is disabled while Bungie.net is under heavy load. ## 5.61.0 (2019-12-15) ## 5.60.0 (2019-12-08) * Bulk tagging no longer erroneously removes notes from selected items. ## 5.59.0 (2019-12-01) * Add a link to Seals on the Progress page sidebar. * Shift click mods in Loadout Optimizer will properly add them to locked mods. * Fix a bug where auto-refresh could stop working if you drag an item while inventory is refreshing. * Seasonal Rank now correctly continues past rank 100. * `maxbasestatvalue` now filters by item slot instead of item type (think masks versus helmets). ## 5.58.0 (2019-11-24) * Wish list files now support block notes. * Option to pretend all Armor 2.0 items are masterworked in the Loadout Optimizer. * Selecting an Armor 2.0 mod in Loadout Optimizer will recalculate stats as if that mod were already socketed. * Ignoring stats in Loadout Optimizer re-sorts the loadouts without including the ignored stats in the total. * Loadout Optimizer is faster. ## 5.57.0 (2019-11-17) * Added support for trash list rolls in wish list files - see the documentation for more info. * Added ability to assume armor 2.0 items are masterworked in the loadout builder. * Mods now indicate where they can be obtained from. * Removed the ornament icons setting, as it didn't do anything since Bungie overrides the icon for ornamented items themselves. * Fix some tricky cases where you might not be able to pull items from Postmaster. * Restore hover tooltip on mods on desktop. You can still click to see all possible mods. * Loadout Optimizer allows you to select "Ignore" in the dropdown for each stat - this will not consider that stat in sorting builds. ## 5.56.0 (2019-11-10) * Fixed some bugs that had crept into DIM's logic for moving items aside in order to allow move commands to succeed. Now if your vault is full, DIM will move items to less-frequently-used characters and avoid moving items back onto your active character. The logic for what items to move has been tuned to keep things organized. * Clicking on a mod will bring up a menu that shows all applicable mods for that slot. You can see what each mod will do to stats and how much it costs to apply. * Perk/mod headers and cosmetic mods are now hidden in Compare and Loadout Optimizer. ## 5.55.1 (2019-11-04) * Fix wonky layout and inability to scroll on item popups, item picker, and infuse tool. ## 5.55.0 (2019-11-04) ## 5.54.0 (2019-11-04) ## 5.53.0 (2019-11-04) * New Archive tag, for those items you just can't bring yourself to dismantle. * Character and Vault stats glued to the page header for now. * Catalysts display updated to handle some API changes. * Fewer surprises on page change: Search field now clears itself so you aren't searching Progress page for masterwork energy weapons. * Compare popup now features more options for comparing by similar grouped weapons and armor. * Smarter loadout builder takes your masterworks into account. * Speaking of masterworks, masterwork stat contribution is now clearly highlighted in Item Popup. * There's more: class items now show their masterwork and mod stat contributions. * Armor 2.0 is now correctly considered a random roll. `is:randomroll` * Grid layouts should display with fewer bugs in older versions of Microsoft Edge. * `is:hasmod` shows items with a Year 2 mod attached. `is:modded` shows items with any Armor 2.0 mods selected. ## 5.52.1 (2019-10-28) ## 5.52.0 (2019-10-27) * Wish lists support integrating with DTR's item database URLs. * Stats can be disabled in the Loadout Optimizer. * Added the ability to search for items that have notes. is:hasnotes * Armor 2.0 is now correctly considered a random roll. is:randomroll * New filters for checking stats without mods - basestat: maxbasestatvalue: * New "any" stat, try basestat:any:>20 for things with mobility>20, or discipline>20, or recovery>20, etc * New seasonal mod filters, for finding somewhere to put your Hive Armaments - modslot:opulent * Wishlist features widely updated with clearer labels * Total stat in the item popup now reflects Mod contribution * Armor 2.0 stats now integrated into the character header * Fixes for caching issues and update loops * Bugfixes, like every week ## 5.51.0 (2019-10-20) * Added an option to disable using ornament icons. Even when enabled, ornaments will only show for items where the ornament is specific to that item - not universal ornaments. * Stats affected by Armor 2.0 mods are highlighted in blue. * Improvements to Loadout Optimizer that should help when you have too many stat options. * Made the Loadout Optimizer ignore Armor 2.0 mods when calculating builds. This ensures finding optimal base sets. * Show element and cost on Mods Collection. * Fixed search autocomplete behavior. * Reverse armor groupings when character sort is set to most recent (reversed). * New search `wishlistnotes:` will find wish list items where the underlying wish list item had the given notes on them. * New search filters `maxstatloadout`, which finds a set of items to equip for the maximum total value of a specific stat, and `maxstatvalue` which finds items with the best number for a specific stat. Includes all items with the best number. * New `source:` filters for `vexoffensive` and `seasonpass` * Improved the styling of popup sheets. * DIM uses slightly prettier URLs now. ## 5.50.1 (2019-10-14) * Made it possible to filter to Tier 0 in Loadout Optimizer. * Changed max power indicator to break out artifact power from "natural" power. ## 5.50.0 (2019-10-13) * It's beginning to feel a lot like Year Three. * Loadout Optimizer updated for Shadowkeep. * Max Power Level includes artifact PL. * Add seasonal rank track to milestones * Equipped ornaments now show up in your inventory tiles. Your items, your look. * "dupe" search and item comparison now recognize the sameness of armor, even if one is armor2.0 and the other isn't. Keep in mind that some "dupes" may still require Upgrade Modules to infuse. * "year" and "season" searches now recognize that armor2.0 versions of old armor, still come from old expansions. * "is:onwrongclass" filter for armor being carried by a character who can't equip it. * Ghosts with moon perks are now badged! * Collections > Mods updated for year 3 style mods. * Check your Bright Dust levels from the Vault header. * Multi-interval triumphs now supported. * Stat displays tuned up. Total stat included, stats rearranged. * CSV exports include more stats. * Clarifications for API errors. * Item popup now features armor Energy capacity. * Reviews filtered better. * Emotes, Ghost Projections, Ornaments, and Mods that you own are badged in vendors. * Added Ghost Projections to Collections. * Hide objectives for secret triumphs. * Added a privacy policy (from About DIM). * Fixed engrams having a number under them. * Subclass path and super are highlighted on subclass icons. ## 5.49.1 (2019-10-07) ## 5.49.0 (2019-10-06) * Add a link to your current profile on D2Checklist to view milestones, pursuits, clan, etc. * Fix PC loadouts not transferring over from Blizzard. * Fix Armor 2.0 showing as masterworked. * Fix stats for Armor 2.0. * Fix well rested for Shadowkeep. * Remove XP and level from character tiles. * Add year 3 search terms. ## 5.48.2 (2019-10-02) ## 5.48.1 (2019-10-01) * For ratings, platform selection has been updated for Shadowkeep - check the setting page to update your selection. * Ratings should be more standard across player inventories. * Happy wish list icon moved into the polaroid strip. ## 5.48.0 (2019-09-29) * Our stat calculations are ever so slightly more accurate. * Collections page now includes equipped/owned Weapon and Armor mods. * UI fixes for shifting page content, subclasses, and some labels & alert messages. * Drag and drop on mobile no should longer spawn a context menu. * Emblems now display their selected variations. * Filter by season names (i.e. `season:opulence`) and masterwork type (`masterwork:handling`) ## 5.47.0 (2019-09-22) * New look and display options under TRIUMPHS: reveal "recommended hidden" triumphs, or hide triumphs you've completed * BrayTech link on Progress now links to your current character. * Prevent accounts from overlapping menu on phone landscape mode. * Show the effects of mods on stat bars. * Removed the stats comparison with the currently equipped weapon. Use the Compare tool to compare items. * Dragging and dropping should be smoother. ## 5.46.0 (2019-09-15) * The notification for bulk tagging now has an Undo button, in case you didn't mean to tag everything matching a search. * The postmaster will highlight red when you have only 4 spaces left! * Firefox for Android is now supported. * Fixes for stats that could show >100. * Show all Sword stats! * The "tag:none" search works again. * The header won't scroll on very narrow screens. * The action bar is pinned to the bottom of the screen on mobile. ## 5.45.0 (2019-09-08) * Milestones are more compact, like Pursuits (click them to see full details). They now show expiration times and clan engrams are broken out into individual items. * The item popup for Pursuits will refresh automatically as you play, if you leave one open (this doesn't yet work for Milestones). * Expiration times only light up red when they have less than an hour left. * Added a new is:powerfulreward search that searches for powerful rewards. * Fixed a bug moving certain items like emblems. * Added a quick-jump sidebar to the settings page. * Add win streak info to ranks on the Progress page. * Include the effect of mods and perks on "hidden" stats like zoom, aim assistance, and recoil direction. * Bonuses from perks and mods shown in their tooltips are now more accurate. * Loadout Optimizer understands multiple kinds of perks/mods that can enhance an item. * Recoil Direction's value has been moved next to the pie. * Searches now ignore accented characters in item names. * Unique stacked items now show the count, instead of just MAX, when they're full. ## 5.44.2 (2019-09-02) * Fix Home Screen app warning for iPad. ## 5.44.1 (2019-09-02) * Added upgrade warning for old iOS versions that don't support Home Screen apps. ## 5.44.0 (2019-09-01) * Allow loadouts to be equipped without dismissing farming mode. * Restore info to D1 ghosts. * Add hotkeys to navigate between major pages (hit "?" to see them all) * Fix move popup not updating amount on stackables when switching items. * Remove Solstice of Heroes armor from Progress page. * Prevent accidentally being able to tag non-taggable items with hotkeys. ## 5.43.1 (2019-08-26) * Fix broken ammo icons. ## 5.43.0 (2019-08-25) ## 5.42.2 (2019-08-22) * Fix D1 accounts disappearing when they were folded into a different platform for D2 cross save. ## 5.42.1 (2019-08-20) * Changes to support preserving tags/notes data for Blizzard users who migrate to Steam. * Fix searching Collections. ## 5.42.0 (2019-08-18) * Power is yellow again. * Remove ugly blur behind popups Windows. (It's still a nice blur on other platforms) ## 5.41.1 (2019-08-16) * Fix overflowing text on ghosts. * Fix crash related to wish lists. ## 5.41.0 (2019-08-11) * Wish lists now support (optional) title and description. * New header design. Your accounts are now in the menu. * Ghosts have labels explaining where they are useful. * Recoil direction stat is shown as a semicircular range of where shots may travel. * Search boxes on item picker sheets now autofocus. * Item counts will properly update when moving partial stacks of stacked items. * Fix a case where the search autocompleter could hang around. ## 5.40.0 (2019-08-04) * Fixed auto-scrolling links in Safari. * Added the ability to lock items from the Compare tool. * Add Solstice of Heroes to the Progress page. * Show Special Orders under the Postmaster. * Add a splash screen for the iOS app. You may have to delete the icon and re-add it. ## 5.39.0 (2019-07-28) * Enabled PWA mode for "Add to Homescreen" in iOS Safari (Requires iOS 12.2 or later). If you already have it on your home screen, delete and re-add it. * Show the amount of materials you have that Spider is selling for exchange on his vendor page. * Updates to support Cross Save. The account menu now shows icons instead of text, and can support accounts that are linked to more than one platform. * Fixed valor resets not showing correctly. ## 5.38.0 (2019-07-21) * Add source:calus to highlight weapons which give "Calus-themed armor and weapons" credit in activities. * Moved search help to an in-screen popup instead of a separate page. * Added rank resets for the current season to ranks display. * You can now swipe between characters anywhere in the page on the Progress and Vendors pages. * Properly invert stat filters when they are prefixed with -. ## 5.37.1 (2019-07-16) * Don't show the "not supported" banner for MS Edge. ## 5.37.0 (2019-07-14) * Updated progress page pursuits to match in-game styling. * Updated our shop link to point to our new store with DIM logo clothing and mugs. * The Weekly Clan Engrams milestone will hide when all rewards have been redeemed. * Moved raids below quests. * Pursuits in the progress page now show exact progress numbers if the pursuit only has a single progress bar. * Show tracked Triumph. * Mark a wider variety of Chrome-based browsers as supported. * Added Seals and Badges to Triumphs/Collections. ## 5.36.2 (2019-07-11) * Fixed a crash viewing Bad Juju. * Text search now also searches notes. * Added new name: and description: searches. * Subclasses no longer look masterworked. ## 5.36.1 (2019-07-09) * Fixed the app on Microsoft Edge. * Fixed an issue where iOS could see the "Update DIM" message over and over without updating. ## 5.36.0 (2019-07-07) * Added raid info to the Progress page. * Sort bounties and quests with expired at the end, tracked at the beginning. * Use weapon icons in objective strings instead of text. * Added perkname: search. * Charge Time and Draw Time now compare correctly! * Fixed: Classified items required some finesse. * Updated is:modded to take into account for activity mods. * Re-added is:curated as a filter for Bungie curated rolls. * Bounty expiration timers are more compact. ## 5.35.0 (2019-06-30) * Removed is:curated as an alias for is:wishlist. ## 5.34.0 (2019-06-23) ## 5.33.3 (2019-06-22) * Fixed failing to show progress bar for bounty steps. * Removed inline Item Objectives from the Progress page. ## 5.33.2 (2019-06-21) * Fixed failing to show progress bar for bounty steps. ## 5.33.1 (2019-06-20) * Fixed issue with item cards and farming mode were under the St Jude overlay. ## 5.33.0 (2019-06-16) * The Progress page sports a new layout to help make sense of all the Pursuits we have to juggle. This is the first iteration of the new page - many improvements are still on their way! * Fixed a bug where weapon mods were causing Banshee-44 wish list items to fail to highlight. * Fixed a bug with expert mode wish lists and dealing with single digit item categories. * CSV exports now include item sources. These match the DIM filter you can use to find the item. * Include more items in the "filter to uncollected" search in Vendors. * Added shader icons to the item details popup. ## 5.32.0 (2019-06-09) * Fixed a crash when expanding catalysts under the progress tab. ## 5.31.0 (2019-06-02) * Fix too-large item icons on mobile view in 3 column mode. * Allow inventory to refresh in the Loadout Optimizer. * Fix equipping loadouts directly from the Loadout Optimizer. * Add icons to selected perks in Loadout Optimizer. ## 5.30.2 (2019-05-31) * Add St. Jude donation banner. ## 5.30.1 (2019-05-27) * Tweaked contrast on search bar. * Added the ability to select multiple perks from the perk picker in Loadout Optimizer before closing the sheet. On desktop, the "Enter" key will accept your selection. ## 5.30.0 (2019-05-26) * Brand new Loadout Optimizer with tons of improvements and fixes. * Redesigned search bar. * Updated DIM logos. * Added Escape hotkey to close open item details dialog. ## 5.29.0 (2019-05-19) * Items with notes now have a note icon on them. * Fixed a bug where the hotkeys for tagging items broke if you clicked directly to another item. * Removed a stray curly brace character from the item reviews on the item popup. ## 5.28.0 (2019-05-12) ## 5.27.0 (2019-05-05) * Added a link to the About page to see the history of all actions made by DIM or other Destiny apps. * The navigation menu better respects iPhone X screens. * Stat values are now shown in the tooltip for perks. They might not be totally accurate... * Added a hotkey (m) for toggling the menu. ## 5.26.0 (2019-04-28) * Restored missing collectibles. ## 5.25.0 (2019-04-21) * A redesigned Vendors page is easier to navigate, and includes a feature to show only those items you are missing from your collections. Searching on the vendors page also now searches the vendor names, and hides items that don't match the search. * Loadout Optimizer on mobile lets you swipe between characters instead of wasting space showing all three at once. * Xur has been removed from the Progress page. * Reputation materials for a vendor's faction are now included in the Vendor page. * Fixed a bug where DIM would cache a lot of data that wasn't needed. ## 5.24.0 (2019-04-14) * Progress page changes to utilize more screen real-estate. ## 5.23.2 (2019-04-09) * Fix Edge issues. ## 5.23.1 (2019-04-08) * Fixed some crashes. ## 5.23.0 (2019-04-07) * Loaded Wish Lists now persist between reloads, and will highlight new items as you get them. Use Wish Lists from expert players to find great items! * Fix an issue where pulling consumables from the postmaster on characters other than the current one could lock up the browser. * The compare tool's Archetypes feature will now use the intrinsic perk of the item rather than solely relying on the RPM. * Item sort presets have been removed - you can choose your own sorting preferences by dragging and dropping sorting properties. * Fixed reloading the page while on the Vendors tab. * Fix search for blast radius (it was accidentally mapped to velocity). * The Loadout Optimizer's perk search now updates when you change characters. * Removed the option to pull from the postmaster into the vault when an item can't be pulled from postmaster at all. * Removed the (broken) option to split a stack by hovering over the drop target. ## 5.22.0 (2019-04-01) * Fix item ratings. * Fix missing loadouts on PC. ## 5.21.0 (2019-03-31) * You can now swipe between pages on the item popup. * Fixed a bug where reviews failing to load would result in an infinite refresh spinner. * Actually fixed the bug where Pull from Postmaster with full modulus reports would move all your other consumables to the vault. * Ratings and reviews are now cached on your device for 24 hours, so they should load much faster after the first time. * The ratings tab has a cleaned up design. * All of the stat filters now show up in search autocomplete and the search help page. * You can now move items from the postmaster directly to the vault or other characters. * When adding all equipped items to a loadout, the class type for the loadout will be set to the class that can use the armor that's equipped. * Fixed a rare bug where you could move an item while DIM was refreshing, and the item would pop back to its original location until the next refresh. * Errors in the Loadout Optimizer now show on the page, instead of just freezing progress. * Fixed the "Loadout Optimizer" button on the new Loadout editor. * If you try to move an item in DIM that you've equipped in game but DIM doesn't know about, it'll now try to de-equip it to make it move, instead of throwing an error. ## 5.20.2 (2019-03-27) * Fixed Pull from Postmaster. ## 5.20.1 (2019-03-26) * Fixed: Pull from Postmaster better handling of unique stacks. * The vendors page now highlights items that you have already unlocked in Collections. * Don't try to move all your consumables to the vault if you add one to your loadout and check the "Move other items away" option. ## 5.20.0 (2019-03-24) * Items in the postmaster now count towards your max possible light. * DIM now correctly calculates how much space you have free for items that can't have multiple stacks (like Modulus Reports). This makes pulling from postmaster more reliable. * The loadout creator/editor has been redesigned to be easier to use. Select items directly from inside the loadout editor, with search. You can still click items in the inventory to add them as well. * Loadouts can now use an option to move all the items that are not in the loadout to the vault when applying the loadout. * Made it clearer when inventory and item popups are collapsed. * The Loadout Optimizer is out of beta! Use it to automatically calculate loadouts that include certain perks or hit your targets for specific stats. ## 5.19.0 (2019-03-17) * Fixed: Export mobility value correctly in CSV export. ## 5.18.0 (2019-03-10) * Added: is:revelry search. * Added: source:gambitprime search. * Fixed engrams wrapping to a second row on mobile in 3-column mode. ## 5.17.0 (2019-03-03) * Add stat:handling as a synonym for stat:equipspeed, to match the name shown in displays. * Remove Exotic Ornaments from Loadout Builder * Fixed: 'NaN' could appear in Item Popup in certain situations. ## 5.16.0 (2019-02-24) ## 5.15.0 (2019-02-17) * Remember the last direction the infusion fuel finder was left in. * Remember the last option (equip or store) the "pull item" tool was left in. * Updated notification style. You can still click the notification to dismiss it. * Search filter will now show button to add matching filtered items to compare (if they're comparable) ## 5.14.0 (2019-02-10) ## 5.13.0 (2019-02-03) * Fixed search queries that include the word "and". * Updated inventory style to reduce the visual impact of category headers. * Added is:reacquirable to show items that can potentially be pulled from your Collection * Redesigned infusion fuel finder to work better on mobile, and support search filtering. ## 5.12.0 (2019-01-27) ## 5.11.0 (2019-01-20) ## 5.10.0 (2019-01-13) * Move Random Loadout into the Loadout menu and add a "Random Weapons Only" option. * Restyle the alternate options in the loadout menu. * Removed the quick consolidate buttons and engram counter from D1 farming mode. * Remove the setting to "Show full item details in the item popup". DIM now just remembers the last state of the popup, and you can expand/collapse with the arrow in the top right corner of the popup. * Fix showing which perks are highly rated by the community. * Fix for getting stuck on the reviews tab when clicking on items that can't be reviewed. * Fix highlighting of subclass perks. * Add source:blackarmory & source:scourge. * Fix CSV to always include the masterwork column. * Add id: and hash: searches. * Improve the performance of the notes field and fix a bug where sometimes a note from another item would show up. * Fix some cases where the manifest wouldn't load. * Fix crash when searching is:inloadout with no loadouts. ## 5.9.0 (2019-01-06) * Click the plus icon under an equipped item to search for and transfer items in that slot from anywhere in your inventory. * Import a CSV file of items with tags and notes to bulk update the tags/notes for all of those items. * CSV - Wrap ID in quotes such that its value is not rounded. ## 5.8.3 (2019-01-02) * More fixes to popup swiping on Android. * Fix perk searching in non-latin languages. * Added a key for the ratings symbols. ## 5.8.2 (2019-01-01) * Make it easier to swipe away the item popup on Android. ## 5.8.1 (2018-12-31) * Fix a bug where some Android phones couldn't see weapon details. * Fix a bug where the wrong item's details would show up in the item popup. * Show "Make Room for Postmaster" if there's anything in the postmaster, not just if there's pullable items. ## 5.8.0 (2018-12-30) * Add the option to sort inventory by tag in custom sort options. * No longer showing community ratings for ornaments/catalysts. * Fixed a long-standing bug where you couldn't transfer some stacks to a full inventory. * Item popup is nicer on mobile. * Wider item popups on desktop. * Larger buttons for transfers. * Wish lists allow you to create and import lists of items or perks that will be highlighted in your inventory. * Dropped support for iOS 10. * Prevent the vault from getting really narrow, at the expense of some scrolling. * Armor in the vault is now organized by class, in the same order as your characters. * Disabled pull-to-reload on Android. * Improved treatment of expert mode wish list items. * Fixed perk searches to keep the whole search term together, so "machine gun reserves" won't match "machine gun scavenger" anymore. ## 5.7.0 (2018-12-23) * Show kill trackers for items with in-progress masterwork catalysts. * You can specify item categories to be specific about your expert wish list items. * Hide ratings on items with fewer than 3 reviews. * Fix some DIM functionality in the Edge browser. ## 5.6.0 (2018-12-17) * Updated Crucible and Gambit ranks to reflect new multi-stage ranks. * DIM loads faster and uses less memory. * Ratings are now displayed on item tiles as an icon indicating whether they are perfect rolls (star), good (arrow up), neutral (dash), or bad (arrow down). The exact rating is still available in the item popup. * The mobile view now defaults to 4 items across (5 including equipped), which fits more on the screen at once. You can still choose other display options in Settings. * Masterwork info is now included in the CSV exports. * Added season info for Dawning items. * Include non-selected perk options while searching perks. * Load the new Simplified Chinese Destiny database when that language is selected. * Show a warning when perks/mods are missing because of a Bungie.net deployment. ## 5.5.2 (2018-12-10) * Changed search behavior of perk:. It now tries to match the start of all words. * Added "expert mode" for more complex wish list expressions. * Allow selecting text on the progress page. * Some redacted items now have a picture and some description, pulled from their collection record. ## 5.5.1 (2018-12-09) * Fixed display of stackables badges in D1. ## 5.5.0 (2018-12-09) * New items, when enabled, now show a red dot instead of an animated shine. * Fixed center column emblem color on Safari. * Loadout and compare popups now use a draggable "Sheet" UI. ## 5.4.0 (2018-12-02) * Moved is:yearX and is:seasonX searches to year:# and season:#. * Fixed a bug where Inventory would not appear on mobile for non-current characters. * On mobile, the search box is now full-width. * Unopened engrams are shown in a small row similar to how they appear in-game, instead of looking like they are in the postmaster. * Engrams no longer appear to be pullable from the postmaster. * Shaders are now sorted by whats defined in the settings. * Fixed the display of tag dropdowns. * Support simplified Chinese (for DIM text only - Destiny items are still in Traditional). * New loading animation. * New look for the Vault tile. * Light cap raised to 650 for Season of the Forge. ## 5.3.2 (2018-11-27) * Fix crash on Progress page caused by redacted Triumphs. * Fix URL not updating while navigating. * Fix display of faction levels. * Fix The Drifter showing an error because of a redacted item. * Fix a case where the Google Drive data file would not be created. * Prevent moving partial stacks of Ghost Fragments, because that doesn't work. * Fix display of vendor checkmark. * Fix horizontal scrolling slop on the mobile header. ## 5.3.1 (2018-11-26) * Fix some settings that weren't quite working right. ## 5.3.0 (2018-11-25) * Remove the ability to set a specific vault width. Vault always takes all remaining space. * Inventory columns are shaded to match the equipped emblem. * DIM has been darkened to provide better contrast with the items. * Fit and finish changes to the new tiles and inventory display. * Add id and hash column to exported csv for ghosts, armor, and weapons. * Add event and season column to exported csv for Destiny 2. * D2 subclasses now show which path, grenade, etc. are chosen. ## 5.2.1 (2018-11-20) * Fix comparing masterworks ## 5.2.0 (2018-11-20) * New item tiles that show more information and don't hide the picture. * Updated storage settings to show Google Drive usage and signed in user. * New D1 Vendors page that resembles the D2 Vendors page. ## 5.1.0 (2018-11-18) * Fix display of exotic catalysts in the item popup. * Restore kill tracker for all items. * Loadouts now sort by type then name. * Global loadouts are now indicated by a globe icon in the LoadoutPopup. * Loadouts of the same type can no longer have a clashing name. * Add count: filters to search for items you have a certain number (or more or less) of. i.e. count:>3 to find all your Edge Transits. * Improve display of your Ranks. * Show progress towards completing cache keys. * Work around a memory leak bug in MS Edge. * Update titles on item popups to display closer to what's in game. * Added community curations (a way to look for god rolls). ## v4 CHANGELOG * v4 CHANGELOG available [here](/docs/OLD_CHANGELOG/OLD_CHANGELOG_4.X.X.md) ================================================ FILE: docs/OLD_CHANGELOG/OLD_CHANGELOG_6.X.X.md ================================================ ## v7 CHANGELOG * v7 CHANGELOG available [here](/docs/CHANGELOG.md) ## 6.99.1 (2022-01-10) * The Loadouts page is a bit cleaner and more compact in the mobile view. * You can once again click within the Compare sheet to close an item popup. * Loadouts don't fail when they contain an ornament that can't be slotted into the current armor. * Sharing loadout builds includes fashion. ## 6.99.0 (2022-01-09) * You can now add Fashion (shaders and ornaments) to loadouts. Creating a loadout from equipped automatically adds in the current shaders and ornaments, and you can edit them manually from the Loadouts editor. * Fixed an issue where the progress bar for exotic weapons were clipping into other page elements. * Fixed an issue that could make moving searches containing stacks of items to fail. * Added Spoils of Conquest to the currencies hover menu. * Fixed an issue where the Loadout Optimizer would let you pin items from other classes. * Fixed an issue where the universal ornament picker would show too many ornaments as unlocked. * Shader picker now hides unavailable, unobtainable shaders. * "Preview Artifact Contents" in artifact popup now shows unlocked mods from the perspective of the owning character, not the current character. * Creating a loadout now defaults the class to whichever character you had selected. ## 6.98.0 (2022-01-02) * Consumables can now be pulled from postmasters other than the active character's. * Vendors page now correctly recognizes owned bounties per character and is more accurate about mods ownership. * Fixed an issue that could make moving searches containing stacks of items to fail. * Fixes for display on iPhones with rounded corners and a notch. * Transmog Ornaments menu now correctly shows whether ornament has been unlocked or not. * Happy New Year ## 6.97.3 (2021-12-30) * Fixed an issue for some users where Stasis Aspects were not shown when selecting Stasis subclass options. * This works around a Bungie API issue, and will allow users to select and try equipping Stasis Aspects they have not unlocked, which may result in failures applying loadouts, if the aspects are not unlocked. ## 6.97.2 (2021-12-30) * Improved the press-to-view tooltips on mobile. It should now be much easier to select perks on mobile. * Removed notification when loading/updating a wish list. Go to the wish list section of the settings menu if you want to see details. * The progress notification for applying a loadout now shows each item and mod as it's being applied. * Mod picker now correctly names inserted thing (e.g Fragment, Shader). * Dares of Eternity now shows streak information under Progress > Ranks * Ignore outdated/removed artifact mods still attached to armor. * You can now select Stasis subclasses in the Loadout Optimizer and use the stat effects from fragments. * When determining mod assignments, DIM will now consider in game mod placement and attempt to use the same position if possible. * Tracked Triumphs are now grouped together with similar records. * Starting the Compare view from a single armor piece now includes other elements in initial comparison. * Inventory items can now be sorted by Element. * Prevent plugging some invalid ornaments. ## 6.97.1 (2021-12-26) * Transmog Ornaments menu once again *incorrectly* shows whether an ornament has been unlocked or not, but fixed a bug where an artifact mod, once slotted on your active gear, would show up as not unlocked. ## 6.97.0 (2021-12-26) * Transmog Ornaments menu now correctly shows whether ornament has been unlocked or not. * The stat bars shown in Compare are more accurately sized, relative to each other. * Fix an issue where mods might get plugged in too fast and bump into the armor's max Energy. * Update some error messages for when equipping items fails. * Added new filter `is:stackfull` to show items that are at max stack size. * Searching by perk now works on languages that use accented characters. * Tooltips for Mods, Fragments, Aspects, etc. now show information about their type. * Fix Subclasses sometimes showing a progress badge on their icon. * Fix cases where an item might inappropriately show with a wishlist thumbs-up. * Loadouts/Loadout Optimizer * When re-Optimizing a loadout, the Loadout Optimizer's Compare button will now initially select the original loadout from the loadout page. * If you open an existing loadout in Loadout Optimizer and the loadout has an exotic equipped, that exotic will be pre-selected in the LO settings. * Armor set stats in Loadout Optimizer or Loadout Details will now show stat tiers exceeding T10 or going below T0. * Radiant Light and Powerful Friends' activation conditions will now be accounted for when showing mod placements and applying mods to a loadout. If possible they will be assigned to an item so that their conditional perks are active. * The option to view Mod Assignments for a loadout is now available outside of Beta. * Loadout notes now retain whitespace formatting. * On the Loadouts page, missing items show up dimmed-out instead of not at all. * The Loadouts page can be filtered from the search bar in the header. * Selecting toggleable subclass abilities like jumps and grenades now works more smoothly. * Fixed an error when applying mod loadouts to armor too old to have mod energy. ### Beta Only * We're trying out a new tool for the desktop inventory screen called "Item Feed". Click the tab on the right to pop out a feed of your item drops with quick buttons to tag them. By default tagged items disappear from the view so you can focus on new stuff. ## 6.96.0 (2021-12-19) * Loadouts now show correct stats for subclass, item, and mod selections. All mods are accounted for whether they actually will fit or not. * Equipping a generated loadout in the Loadout Optimizer will now apply the selected mods. * Improved the time taken to apply a loadout with mods. * Stasis subclass can also be applied in the Loadouts page. * Fixed showing the amount of Dawning Spirit in the holiday oven popup. * Add energy bar displays to the Mod Assignments view. * Fixed the 5th slot on Artifice Armor not showing up in Loadout Optimizer if no mod was plugged in. ## 6.95.1 (2021-12-14) * Fixed issue where selecting mods from the Mod Picker, opened from an item socket, would clear other mod selections. * Added the ability to favorite finishers ## 6.95.0 (2021-12-12) * Fix image paths for D1 perks. * Strange Favor rank now correctly shows remaining reset count, when you hover the icon. * Ability Cooldown times are no longer shown for stat tooltips. This may return but at the moment, they were incorrect. * Added 'source:30th' for items coming from the Bungie 30th Anniversary. ### Loadouts * Stasis subclass abilities, aspects, and fragments are now displayed in the loadouts page. * When displaying mod placement in Loadouts, if an armor slot has no item in the loadout, the character's current armor piece will be used. * Removed the "Max Power" loadout from the loadouts page. You can still apply it from the loadout menu on the inventory screen. * If loadouts have notes, those notes are now displayed in the hover text on the loadout dropdown * Artifice Armor mod slots are now handled in Loadouts and the Loadout Optimizer. * Hitting +Equipped in the loadout editor will add current mods. * Creating a new loadout from equipped items will also save your subclass configuration. * Creating a new loadout from equipped, or hitting +Equipped in the loadout editor, will now also include your current emblem, ship, and sparrow. * Added more visual distinction between loadouts on the loadouts page. * Some repeat text and unnecessary instructions were removed from mods and Stasis Fragments, in the mod picker. ### Loadout Optimizer * Fix an error that can occur when loading a shared build link. * Fix issue where Optimizer throws an error when selecting a raid or combat mod. * Fix an issue where energy swaps in the Optimizer where not displaying the correct resulting energy. ### Mod Plugging Capabilities * Bungie has enabled the API capabilities to apply armor mods, toggle weapon perks, and perform other plugging-in type operations. So now you can take advantage of these features of DIM! * This works from the item popup, and when applying loadouts that contain mods. * DIM can apply weapon perks, armor mods, shaders, weapon & exotic ornaments, and Stasis Aspects and Fragments. * It cannot apply weapon mods, which still cost glimmer in the game. * It can't yet apply transmog/Synthesis ornaments, but Bungie is working on addressing this. * Swapping Stasis aspects & fragments via loadouts is coming soon. ### Mods in Loadouts * When you apply a loadout with armor mods, DIM will automatically assign these among armor pieces. * If there's no armor in the loadout, it will apply these mods to your character's current pieces. * More specific/custom placement options are in the works. * DIM will not clear off existing mods except to make room for requested ones. ### Plugging-Related Fixes * Fix a bug that prevented applying shaders or ornaments from the item popup. * Fix emblems and subclasses not applying from loadouts. * The mod picker launched from the item popup or Loadout Optimizer will now correctly show the mods unlocked by the applicable character, rather than across all characters. This helps a lot with artifact mods where you may have different ones unlocked on different characters. Note that this also means opening the mod picker for items in the vault will show no artifact mods unlocked - move the item to a character if you want to apply that mod. * Vendor items no longer offer to apply perks. ## 6.94.0 (2021-12-05) * You can change perks and slot zero-cost mods from the item Popup. * Loadouts can now apply mods. See [Mods in Loadouts](https://guide.dim.gg/Mods-in-Loadouts) for details. Some things to keep in mind: * This will not work until the 30th Anniversary patch. * Applying mods will also strip off any mods that aren't in your loadout from your equipped armor. * Mods will be placed on your equipped armor whether that armor came from your loadout or not. * Loadouts can now have notes. * Share loadout build settings (mods, notes, loadout optimizer settings) from the Loadouts page. * Loadouts can now save stasis subclass abilities, aspects, and fragments. These do not yet get applied when applying a loadout. * We made several bugfixes to how loadouts are applied that should fix some issues where not all items got equipped or failures were shown when nothing failed. * The "Create Loadout" button on the Loadouts page defaults the loadout to match the class of the selected character. * The menu for pinning or excluding an item in Loadout Optimizer now only shows items that match the overall search filter. * Stat searches support keywords like "highest" and "secondhighest" in stat total/mean expressions. e.g. basestat:highest&secondhighest:>=17.5 ## 6.93.0 (2021-11-28) * Steam browser is officially unsupported, and we now show a banner explaining that. * However, we have managed to fix DIM so it doesn't crash loop in the Steam overlay. Until the next time Steam updates... * Loadout Optimizer performance has been improved significantly - so much so that we now always look at all possible combinations of armor. Previously we trimmed some items out to get below an a number that we could process in time. This means that your LO builds are now guaranteed to be optimal, and the "Max" range shown in the stat tiles will always be accurate. * We no longer cap stats in the Loadout Optimizer's tooltips so you can see how far over 100 a stat goes. * Fixed a bug where Loadout Optimizer would only show one set. * Cryptolith Lure and Firewall Data Fragment have been moved from "Quests" to "Quest Items". * We've launched a new Loadouts page that makes it easy to browse through your loadouts. The Loadout Optimizer is accessible from that page. Also, loadouts are now by default sorted by when they were last edited, rather than their name. You can change this on the Loadouts page or in settings. * Some perks in the Armory view that showed as not rolling on the current version of an item now correctly show that they can roll. ## 6.92.1 (2021-11-23) * Fixed "Optimize Armor" button (formerly "Open in Loadout Optimizer") in the loadout drawer. ## 6.92.0 (2021-11-21) * Show bars next to armor stats in Compare. * At long last, found and fixed a bug that could lead to tags and notes getting wiped if you switched accounts while another account's data was loading. Many apologies to anyone who lost their tags and notes from this bug, and we hope it's gone for good. * Remove bright engram rewards from prestige season pass rewards as these were guesses and not quite right. ### Beta Only * We're testing out a new Loadouts page that makes it easy to browse through your loadouts. The Loadout Optimizer is accessible from that page. Also, loadouts are now by default sorted by when they were last edited, rather than their name. You can change this on the Loadouts page or in settings. Let us know what you think, and how it can be made more useful! ## 6.91.2 (2021-11-16) * Put back the full item tile in Compare. ## 6.91.1 (2021-11-16) * Fix issue in Loadout Optimizer where only one set would show when using Safari or iOS apps. ## 6.91.0 (2021-11-14) * The link to D2Gunsmith from the Armory view is now shown on mobile. * Currency counts won't get squished anymore * Simplified item tiles in the Compare view since a lot of the tile info was redundant. ## 6.90.1 (2021-11-08) * Mod costs now show in Firefox. * Fixed search transfer not moving items that aren't equippable on the selected character. ## 6.90.0 (2021-11-07) * If a loadout has items for multiple character classes in it, applying it to a character behaves as if only the items that can be equipped on that character are in the loadout. * Fixed an issue where the Loadout Optimizer would allow masterworked items to have their energy changed when using the Ascendant Shard (not exotic) armor upgrade option. * Fixed an issue where clicking a mod icon in the Loadout Optimizer would select more than one of the mod. ## 6.89.0 (2021-10-31) ## 6.88.1 (2021-10-28) * `modslot:activity` now identifies Armor 2.0 items that have a modslot related to an activity (currently, a raid or a Nightmare mod slot). * Fix an issue where an invalid query is passed to the Loadout Optimizer when you click a mod socket. ### Beta Only * Loadouts can now show you an assignment strategy for mods. It optimizes for the least number of unassigned mods. ## 6.88.0 (2021-10-24) * DIM will now display Shaders if they were leftover in your Vault after the transmog conversion. * The item popup has a toggle to choose between list-style perks (easier to read!) and grid-style perks (matches in game). No, we will not add an option to change the order of the list-style perks. * List-style perks in the item popup have a hover tooltip on desktop so you don't have to click them if you don't want to. * The item popup has a button to select all the wishlisted perks if they aren't already the active perks, so you can preview the wishlisted version of the item quickly. * Added a "is:statlower" search that shows armor that has strictly worse stats than another piece of armor of the same type. This does not take into account special mod slots, element, or masterworked-ness. "is:customstatlower" is the same thing but only pays attention to the stats in each class' custom total stat. * Stat bars now correctly subtract the value of mods from the base segment. ## 6.87.0 (2021-10-17) * Moved "Tracked Triumphs" section to the top of the Progress page. * You can now track and untrack Seasonal Challenges from the Progress page. * Loadout Optimizer now correctly handles nightmare mods. * Loadout Optimizer makes a better attempt at assigning mods to compared loadouts. * Added `is:infusionfodder` search to show items where a lower-power version of the same item exists. Use `tag:junk is:infusionfodder` to check your trash for its potential to infuse! * Loadout Optimizer will warn you if you try to load a build that's for a character class you don't have. * If your D1 account had disappeared from DIM, it's back now. * Aeon exotic armor pieces now show mod slots again. * In Loadout Optimizer, the Select Exotic menu now lets you select "No Exotic" and "Any Exotic". "No Exotic" is the same as searching "not:exotic" before, and "Any Exotic" makes sure each set has an exotic, but doesn't care which one. ## 6.86.0 (2021-10-10) * Clicking a perk in the item popup now previews the stat changes from switching to that perk. * Clicking a perk in the Organizer view also previews the stats for that perk. * Changes to the Armory view (bring up Armory by clicking an item's name in the item popup): * Armory highlights which perks cannot roll on new copies of the weapon. * Armory highlights the perks rolled on the item you clicked. * Clicking other perk option previews their stat effects. * You can click the "DIM" link to open the item info on its own, and share a roll with others. * Clicking modslots lets you change mods. * Selecting different ornaments shows what the ornament looks like on the item. * Added a link to D2 Gunsmith for weapons. * Inventory screen can now be sorted by whether an item is masterworked. Check [Settings](/settings) to view and rearrange your sort strategy. * Loadout Optimizer shows an estimate of how long it'll take to complete finding sets. * DIM shouldn't bounce you to your D1 account when Bungie.net is having issues anymore. * `is:maxpower` search now shows all the items at maximum power, instead of just the items that are part of your maximum power loadout. The previous meaning has been moved to `is:maxpowerloadout`. Keep in mind that because of exotics, you may not be able to equip all your max power items at once. ### Beta Only * Loadout Optimizer now shows the maximum stat tier you can get for each stat, taking into account all of your loadout settings including min/max stats, mods, and search filter. We're still not sure of the best way to display this, so it's in Beta only for now to get some feedback. * We've tweaked the way Loadout Optimizer chooses which subset of items to look at when you have too many items to process. We should be better at making use of items that have "spiky" stats. ## 6.85.0 (2021-10-03) * Postmaster and Engrams should be sorted exactly like in game now. * Loadout Optimizer no longer saves stat min/max settings as the default for the next time you use it. Opening an existing loadout in the Optimizer will still reload the min/max settings for that loadout. * We won't automatically refresh your inventory when you're on the Loadout Optimizer screen anymore - click the refresh button or hit R to recalculate sets with your latest items. * The "Perks, Mods & Shaders" column in Organizer no longer shows the Kill Tracker socket. * The Recoil Direction stat now sorts and highlights differently in both Compare and Organizer - the best recoil is now straight up, and recoil that goes side to side is worse. * Farming mode can now be configured in settings to clear a preferred number of slots (1-9) ## 6.84.0 (2021-09-26) * Items in the Compare view no longer move around according to the character they're on. * Fixed an issue where the Loadout Optimizer would not load due to deprecated settings. * Hovering over stat tiers in the Loadout Optimizer's compare drawer now shows stat tier effects for the new set too. ## 6.83.0 (2021-09-19) * Still adjusting to Stasis... `is:kineticslot` now identifies items which are in the "Kinetic" slot (the top weapon slot) but aren't Kinetic type damage. * Loadout Optimizer finds better mod assignments. * Engram power level is now also shown on hover. * Clicking on the title of an item now brings up a new item detail page which shows all possible perks and wishlist rolls. * Note that D1 items no longer have a link at all. We're not adding D1 features anymore. * Random-roll items in Collections now show all the perk possibilities they could roll with. * Armor in Collections now shows what mod slots it has. * Fixed vendor items showing some incorrect wishlist matches. ### Beta Only * Removed the press-and-hold mobile item menu, which saw very limited use. This will also be removed in the release version after some time. * Removed the "Active Mode" experiment - its ideas will come back in the future in other forms, but for now it doesn't offer enough above the Progress page (which can be opened in another tab/window next to Inventory if you want to see both). ## 6.82.0 (2021-09-12) * Loadout Optimizer remembers stats you've Ignored between sessions. * Opening a saved loadout in Loadout Optimizer restores all the mods and other settings from when it was originally created. * Share your Loadout Optimizer build - the new share button copies a link to all your build settings. Share great mod combos with other DIM users! * Fixed issue in Loadout Optimizer where locking energy type didn't work for slot specific mods. * Clicking on an item's picture in the Compare tool now opens the full item popup. * Added a "pull" button (down-arrow) to each item in the Compare tool that will pull the item to your current character. * Collapsed the Tag menu into an icon in Compare to allow more items to fit on screen. * Shortened the names of stats in Compare to allow more items to fit on screen. * Added hover titles to the new compare buttons for more clarity. * Selecting "Add Unequipped" in the loadout editor no longer tries to equip all your unequipped items. * Progress win streak will now correctly display when a user hits a 5 win streak. * Fixed broken description for some new triumphs. * Loadout Optimizer's exotic picker now consistently orders slots. * Loadout Optimizer's stat filters no longer attempt to automatically limit to possible ranges. * Added numerical faction ranks alongside rank names on the Progress page. * Fixed the order of items in vendors and seasonal vendor upgrade grids. * Seasonal artifact display now matches the games display. * Ritual rank progress for vendors now matches the ritual rank circle shape. * Fixed vendor ranks being off by 1. * Accounts list shows your Bungie Name. * Add a tip for how to scroll Compare on iOS. ## 6.81.0 (2021-09-05) * Fixed wonky rank display in the phone portrait layout. * Elemental Capacitor stats are no longer added to weapons with the perk enabled. * In the Loadout Optimizer, searching items now works in conjunction with locking exotics and items. * Added `is:currentclass` filter, which selects items currently equippable on the logged in guardian. * Fixed armor swaps away from Stasis in Loadout Optimizer. * Added a warning indicator to previously created loadouts that are now missing items. ## 6.80.0 (2021-08-29) * Fix sorting by power and energy in Compare when "Show Base Stats" is enabled. * Fixed misalignment in stat rows, and vertical scrolling, in Compare. * Highlighting stats in Compare is faster. * You can click any perk in Compare, not just the first couple. * Clicking an item's name to find it in the inventory view will now change character on mobile to wherever the item is. * In Compare for D1, fixed an issue where you could only see the first 2 perk options. * Mods can be saved and viewed in Loadouts - this is automatic for loadouts created by Loadout Optimizer but you can edit the mods directly in the loadout editor. * Search results can be shown in their own popup sheet now (this shows by default on mobile) * There is now a helpful banner prompt to install the app on mobile. * When the postmaster is near full, a banner will warn you even if you're not on the inventory screen. * Artifact XP progress is now displayed for the correct season. * Rearranged the search buttons so the menu icon never moves. * Ranks for Vanguard and Trials are now shown in the Progress page. * Changed the icons in the Vendors menu. * Added Parallax Trajectory to the currencies hover menu. ## 6.79.1 (2021-08-25) * Legacy mods are no longer selectable in the Loadout Optimizer. ## 6.79.0 (2021-08-22) ## 6.78.0 (2021-08-15) * Armor in the Organizer no longer displays the now-standard Combat Mod Slot ## 6.77.0 (2021-08-08) * Timelost weapons now include their additional Level 10 Masterwork stats. ## 6.76.0 (2021-08-01) * Legendary Marks and Silver once again appear in the D1 inventory view. * Tap/hover the Artifact power level in the header, to check XP progress towards the next level. * When you install DIM on your desktop or home screen, it will now be badged with the number of postmaster items on the current character. You can disable this from Settings. This won't work on iOS. ## 6.75.0 (2021-07-25) * When opening Compare for a Timelost weapon, we now also include non-Timelost versions of that weapon. * Display the energy swap or upgrade details for items in the Optimizer. * Optimizer is now better at matching a set to an existing loadout. * Compare will properly close (and not just become invisible) if all the items you're comparing are deleted. * Fixed the search actions (three dots) menu not appearing in Safari. ## 6.74.0 (2021-07-18) * Added the option to lock item element in the Optimizer's armor upgrade menu. * Not be broken * Fix issue with Optimizer crashing when socket data is not available. * Invalid search queries are greyed out, and the save search star is hidden. * Favour higher energy and equipped items for grouped items in the Optimizer. This will mainly be noticed by the shown class item. * Adding unequipped items to a loadout no longer also adds items from the Postmaster. ### Beta Only * The Search Results drawer is back in beta, ready for some more feedback. On mobile it shows up whenever you search, on desktop you can either click the icon or hit "Enter" in the search bar. Try clicking on items in the search results drawer - or even dragging them to characters! ## 6.73.0 (2021-07-11) * Solstice of Heroes pursuit list now shows the full description of the objectives, not just the checkboxes. * Recent searches are now capped at 300 searches, down from 500. * Armor synthesis materials are no longer shown in the currencies block under the vault. ## 6.72.1 (2021-07-06) * Solstice of Heroes is back and so is the **Solstice of Heroes** section of the **Progress** tab. Check it out and view your progress toward upgrading armor. ## 6.72.0 (2021-07-04) * Fixed issue with locked mod stats not being applied to a compared loadouts in the Optimizer. ## 6.71.0 (2021-06-27) * Armor 1 exotics are visible in the exotic picker, albeit unselectable. * Default to similar loadout as comparison base in Loadout Optimizer. * Armor upgrades in the Optimizer have full descriptions of their functionality. Added Ascendant Shard 'Not Masterworked' and 'Lock Energy Type' options. * In the Exotic Selector, the currently selected exotic is now highlighted. ## 6.70.0 (2021-06-23) * Fixed an issue where unwanted energy swaps were happening in the Optimizer. * Fixed an issue where mod energy types could be mismatched in the Optimizer. ## 6.69.2 (2021-06-22) * Fixed an issue with general mods returning no results in the Optimizer. ## 6.69.1 (2021-06-21) * Fix an issue crashing DIM on older versions of Safari. ## 6.69.0 (2021-06-20) * Added "Recency" Column & Sorting to Loadout Organizer, this allows viewing gear sorted by acquisition date. * Added ctrl-click to toggle item selection in Organizer. * Fix over-eager prompt to backup data when signing in. * Viewing artifact details no longer always shows The Gate Lord's Eye. * Scrolling to an item tile is now more accurate. * Vault of Glass milestone is now more clearly named. * Loadout Optimizer support for Vault of Glass mods. ## 6.68.0 (2021-06-06) * Some support for Vault of Glass mods in filters. Expect Loadout Optimizer fixes next week. * Clearer hover text for some Destiny icons inline with text. * Hovering Consumables in the Vault header now shows a list of owned materials and currencies. * `is:hasornament` now recognizes Synthesized armor. * DIM is less likely to log you out if Bungie.net is experiencing difficulties. * Stat searches now support `highest`, `secondhighest`, `thirdhighest`, etc as stat names. * Try out `basestat:highest:>=20 basestat:secondhighest:>=15` * Login screen is now more descriptive, and helps back up your settings if you're enabling DIM Sync for the first time. ## 6.67.0 (2021-05-30) * Items tagged "archive" are no longer automatically excluded from Loadout Optimizer and the Organizer. * Vendor items can now match wish lists. Check what Banshee has for sale each week! * You can put tags and notes on Shaders again. And for the first time, you can put them on Mods. Both are accessible from the Collections row in the Records tab. * iPhone X+ screens once again do not show grey corners in landscape mode. * Fixed a bug that broke part of the Progress page. * Fixed a bug that crashed DIM if you clicked the masterwork of some items. ## 6.66.2 (2021-05-25) * Fix for errors on viewing some items when DIM had just loaded. ## 6.66.1 (2021-05-24) * Fix for 404 errors when signing in with Bungie. ## 6.66.0 (2021-05-23) * Fix strange wrapping and blank space on the list of Currencies in the header. ## 6.65.1 (2021-05-17) * Fix for a crash on older browsers. ## 6.65.0 (2021-05-16) * Reimplemented the is:shaded / is:hasshader searches. * Crucible and Gambit ranks show on the Progress page again. * Fixed the display text for some bounties and rewards from a new text system in Season of the Splicer. * Fixed currencies wrapping weirdly when you're not in-game. ## 6.64.1 (2021-05-11) * Fix an issue where owning Synthesis currency was causing a crash. ## 6.64.0 (2021-05-09) ## 6.63.0 (2021-05-02) ## 6.62.0 (2021-04-25) * Exotic class item perks don't prevent selecting another exotic perk in Loadout Optimizer. * Buttons and menus are bigger and easier to tap on mobile. * Fixes to the heights of Loadout Optimizer result sets. * Aeon perks are highlighted as their armor's exotic effect. * Notes field hashtag suggestions tuned a bit to be more helpful. * Item notes are displayed in Compare sheet when hovering or holding down on an item icon. * Improvements to how drawer-style elements size themselves and interact with mobile keyboard popups. * Some quests were being skipped, but now display on the Progress page (catalyst quests, Guardian Games cards, Medal Case). * Armor stats changes * Stats have been revamped and show their actual game effect, including stats past the in-game display caps of 0 and 42. * Base stats are no longer confused by very large or very low current values. * Multiple mods affecting the same stat now display as separate stat bar segments. You can hover or tap these for more information. * Armor in collections now includes default stats and their exotic perks. ### Beta Only * If your postmaster is getting full, we'll show a banner if you're on a page where you wouldn't otherwise notice your full postmaster. Hopefully this helps avoid some lost items. * On mobile, if you're using DIM through a browser, we prompt to install the app. Not trying to be annoying, but DIM is way better installed! ## 6.61.0 (2021-04-18) * Fixed the stats for some perks if they would bring a stat above the maximum value. * Creating a loadout from existing items will also save the items' current mods in the loadout. Viewing the mods is still Beta-only. * Fixed Loadout Optimizer mod assignment for raid mods. * Fixed Loadout Optimizer sometimes not handling T10+ stats correctly. * Loadout Optimizer knows about Nightmare Mods now. * You can now combine stats in search with & to average multiple stats. For example `basestat:intellect&mobility:>=15` shows if the average of intellect & mobility is greater than or equal to 15. * Notes field now suggests your previously-used hashtags as you type. * Collect Postmaster button is looking as slick as the rest of the app now. ## 6.60.0 (2021-04-11) * When opening Compare for an Adept weapon, we now also include non-Adept versions of that weapon. * We now remove leading or trailing spaces from loadout names when they are saved. * In the item popup, exotic armor's exotic perk is now described in full above the mods. * You can once again compare ghosts and ships. You can no longer compare emblems. * Changing perks on items in Compare now re-sorts the items based on any updated stats. ### Beta Only * You can now edit a loadout's mods in the loadout drawer. ## 6.59.1 (2021-04-05) * Correct suggestions & interpretation for `inloadout` filter. ## 6.59.0 (2021-04-04) * Visual refresh for buttons and some dropdowns. * Swiping between characters on mobile by swiping the inventory works again. * Swiping the character headers behaves more sensibly now. * Search * Loadouts can be found by exact name. For instance, `inloadout:"My PVP Equipment"` will highlight any items in the `My PVP Equipment` loadout. * To help reduce typing and remembering, `inloadout` names, `perkname`s, and item `name`s are now suggested as you type them. * We will also suggest any #hashtags found in your notes, for instance... `#pve`? * Loadout Optimizer * Mod groupings have been updated so inconsistent labels don't split them apart. * Half-tiers show up in results to warn you when a +5 stat mod might benefit you. * In these cases, a new +5 button can quickly the suggested mods to your loadout. ## 6.58.0 (2021-03-28) * When comparing items, the item you launched Compare from is now highlighted with an orange title. * The Compare screen has an "Open in Organizer" button that shows the exact same items in the Organizer which has more options for comparing items. * Fixed some mods in Loadout Organizer that weren't applying the right stats. * You can now sort inventory by how recently you acquired the item. ## 6.57.1 (2021-03-22) * Remove `sunsetsin:` and `sunsetsafter:` filters, and remove power cap display from Compare/Organizer. Organizer gains a new "Sunset" column. Items that are sunset can still be selected with `is:sunset` and have a grey corner. * Fix Loadout Optimizer acting as if "Assume Masterworked" was always checked. ## 6.57.0 (2021-03-21) * We went back to the old way search worked, reverting the change from v6.56. So now `gnaw rampage zen` searches for three independent properties instead of the literal string `"gnaw rampage zen"`. * Clicking on the empty area below Organizer can now close item popups, where it didn't before. * Fix an issue where an exotic perk could sometimes be unselectable in Loadout Optimizer. * Added a new `is:pinnaclereward` search that searches for pinnacle rewards on the Progress page. * DIM Sync now less picky about saving very simple searches. * Fix mis-sized kill tracker icons in Organizer. * Support addition syntax in stat filters, i.e. `stat:recovery+mobility:>30` * Mulligan now shows up as a Wishlisted perk. * Search bar expands more readily to replace the top tabs, so the field isn't squished really tiny. * Loadout Optimizer * Reposition some misplaced pieces of UI * Performance optimizations and some tweaks that could theoretically include some builds that wouldn't have shown up before. * Fixed an issue that would show builds with more than 100 in a single stat once mods were included. * Removed the minimum power and minimum stat total filters. Minimum power didn't see much use and minimum stat total can be achieved by searching `basestat:total:>52` in the search bar. ## 6.56.1 (2021-03-14) * Fix a bug where clicking inside the mod picker would dismiss the popup. ## 6.56.0 (2021-03-14) * On the Compare screen, items will update to show their locked or unlocked state. * Deleting multiple searches from your search history works now - before there was a bug where only the first delete would succeed. * On the Search History page accessible from Settings, you can now clear all non-saved searches with a single button. * Deprecated search filters no longer show up in Filter Help. * Searches that don't use any special filters now search for the entire string in item names and descriptions and perk names and descriptions. e.g. `gnawing hunger` now searches for the full string "gnawing hunger" as opposed to being equivalent to `"gnawing" and "hunger"`. * Invalid searches no longer save to search history. * Bright engrams show up correctly in the seasonal progress again. * Added an icon for Cabal Gold in objective text. * You can sort items by ammo type. * There's a new button in the Loadout editor to add all unequipped items, similar to adding all equipped items. * The farming mode "stop" button no longer covers the category strip on mobile. * Reverting a loadout (the button labeled "Before [LoadoutName]") no longer pulls items from Postmaster. ## 6.55.0 (2021-03-07) * You can once again select how much of a stackable item to move, by editing the amount in the move popup before clicking a move button. Holding shift during drag no longer allows you to select the amount - you must do it from the buttons in the popup. ## 6.54.0 (2021-02-28) ## 6.53.0 (2021-02-21) * Pulling from postmaster, applying loadouts, moving searches, moving individual items, and more are now cancel-able. Click the "cancel" button in the notification to prevent any further actions. * Bulk tagging in the Organizer no longer shows an "undo" popup. We expect you know what you're doing there! ## 6.52.0 (2021-02-14) * Search filters that operate on power levels now accept the keywords "pinnaclecap", "powerfulcap", "softcap", and "powerfloor" to refer to the current season's power limits. e.g "power:>=softcap" * `powerlimit:pinnaclecap` will show items with a power limit that matches this season's limit on all items. * `sunsetsin:next` will show the same items: items whose power limit won't reach next season's limit on all items. * Confirm before pulling all items from Postmaster. * Added Seasonal Challenges to the Records page. You can track as many of these as you want in DIM and the tracked ones will show up in the Progress page. * Quests that expire after a certain season now show that info in the item popup. * Quests show which step number on the questline they are. * Triumphs that provide rewards for completing a part of the triumph now show that reward. ## 6.51.1 (2021-02-10) * Updates for Season of the Chosen ## 6.51.0 (2021-02-07) ## 6.50.0 (2021-01-31) * Some emblem stats have better formatting now. * Perks which would grant a bonus in a stat, but which grant zero points due to how stats work, now show +0 instead of just not showing the stat. * Bounty guide for special grenade launchers now shows a label and not just an icon. * Fixed some issues with Loadout Optimizer on mobile. ## 6.49.0 (2021-01-24) * Mod categorization in the Loadout Optimizer mod picker is now driven from game data - it should stay up to date better as new mods appear. * Disabled weapon mods no longer contribute to stats. * Automatic updates for the latest patch. ## 6.48.0 (2021-01-17) * Allow clicking through the loading screen to get to the troubleshooting link. ## 6.47.1 (2021-01-11) * Fix a bug that could crash loadout optimizer. ## 6.47.0 (2021-01-10) * Show a star icon for favorited finishers rather than a lock icon. * Search history truncates huge searches to three lines and aligns the icons and delete button to the first line. * Added indicators in the Compare view to show which stat we are sorting by, and in which direction. * Fix visuals on the pull from postmaster buttons. * Loadout Optimizer now allows selecting up to 5 raid mods, not just 2. * Armor mods with conditional stats, like Powerful Friends and Radiant Light, now correctly take into account the conditions that cause their stats to be applied. This only works within a single piece of armor - for example, it will work if you have Powerful Friends and another Arc mod is socketed into that piece of armor, but will not yet correctly identify that the stats should be enabled when you have another Arc Charged With Light mod on *another* piece of armor. * Masterworked Adept weapons should show all their stat bonuses. * Fix a bug where using the move buttons instead of drag and drop wouldn't show item move progress popups or error popups. * The most recent Steam Overlay browser version shouldn't be reported as not supported anymore. Keep in mind we can't really debug any problems that happen in the Steam Overlay. * Fixed some event-specific searches, such as source:dawning. ## 6.46.0 (2021-01-03) * Base stats no longer cause sort issues in the compare pane, and no longer apply to weapons. * Older pieces of Last Wish and Reverie Dawn armor now count as having normal Legacy mod slots. * Deep Stone Crypt Raid mods now show up in the Loadout Optimizer mod picker. ## 6.45.2 (2020-12-30) * Fixed an issue that could harm the DIM Sync service. ## 6.45.1 (2020-12-29) * Fixed an issue where linking directly to any page would redirect to the inventory. ## 6.45.0 (2020-12-27) * Faster initial page load for inventory (loading a subset of things from bungie.net api) * Wishlists now support multiple URLs * Collection items in records now display the intrinsic perk. * Fixed an issue with the item popup sidecar on safari. * Fixes for compare view on mobile. * The optimizer now clears results if a character is changed. * Fix typo in energycapacity organizer search * Clean up toolbar on organizer page on mobile. * Some routes can now be accessed without being logged in (Whats New, Privacy Policy, etc.) * What's new page is now rendered at build time instead of run-time, so it should load faster. * Various dependency upgrades ## 6.44.0 (2020-12-20) * Fixed a bug that could potentially erase some tags/notes if there were errors in DIM. * When Bungie.net is undergoing maintenance, item perks won't be shown anymore. Before, we'd show the default/collections roll, which confused people. * Fix the element type of items not showing in some cases. * Improved the sizing of sheet popups on Android when the keyboard is up. * You can no longer transfer Spoils of Conquest anywhere. * Hide action buttons on collections/vendors items. * Fixed character headers wrapping on non-English locales. ### Beta Only * We continue to experiment with the order of the list-style perk display on weapons - the most important perks tend to be on the rightmost column of the grid, so now we list the perks in right-to-left order from the original grid. ## 6.43.2 (2020-12-13) ## 6.43.1 (2020-12-13) ## 6.43.0 (2020-12-13) * New Rich Texts added for Lost Sectors and Stasis. * Show reasons why you can't buy vendor items, and grey out bounties that you've already purchased on the vendors screen. * Updated the item popup header for mobile and desktop. The buttons on mobile now have larger click targets and should be easier to find/use. * Green items can no longer mess up loadout optimizer. * Special-ammo grenade launchers are now distinguished from heavy grenade launchers. ## 6.42.3 (2020-12-07) * Filter ornaments to the correct class for season pass on progress page. * Enable bounty guide on app.destinyitemmanager.com. * Spoils of Conquest vault prevention. ### Beta Only * Re-order sockets putting key traits first. ## 6.42.2 (2020-12-06) * Banner Tweaks ## 6.42.1 (2020-12-06) * Banner Tweaks ## 6.42.0 (2020-12-06) * Farming mode now refreshes only every 30 seconds, instead of every 10 seconds, to reduce load on Bungie.net. * When the postmaster section is collapsed, it now shows the number of items in your postmaster so you can keep an eye on it. * Fixed an issue where the Game2Give donation banner could sometimes appear in the mobile layout. ### Beta Only * We're trying out a new display for weapon perks, which displays the name of the active perk and shows details on click, instead of on hover. This is partly to make perks easier to understand, but also to allow for more actions on perks in the future. Let us know what you think! Animations will be added later if this design catches on. * Continued improvements to Active mode, incorporating Bounty Guide and better suggested vendor bounties. ## 6.41.1 (2020-12-02) ## 6.41.0 (2020-12-02) * Bounties and Quests sections on the Progress page now show a summary of bounties by their requirement - weapon, location, activity, and element. Click on a category to see bounties that include that category. Other categories will light up to show "synergy" categories that can be worked on while you work on the selected one. Shift-click to select multiple categories. Click the (+) on a weapon type to pull a weapon matching that type. * New item sort option to sort sunset items last. * Engrams show their power level - click on small engrams to see their power level in the item popup. * The checkmark for collectibles is now on the bottom right corner, so it doesn't cover mod cost. * Mod costs display correctly on Firefox. * Fixed the `is:powerfulreward` search to recognize new powerful/pinnacle engrams. * When items are classified (like the new Raid gear was for a bit), any notes added to the item will show on the tile so you can keep track of them. * Fixed filter helper only opening the first time it is selected in the search bar * Pinnacle/powerful rewards show a more accurate bonus, taking into account your current max power. ### Beta Only * A new "Single character mode" can be enabled through settings, or the « icon on desktop. This focuses down to a single character, and merges your other characters' inventories into the vault (they're really still on the other characters, we're just displaying them different). This is intended for people who are focused on one character, and always shows the last played character when collapsed. ## 6.40.0 (2020-11-22) * Mod and mod slot info in Loadout Optimizer have been updated to handle the new mod slots better. * Postmaster items can be dragged over any items on your character to transfer them - they don't need to be dragged to the matching item type. * Stop showing extra +3 stats on masterwork weapons. The fix for this means that Adept weapons may not show that bonus when they are released. * Progress page now shows more Milestones/Challenges, shows rewards for all of them, includes vendor pictures where available, and gives a hint as to what power pinnacle/powerful engrams can drop at. ## 6.39.1 (2020-11-16) * Farming mode will no longer immediately kick out items you manually move onto your character. * The Records page now includes all the Triumphs and Collections info that are in the game. * Mods in the Loadout Optimizer can be searched by their description. * Fixed Active Mode showing up in release version if you'd enabled it in Beta. * Fixed a crash when viewing Stasis subclasses. ## 6.39.0 (2020-11-15) * Xur's location is now shown on his entry in the Vendors page. * The Raids section is back in Progress, and Garden of Salvation shows up in Milestones. * Search autocomplete suggests the `current` and `next` keywords for seasons. * Reworked mod handling to account for new legacy and combat mod slots. New searches include `holdsmod:chargedwithlight`, `holdsmod:warmindcell`, etc., and `modslot:legacy` and `modslot:combatstyle`. * Armor tiles now display the energy capacity of the armor. * Masterwork levels in the mod details menu once again show which level masterwork they are. * Added a new sort order for items, sort by Seasonal icon. * Darkened the item actions sidecar to improve contrast with the background. * Fixed a visual glitch where the tagging menu looked bad. * Fixed logic for determining what can be pulled from postmaster to exclude stacked items like Upgrade Modules when you cannot actually pull any more of them. * Removed the counter of how many items were selected in Organizer. This fixes a visual glitch that cut off the icons when items were selected. * Fixed the vendor icon for Variks. * Loadout drawer, Compare, Farming, and Infusion now work on every page that shows an item from your inventory. * Deleting a loadout from the loadout drawer now closes the loadout drawer. * When Bungie.net is not returning live perk information, we won't show the default perks anymore. ### Beta Only * Preview of "Active Mode", an in-progress new view that focuses down to a single character plus your vault, and has easy access to pursuits, farming, max light, and more. ## 6.38.1 (2020-11-11) * Removed character gender from tiles and notifications. * Don't show empty archetype bar for items in collections. * Deprecated the `sunsetsafter` search filter because its meaning is unclear. Introduced the `sunsetsin` filter and the `is:sunset` filter. * Try out `sunsetsin:hunt` for weapons which reached their power cap in season 11. * `is:sunset` won't show anything until Beyond Light launches! * Added `current` and `next` as season names for searches. Search `sunsetsin:next` to see what'll be capped in next season even before it has an official name. * Vendorengrams.xyz integration has been removed, because of the vendor changes in Beyond Light. * Legacy Triumphs have been removed. * Fixed the Progress page not loading. * Fixed Catalysts not showing on the Records page. * Fix errors when selecting mods in Loadout Optimizer. * Removed the opaque background from item season icons. ## 6.38.0 (2020-11-08) * New background color theme to tie in with Beyond Light. The character column coloring based on your equipped emblem has been removed. * Perk and mod images are once again affected by the item size setting. ## 6.37.2 (2020-11-03) * Fix the item tagging popup not working on mobile by un-fixing the Safari desktop item popup. ## 6.37.1 (2020-11-02) * Fixed not being able to scroll on mobile. * Fixed filter help not always showing up. ## 6.37.0 (2020-11-01) * Removed "Color Blind Mode" setting. This didn't help with DIM's accessibility - it just put a filter over the page to *simulate what it would be like* if you had various forms of color blindness. * Added `hunt` as valid season synonym. * Clicking on the energy track or element for armor can now let you preview how much it'd cost in total to upgrade energy or change element. * Redesigned weapon perks/mods to more clearly call out archetype and key stats. * Improved the buttons that show in the item popup for items in postmaster. For stacked items you can now take just one, or all of the item. * Some items that DIM couldn't pull from postmaster before, can be pulled now. * Fixed the display of stat trackers for raid speed runs. * Hide the "kill tracker" perk column on masterworked weapons. * Fixed the tagging dropdown not being attached on desktop Safari. ## 6.36.1 (2020-10-26) * Some more tweaks to the sidecar layout. * Put back automatically showing dupes when launching compare. * The item popup now closes when you start dragging an item. ## 6.36.0 (2020-10-25) * Rearranged equip/move buttons on sidecar to be horizontal icons instead of menu items. * On mobile, you can switch characters in either direction, in a loop. * Added cooldown and effect values to stat tooltips. * Added stat tooltips to the Loadout Optimizer. * Fixed descriptions for mod effects in the Loadout Optimizer's mod picker. * New keyboard shortcuts for pull item (P), vault item (V), lock/unlock item (L), expand/collapse sidecar (K), and clear tag (Shift+0). Remember, you must click an item before being able to use shortcuts. * Made the item popup a bit thinner. * Collapsing sections now animate open and closed. ### Beta Only * We're experimenting with a new "Search Results" sheet that shows all the items matching your search in one place. ## 6.35.0 (2020-10-18) * Added the "sidecar" for item popup actions on desktop. This lets us have more actions, and they're easier to understand. If you always use drag and drop, you can collapse the sidecar down into a smaller version. * On mobile, press and hold on an item to access a quick actions menu, then drag your finger to an option and release to execute it. Move items faster than ever before! * Added buttons to the settings page to restore the default wish list URL. * Tweaked the Loadout Optimizer to make it easier to understand, and more clearly highlight that stats can be dragged to reorder them. * In Loadout Optimizer, Compare Loadout can now compare with your currently equipped gear. Also, clicking "Save Loadout" will prompt you for whether you want to overwrite the loadout you're comparing with. * Fixed an issue where you couldn't directly edit the minimum power field in Loadout Optimizer. * D1 items can no longer incorrectly offer the ability to pull from postmaster. * Tuned the search autocomplete algorithm a bit to prefer shorter matches. * Fixed multi-stat masterworked exotics messing up the CSV export. * Darkened the keyboard shortcut help overlay (accessed via the ? key). * Removed tagging keyboard shortcut tips from places where they wouldn't work. ## 6.34.0 (2020-10-11) * Replaced the tagging dropdown with a nicer one that shows the icon and keyboard shortcut hints. * Made the farming mode popup on mobile not overlap the category selector, and made it smaller. * Secretly started recording which mods you selected in Loadout Optimizer when you create a loadout, for future use. * In the Organizer, the selected perk for multi-option perks is now bold. * Updated the style and tooltip for wishlist perks to match the thumb icon shown on tiles. * Fix some display of masterworked exotics in the CSV export. ## 6.33.0 (2020-10-04) * The Organizer's buttons now show you how many items you have in each category. These counts update when you use a search too! * On mobile, the search bar appears below the header, instead of on top of it. * Changed the effect when hovering over character headers. * Hitting Tab while in the search bar will only autocomplete when the menu is open. * Fixed the "custom stat" setting not being editable from Safari. * Consumables may no longer be added to loadouts for D2. * The Loadout Optimizer lock item picker will show items that are in the Postmaster. ### Beta Only * Removed the ability to move a specific amount of a stacked consumable item. * Continued updates to our new background style and desktop item actions menu. ## 6.32.2 (2020-09-29) * Actually fixed "Store" buttons not showing for items in Postmaster. * Fix wishlists not highlighting the right rolls. ## 6.32.1 (2020-09-29) * Fixed "Store" buttons not showing for items in Postmaster. * Fixed masterwork stats for Exotics not displaying correctly. * Fixed character stats only displaying the current character's stats on mobile. * Fixed Postmaster not appearing on D1 for mobile. ## 6.32.0 (2020-09-27) * In Compare, you can click on perks to see what the new stats would look like if you chose another option. * When the item popup is open, hitting the "c" key will open Compare. * Your subclass has been moved below weapons and armor (it's been this way in Beta for a while). * On mobile, instead of showing all your items at once, there's now a category selection bar that lets you quickly swap between weapons, armor, etc. The postmaster is under "inventory". * Transferring items is just a touch snappier. * The tag and compare button on the search bar have been replaced with a dropdown menu (three dots) with a lot more options for things you can do with the items that match your search. * On mobile, your equipped emblem no longer affects the color of your screen. * Loadout Optimizer has a nicer layout on mobile and narrower screens. * Fix some masterwork stats not showing. * Fix some issues with how mods got auto-assigned in Loadout Optimizer. * Fix masterwork stats not always highlighting. * Fix masterwork tier for some items. * Fix an issue where searching for "ote" wouldn't suggest "note:" * The Organizer shows up in the mobile menu, but it just tells you to turn your phone. ### Beta Only * We're experimenting with moving the item action buttons to the side of the item popup on desktop - we call it the "sidecar". It moves the actions closer to the mouse, allows room to have clearer labels, and gives more room to add more commands. Plus generally people have screens that are wider than they are tall, so this reduces the height of the popup which could previously put buttons off screen. We'll be tweaking this for a while before it launches fully. * Beta now has an early preview of a new theme for DIM. ## 6.31.2 (2020-09-22) * Fix an issue where moving Exotic Cipher to vault with DIM would cause your characters to be filled up with items from your vault. ## 6.31.1 (2020-09-21) * Loadout Optimizer highlights loadouts you've already saved. * Add new searches `kills:`, `kills:pvp:`, and `kills:pve:` for Masterwork kill trackers. * Fixed: "Source" was not being set for all items. * Fixed: Item type searches (e.g. is:pulserifle) not working for D1. * Fixed: Spreadsheets missing power cap. ## 6.31.0 (2020-09-20) * Added a link to the DIM User Guide to the hamburger menu. * "Clear new items" has been moved into the Settings page instead of being a floating button. The "X" keyboard shortcut no longer clears new items. * Linear Fusion rifles are technically Fusion Rifles, but they won't show up in Organizer or in searches under Fusion Rifle anymore. * While API performance is ultimately up to Bungie, we've changed things around in DIM to hopefully make item transfers snappier. Note that these changes mean you may see outdated information in DIM if you've deleted or vaulted items in-game and haven't clicked the refresh button in DIM. * Improved the autocomplete for `sunsetsafter:` searches. * Fix the `is:new` search. * The D1 Activities page now shows Challenge of the Elders completion. * Fixed buttons not showing up on tablets for track/untrack triumphs. * Invalid searches are no longer saved to your search history. * The "Filter Help" page is now searchable, and clicking on search terms applies them to your current search. * Added a Search History page accessible from "Filter Help" and Settings so you can review and delete old searches. * Shift+Delete while highlighting a past search in the search dropdown will delete it from your history. * Fixed the `masterwork:` filters. * Fixed the icon for "Take" on the item popup for stackable items. * Removed the ability to restore old backups from Google Drive, or backups created from versions of DIM pre-6.0 (when DIM Sync was introduced). * Armor 1.0 mods and Elemental Affinities removed from the perk picker in Loadout Optimizer. * Improved search performance. * Items in collections now show their power cap. * Character stats now scroll with items on mobile, instead of always being visible. Max power is still shown in the character header. * Added "Location" column to the Organizer to show what character the item is on. * When "Base Stats" is checked in the Compare tool, clicking on stats will sort by base stat, not actual stat. ### Beta Only * On mobile, there is now a bar to quickly swap between different item categories on the inventory screen. ## 6.30.0 (2020-09-13) * Compare loadouts in Loadout Optimizer to your existing loadout by clicking the "Compare Loadout" button next to a build. * Improvements to search performance, and search autocomplete suggestions. * Fix cases where some odd stats would show up as kill trackers. * Sword-specific stats now show up in `stat:` filters. ## 6.29.1 (2020-09-11) * Improved performance of item transfers. We're still limited by how fast Bungie.net's API can go, though. * Fixed a couple of the legacy triumphs that indicated the wrong triumph was being retired. * Completed legacy triumph categories, and collections categories, now show the "completed" yellow background. * is:seasonaldupe now correctly pays attention to the season of the item. * Fixed a bug where notes wouldn't be saved if you clicked another item before dismissing the item popup. * Tweaks to the display of legacy triumphs. * Reduce the number of situations in which we autoscroll the triumph category you clicked into view. ## 6.29.0 (2020-09-10) * Legacy Triumphs are now indicated on the Records page and have their own checklist section. Legacy Triumphs are triumphs that will not be possible to complete after Beyond Light releases. The list of which Triumphs are Legacy Triumphs was provided by Bungie. * Mods in the Loadout Optimizer mod picker are now split up by season. * The number of selected items is now shown on the Organizer page. * Empty mod slot tooltips spell out which season they're from. * Locking/unlocking items in D1 works again. ## 6.28.1 (2020-09-06) * Actually release the Records page ## 6.28.0 (2020-09-06) * Triumphs, Collections, and Stat Trackers are now all together in the new Records page. * You can track triumphs in DIM - tracked triumphs are stored and synced with DIM Sync. These show up on both the Progress and Records pages. * Everything on the Records page responds to search - search through your Collections, Triumphs, and Stat Trackers all at once! * Unredeemed triumphs show their rewards * Compare sheet now offers a Base Stat option for armor, so you can directly compare your stat rolls * Mod costs now shown in Loadout Optimizer results * Vendors can now track some "pluggable" items like emotes & ghost projections, to filter by whether you already own them * Clearing the search input no longer re-opens the search dropdown * Mod slot column in the Organizer now shows all supported mod types (i.e. season 10 armor will show seasons 9,10,11) * Support for `mod:` and `modname:` filters to parallel the `perk:` and `perkname:` ones * Use the dark theme for Twitter widget ## 6.27.0 (2020-08-30) * The new armor 2.0 mod workflow is available in the Loadout Optimizer, this includes: * A new Mod Picker component to let you choose armor 2.0 mods to lock. * The mod sockets shown in the optimizer are now the locked mods, rather than the mods currently equipped on the item. * Clicking on a mod socket will open the picker to show available mods for that slot. Note that locking a mod from this won't guarantee it gets locked to the item specifically. * Items have different levels of grouping depending on the requirements of the locked mods. Locking no mods keeps the previous grouping behavior. * The mods stat contributions are now shown in the picker. * The Mod Picker can now filter for items from a specific season, just filter by the season number directly e.g. "11" for arrivals. * The search bar now remembers your past searches and allows you to save your favorite searches. These saved and recent searches are synced between devices using DIM Sync. * The quick item picker (plus icon) menu no longer has an option to equip the selected item. Instead it will always just move the item - very few users selected "Equip" and it won't ever work in game activities. * Added background colors for items and characters before their images load in, which should reduce the "pop-in" effect. * Shaders can be tagged from the Collections page and the tags/notes show up there as well. * Shift+Click on the Notes field in Organizer while in edit mode no longer applies a search. * For pages with sidebars (like Progress), scrollbars appearing will no longer cover content. * Add character stats to loadout sheet if full armor set is added. ### Beta Only * Long-pressing on an item in mobile mode will bring up a quick actions menu - drag and release on a button to apply the action to the item you pressed on. * Move Sub-class out of Weapons to the General category ## 6.26.0 (2020-08-23) * Better touchscreen support for drag and drop. * Wishlists now support Github gists (raw text URLs), so there's no need to set up an entire repository to host them. If you are making wishlists, you can try out changes easier than ever. If you're not making wishlists, hopefully you're using them. If you don't know what wishlists are, [here you go](https://guide.dim.gg/Wish-Lists) * Engrams get a more form-fitting outline on mouse hover. * If you have a search query active, DIM will not automatically reload to update itself. * The `is:curated` search has been overhauled to better find curated rolls. * Fixes to how the character headers look in different browsers. * Fixed the missing armor.csv button on the Organizer. ### Beta Only * Loadout Optimizer: DIM Beta is now using the new Mod Picker, a separate and improved picker just for armor mods. Try it out and let us know how it feels * In Beta only, the filter search bar has been upgraded to remember recent searches and let you save your favorite searches. * Phone/mobile resolutions will now show a mini-popup to make inspecting and moving items much easier. ## 6.25.0 (2020-08-16) * Removed `is:reacquireable` as it is inaccurate in its current state * Removed outline from clicked character headers on iOS * Adjusted spacing on items in the loadout drawer, so they can fit 3-wide again * Main (top) search field is now the place to filter items for the Loadout Optimizer * For real, stat bars should be the right length this time * Keyboard controls in the Notes field: ESC reverts and leaves editing, ENTER saves the value * Item notes can now be edited directly in the notes column of the Organizer tab * Mobile - changes in DIM beta only: different parts of the header now stick with you as you scroll down. * Armor CSV export appearing properly on the Organizer tab again. ## 6.24.1 (2020-08-12) * Updated the character tiles, now uses triple dot instead of chevron * Solstice of Heroes is back and so is the **Solstice of Heroes** section of the **Progress** tab. Check it out and view your progress toward upgrading armor. ## 6.24.0 (2020-08-09) * Configure a custom armor stat per-class in Settings, and it'll show up in item popups, Organizer, Compare, and the new `stat:custom:` search. * Speed improvements to wishlist processing. * `is:smg` for if you're as bad at remembering "submachine gun" as.. some of us are. * No more accidental app reloads when swiping down hard on the page on mobile. * Spring (Summer?) cleaning in the Item Popup. Some less important elements have been moved or removed, to make room for more functionality and stats. * Bar-based stat values in the Mod preview menu are no longer extremely large bois. * Anti-champion damage types are now interpreted in tooltip descriptions. * Seasonal Artifact is now previewable, but be warned: * Some data from the API is wrong, and the Season 11 artifact is incorrectly labeled. * It can show seasonal mods you have equipped, but Season 11 mods still aren't in Collections data, so mod unlocks aren't displayed. * Spreadsheet columns slightly adjusted to get them back to their usual column names. * Lots going on behind the scenes to clear up errors and get Loadout Optimizer ready for upgrades! ## 6.23.0 (2020-08-02) * You can add tags and notes to shaders! Keep track of your favorites and which shaders you could do without. * Searches now support parentheses for grouping, the "and" keyword, and the "not" keyword. Example: `(is:weapon and is:sniperrifle) or not (is:armor and modslot:arrival)`. "and" has higher precedence than "or", which has higher precedence than just a space (which still means "and"). * Fixed the size of damage type icons in D1. * Our Content Security Policy is more restrictive now, external and injected scripts may fail but this keeps your account and data safer. ## 6.22.1 (2020-07-27) ## 6.22.0 (2020-07-26) * New: More detailed gear information is available by hovering or clicking the Maximum Gear Power stat in each character's header. * Improved detection that you need to reauthorize DIM to your Bungie account. * Fixes to how stat bars display when affected by negative modifiers & perks. * Clearer errors if DIM is unable to save the item information database. * Organizer * Power Limit column now generates the right filter when Shift-clicked. * Traits column content has been narrowed down. * Improved top level categories take fewer clicks to reach your items. * Loadout Optimizer * Fixed finding slots for seasonal mods. ## 6.21.0 (2020-07-19) * Added support for negative stats on mods. This should be visible in item displays and make loadout optimizer results more accurate. * Fix quick item picker not remembering your preference for "equip" vs "store". * Some quests can now be tracked or untracked from DIM. * Locking or unlocking items from DIM is now reflected immediately on the item tiles. * Items with the Arrivals mod slot now match the `holdsmod:dawn` search. ## 6.20.0 (2020-07-12) * Fix sorting by Power Limit in the compare pane. * When opening a loadout in the loadout optimizer from the inventory page, the correct character is now selected rather than the last played character. * Allow masterworks to affect more than one stat * Exclude subclasses from `is:weapon` filter. * Fixed Loadout Optimizer not including all the right tiers when tier filtering was in place. ## 6.19.0 (2020-07-05) * Loadout Optimizer has been... optimized. It now calculates sets in the background, so you can still interact with it while it works. * Removed ghosts from loadout optimizer as they don't have enough interesting perks to build into loadouts. * The filter help button is now always shown in the search bar, even when a search is active. * The item count in the search bar is now more accurate to what you see on the inventory screen. * Make it clearer that not having Google Drive set up doesn't matter that much since it's only for importing legacy data. * Better handling for if the DIM Sync API is down. ## 6.18.0 (2020-07-02) * Breaker type is now shown on the item popup and in the Organizer. * New filter for breaker types on weapons, `breaker:`. * Fixed another crash on the vendors screen also caused by the Twitch gift sub shader. * Protect against certain weird cases where DIM can get stuck in a non-working state until you really, thoroughly, clear your cache. ## 6.17.1 (2020-07-01) * Fix a crash with the Twitch gift sub shader. ## 6.17.0 (2020-06-28) * You can now filter out armor in the Loadout Optimizer by minimum total stats. This narrows down how many items are considered for builds and speeds up the optimizer. * Renamed the "is:reacquireable" filter to "is:reacquirable" * Searches like "is:inleftchar" now work with consumables in the postmaster. * Fixed the inventory screen jumping a bit when the item popup is open on mobile. * Add a link to the troubleshooting guide to error pages. * Seasonal mods in the loadout optimizer now force armor to match their element, again. * The stat in parentheses in a weapon perk tooltip, is the stat matching the masterwork. UI slightly updated to help show this. ## 6.16.1 (2020-06-22) * Fix a crash when opening some items in Organizer. ## 6.16.0 (2020-06-21) * Remove `is:ikelos` filter * Loadout Optimizer: Save stat order and "assume masterworked" choices. * Fixed a bug that caused the inventory view to jump to the top of the screen when items were inspected. * Add a disclaimer to power limit displays that they may change in the future. Please see https://www.bungie.net/en/Help/Article/49106 for updates * Save column selection for Ghosts in the Organizer separate from Armor. * Display how many tags were cleaned up in the DIM Sync audit log. * Fix a bug where canceling setting a note in the Organizer would wipe notes from selected items. * Add a pointer cursor on item icons in the Organizer to indicate they're clickable. * Fix minimum page width when there are fewer than three characters. * Fix Arrival mods not appearing in the Loadout Optimizer. * Fix a bug when DIM Sync is off that could repeatedly show a notification that an import had failed. Please consider enabling DIM Sync though, your data WILL get lost if it's disabled. ## 6.15.1 (2020-06-15) ## 6.15.0 (2020-06-14) * Items now show their power limit in the item popup, Compare, and in the Organizer (new column). Keep in mind some power limits may change in upcoming seasons. * Try the `sunsetsafter:` or `powerlimit:` filters to find things by their power limit. * Fix the season icon for reissued items. * Fix not being able to dismiss the item popup on the Organizer in certain cases. * Remove the 15 second timeout for loading data from Bungie.net. * Fix umbral engrams showing up weird in the engram row. * Prevent Chrome on Android from showing a "download this image" prompt when long-pressing on images. * Fix non-selected perks not showing on old fixed-roll weapons. * Add Charge Rate and Guard Endurance stat to swords. ## 6.14.0 (2020-06-07) * Fixed misdetection of seasonal mods in Compare. * Work around a Bungie.net issue that could prevent the Destiny info database from loading. * Improved the experience for users who previously had DIM Sync off. ## 6.13.2 (2020-06-03) ## 6.13.1 (2020-06-01) * Add a banner to support Black Lives Matter. * Avoid an issue where shift-clicking on empty space near perks in the Organizer can enable a useless filter. ## 6.13.0 (2020-05-31) * DIM data (loadouts, tags, settings) can no longer be stored in Google Drive. If you already have things stored there, you can use that data to import into the new storage, but it will no longer be updated. Disabling DIM Sync will now store data locally only. * The Vault Organizer is now available for D1. * CSV export will no longer erroneously consider calus as a source and instead output the correct source. * CSV export will now export the same source information that DIM uses for items that do not have a source in the API. * Fixed import/export of data - if your backups didn't load before, they should now. * Fixed Organizer default sorting for stats, and shift-click filtering for modslot. * Vendors data no longer has to reload every time you visit the page. * is:dupelower search is stabilized so that tagging items as junk doesn't change what is considered "lower" * Fixed loadouts with subclasses not fully transferring to the vault. * Don't display "ms" unit on Charge Time stat for D1 fusion rifles. ## 6.12.0 (2020-05-24) * DIM has a new community-driven user guide at https://destinyitemmanager.fandom.com/wiki/Destiny_Item_Manager_Wiki ## 6.11.0 (2020-05-17) * Added the Organizer page, which lets you see all your items in a table form, which you can sort and filter (try shift-clicking on a cell!). Add and remove columns and bulk-tag your items to help quickly figure out which items you want to keep and which you can get rid of. * Fixed stat calculations for special Taken King class items in D1. ## 6.10.0 (2020-05-10) ## 6.9.0 (2020-05-03) * In the Loadout Optimizer, mods have been split into their own menu, separate from perks. * Fixed a bug where wishlists would ignore settings and load the default wishlist instead. ## 6.8.0 (2020-04-26) * Added "armor 2.0" column to spreadsheet exports. * Fixed a bug that could affect the display of percentage-based objectives. ## 6.7.0 (2020-04-19) * Emblems now show a preview of their equipped stat tracker, and show which types of stat tracker the emblem can use. * Certain stat trackers (under "Metrics" in "Collections") had the wrong display value, like KDA. These have been fixed. * Loadout Optimizer now allows you to select seasonal mods independent of the gear they go on - it'll try to slot them into any gear. ## 6.6.0 (2020-04-12) * Better handling of logging out and into a different Bungie.net account. * Improved error handling for Bungie.net and DIM Sync issues. ## 6.5.0 (2020-04-10) * Improved overall performance and memory usage of DIM - as the game grows, so has DIM's memory usage. If your browser was crashing before, give it a try now. * Collectibles now show perks. ## 6.4.0 (2020-04-05) * Added stat trackers to the Collections page (under "Metrics") * Improved error handling when Bungie.net is down or something is wrong with your account. Includes helpful tips for D1 users locked out by Twitch-linking bug. If your D1 accounts disappeared, they're in the menu now. * Accounts in the menu are now always ordered by last-played date. * DIM will no longer bounce you to a different account if the one you wanted cannot be loaded. * Fixed some bugs that could cause D1 pages to not display. * Fix display of collectibles that are tied to one of your alternate characters. * Fix the levels that reward Bright Engrams after season rank 100. ## 6.3.1 (2020-03-29) * Fixed a bug where D1 items could fail to display. * Fixed a bug where responding "Not now" to the DIM Sync prompt wouldn't cause it to go away forever. * Make mod slot for Reverie Dawn armor set detect correctly as outlaw. ## 6.3.0 (2020-03-29) * Removed duplicate Mods section from the top level of the Collections screen - they're still under the normal collections tree. * Fixed a missing icon when season rank is over 100. ## 6.2.0 (2020-03-22) ## 6.1.1 (2020-03-22) ## 6.1.0 (2020-03-22) * Introducing [DIM Sync](https://guide.dim.gg/DIM-Sync-(new-storage-for-tags,-loadouts,-and-settings)), a brand new way for DIM to store your loadouts and tags and sync them between all your devices. This is a big step forward that'll let us build lots of new things and share data between other apps and websites! Plus, you no longer have to log into anything separate, and we should avoid some of the bugs that have in the past led to lost data. * External wish lists will be checked daily. Settings menu shows last fetched time. * Seasonal Artifact is no longer considered a weapon or a dupe when searching. * Event sources for items like Festival of the Lost and Revelry are now under the `source:` search like other sources, instead of `event:`. * Fixed some recent bugs that prevented editing loadouts. * Show how much of each material you have next to Spider's vendor info. * Updated privacy policy with DIM Sync info. ## v5 CHANGELOG * v5 CHANGELOG available [here](/docs/OLD_CHANGELOG/OLD_CHANGELOG_5.X.X.md) ================================================ FILE: docs/TRANSLATIONS.md ================================================ # Supported Languages Due to what information is provided by Bungie about item names and descriptions the following languages are the only languages that will be supported by DIM: - English - French - German - Italian - Japanese - Korean - Portuguese (BR) - Spanish - Spanish (Latin America) - Polish - Russian - Chinese (Traditional) - Chinese (Simplified) # Join the translation team @ Crowdin [Crowdin](https://crowdin.com/project/destiny-item-manager/invite?d=65a5l46565176393s2a3p403a3u22323e46383232393h4k4r443o4h3d4c333t2a3j4f453f4f3o4u643g393b343n4) There are two different roles available per language | Role | Responsibilities | |------|------------------| | Translator | Provide Translations | | Proofreader | Provide Translations and verify translations of others | # Translators Using the 'Show' dropdown menu, select 'Untranslated'. Translate these to your language. *Translations are not considered complete, until they have been proofread.* # Proofreaders *Ensure you mark all completed and correct translations as proofread!* # Raising Issues/Comments If a translation is wrong ensure you mark it as 'Fuzzy' or comment as an issue. If you just apply a comment stating something is wrong, the only way someone would see it is if they were reading all the comments on all the keys. # Discord Also ensure you join the [Discord](https://discord.gg/NV2YeC8) and PM @DelphiActual#3093 for an invite to the translation channel(#i18n). If you have any questions about translating/translations do not hesitate to ask in the #i18n channel. # Plurals & Gender Plurals, and gender are handled by strings that end in _one, _few, _many, _other, _male, or _female. If your language does not require the plural or gender form just copy the singular or neutral form and mark the translation as 'Fuzzy' and 'Proofread'. By marking it 'Fuzzy' it will not be downloaded automatically. Some locales handle plurals different and do not currently work using the `_one` and `_other` combination. * `_many` and `_few` * Polish * Russian * Only `_other` * Japanese * Chinese (Simplified) * Chinese (Traditional) If your language requires plural or gender support for a translation do not hesitate to ask! # List of Plural & Gender keys - BungieService.ItemUniquenessExplanation - FarmingMode.Desc - FarmingMode.MakeRoom.Desc - ItemService.BucketFull.Guardian - Loadouts.Applied - Loadouts.MakeRoomDone # Variables | Variable | Resolves to | |-----------|-------------| | {{store}} | Exo Male Warlock, etc | ================================================ FILE: docs/clean-changelog.rb ================================================ ARGF.each_line do |line| if line =~ /^# v?([\d.]+)/ version = $1 date = `git show v#{version} --quiet --pretty="format:%ai"`.strip.split(/ /)[0] if $?.success? puts line.strip + " (#{date})" else puts line end else puts line end end ================================================ FILE: eslint.config.js ================================================ import cssModules from '@bhollis/eslint-plugin-css-modules'; import react from '@eslint-react/eslint-plugin'; import { fixupPluginRules } from '@eslint/compat'; import eslint from '@eslint/js'; import arrayFunc from 'eslint-plugin-array-func'; import github from 'eslint-plugin-github'; import jsxA11y from 'eslint-plugin-jsx-a11y'; import reactPlugin from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import * as regexpPlugin from 'eslint-plugin-regexp'; import sonarjs from 'eslint-plugin-sonarjs'; import globals from 'globals'; import tseslint from 'typescript-eslint'; // TODO: different configs for JS vs TS export default tseslint.config( { name: 'eslint/recommended', ...eslint.configs.recommended }, ...tseslint.configs.recommendedTypeChecked, { name: 'typescript-eslint/parser-options', languageOptions: { parserOptions: { project: true, tsconfigRootDir: import.meta.dirname, }, }, }, ...tseslint.configs.stylisticTypeChecked, regexpPlugin.configs['flat/recommended'], { name: 'react', ...reactPlugin.configs.flat.recommended, plugins: { react: fixupPluginRules(reactPlugin), }, settings: { react: { version: 'detect', linkComponents: [ // Components used as alternatives to for linking, eg. , from React Router { name: 'Link', linkAttribute: 'to' }, { name: 'NavLink', linkAttribute: 'to' }, ], }, }, }, { name: 'array-func', ...arrayFunc.configs.all }, { name: 'css-modules', plugins: { 'css-modules': cssModules, }, rules: { 'css-modules/no-unused-class': ['error', { camelCase: true }] }, }, { name: 'sonarjs/recommended', ...sonarjs.configs.recommended }, { ...reactHooks.configs.flat.recommended, rules: { // Core hooks rules 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'error', // TODO: Enable react compiler rules when they are fixed and we are using // react compiler. Until then they are noisy and slow. }, }, { // We want to choose which rules to enable from the github plugin, not use a preset. name: 'github', plugins: { github: fixupPluginRules(github), }, rules: { 'github/array-foreach': 'error', 'github/async-currenttarget': 'error', 'github/async-preventdefault': 'error', 'github/no-innerText': 'error', }, }, { name: 'jsx-a11y', files: ['**/*.tsx'], plugins: { 'jsx-a11y': fixupPluginRules(jsxA11y), }, rules: { 'jsx-a11y/aria-props': 'error', 'jsx-a11y/aria-proptypes': 'error', 'jsx-a11y/aria-role': 'error', 'jsx-a11y/aria-unsupported-elements': 'error', 'jsx-a11y/autocomplete-valid': 'error', 'jsx-a11y/label-has-associated-control': 'error', 'jsx-a11y/no-noninteractive-element-interactions': 'error', 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error', 'jsx-a11y/no-noninteractive-tabindex': 'error', 'jsx-a11y/no-redundant-roles': 'error', 'jsx-a11y/role-has-required-aria-props': 'error', 'jsx-a11y/role-supports-aria-props': 'error', }, }, { name: 'eslint-react', ...react.configs['recommended-type-checked'], }, { name: 'global ignores', ignores: [ '*.m.scss.d.ts', '*.m.css.d.ts', 'src/build-browsercheck-utils.js', 'src/testing/jest-setup.cjs', 'src/fa-subset.js', 'src/data/font/symbol-name-sources.ts', // TODO: fix the source! ], }, { name: 'dim-custom', languageOptions: { ecmaVersion: 'latest', parserOptions: { ecmaVersion: 'latest', sourceType: 'module', }, sourceType: 'module', globals: { ...globals.browser, // TODO: limit to service worker ...globals.serviceworker, ...globals.es2021, $featureFlags: 'readonly', ga: 'readonly', $DIM_FLAVOR: 'readonly', $DIM_VERSION: 'readonly', $DIM_BUILD_DATE: 'readonly', $DIM_WEB_API_KEY: 'readonly', $DIM_WEB_CLIENT_ID: 'readonly', $DIM_WEB_CLIENT_SECRET: 'readonly', $DIM_API_KEY: 'readonly', $BROWSERS: 'readonly', workbox: 'readonly', React: 'readonly', require: 'readonly', module: 'readonly', }, }, linterOptions: { reportUnusedDisableDirectives: true, }, rules: { 'no-alert': 'error', 'no-console': 'error', 'no-debugger': 'error', 'no-empty': 'off', 'no-implicit-coercion': 'error', 'no-restricted-globals': [ 'error', 'name', 'location', 'history', 'menubar', 'scrollbars', 'statusbar', 'toolbar', 'status', 'closed', 'frames', 'length', 'top', 'opener', 'parent', 'origin', 'external', 'screen', 'defaultstatus', 'crypto', 'close', 'find', 'focus', 'open', 'print', 'scroll', 'stop', 'chrome', 'caches', 'scheduler', ], 'no-restricted-imports': [ 'error', { patterns: [ { group: ['testing/*'], message: 'You cannot use test helpers in regular code.', }, ], paths: [ { name: 'i18next', importNames: ['t'], message: 'Please import t from app/i18next-t.', }, { name: 'es-toolkit', importNames: [ 'compact', 'mapValues', 'isEmpty', 'sortBy', 'count', 'invert', 'sumBy', 'take', 'noop', ], message: 'Please use functions from app/util/collections or native equivalents instead.', }, { name: 'es-toolkit', importNames: ['uniq'], message: 'Please use Array.from(new Set(foo)) or [...new Set(foo)] instead.', }, { name: 'es-toolkit', importNames: ['uniqBy'], message: 'Please use the uniqBy from app/utils/util instead', }, { name: 'es-toolkit', importNames: ['groupBy'], message: 'Use Object.groupBy or Map.groupBy instead.', }, { name: 'es-toolkit', importNames: ['cloneDeep'], message: 'Use structuredClone instead.', }, { name: 'es-toolkit', importNames: ['sortBy'], message: 'Use the native .sort or .toSorted functions with compareBy and chainComparator.', }, { name: 'es-toolkit', importNames: ['compact'], message: 'Use the compact function from app/util/collections instead.', }, { name: 'es-toolkit', importNames: ['isEmpty'], message: 'Use the isEmpty function from app/util/collections instead.', }, ], }, ], 'no-restricted-syntax': [ 'error', { selector: "CallExpression[callee.name='compact'][arguments.0.callee.property.name='map']", message: 'Please use `filterMap` instead', }, { selector: "CallExpression[callee.name='clsx'][arguments.length=1][arguments.0.object.name='styles'],CallExpression[callee.name='clsx'][arguments.length=1][arguments.0.type='Literal']", message: 'Unnecessary clsx', }, { selector: 'TSEnumDeclaration:not([const=true])', message: 'Please only use `const enum`s.', }, ], // TODO: Switch to @stylistic/eslint-plugin-js for this one rule 'spaced-comment': [ 'error', 'always', { exceptions: ['@__INLINE__'], block: { balanced: true } }, ], 'arrow-body-style': ['error', 'as-needed'], curly: ['error', 'all'], eqeqeq: ['error', 'always'], 'no-return-await': 'off', '@typescript-eslint/return-await': ['error', 'in-try-catch'], 'prefer-regex-literals': 'error', 'prefer-promise-reject-errors': 'error', 'prefer-spread': 'error', radix: 'error', yoda: ['error', 'never', { exceptRange: true }], 'prefer-template': 'error', 'class-methods-use-this': ['error', { exceptMethods: ['render'] }], 'no-unmodified-loop-condition': 'error', 'no-unreachable-loop': 'error', 'no-unused-private-class-members': 'error', 'func-name-matching': 'error', 'logical-assignment-operators': 'error', 'no-lonely-if': 'error', 'no-unneeded-ternary': 'error', 'no-useless-call': 'error', 'no-useless-concat': 'error', 'no-useless-rename': 'error', 'react/jsx-uses-react': 'off', 'react/react-in-jsx-scope': 'off', 'react/no-unescaped-entities': 'off', 'react/jsx-no-target-blank': 'off', 'react/display-name': 'off', 'react/prefer-stateless-function': 'warn', 'react/no-access-state-in-setstate': 'error', 'react/no-this-in-sfc': 'error', 'react/no-children-prop': 'error', 'react/no-unused-state': 'error', 'react/button-has-type': 'error', 'react/prop-types': 'off', 'react/self-closing-comp': 'error', 'react/function-component-definition': 'error', 'react/no-redundant-should-component-update': 'error', 'react/no-unsafe': 'error', 'react/jsx-no-constructed-context-values': 'error', 'react/jsx-pascal-case': 'error', 'react/jsx-curly-brace-presence': [ 'error', { props: 'never', children: 'never', propElementValues: 'always' }, ], 'react/iframe-missing-sandbox': 'error', 'react/jsx-key': 'off', 'react/forbid-component-props': [ 'error', { forbid: [ 'onMouseEnter', 'onMouseLeave', 'onMouseOver', 'onMouseOut', 'onTouchStart', 'onTouchEnd', 'onTouchCancel', ], }, ], '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: false, }, ], '@typescript-eslint/ban-types': 'off', '@typescript-eslint/explicit-member-accessibility': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/method-signature-style': 'error', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-use-before-define': ['error', { functions: false }], '@typescript-eslint/no-parameter-properties': 'off', '@typescript-eslint/no-extraneous-class': 'error', '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-unnecessary-type-constraint': 'error', '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', '@typescript-eslint/no-unnecessary-qualifier': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/no-unnecessary-type-arguments': 'error', '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-includes': 'error', '@typescript-eslint/prefer-string-starts-ends-with': 'error', '@typescript-eslint/prefer-ts-expect-error': 'error', '@typescript-eslint/prefer-regexp-exec': 'off', '@typescript-eslint/array-type': 'error', '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', '@typescript-eslint/unified-signatures': 'error', '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', '@typescript-eslint/switch-exhaustiveness-check': [ 'error', { considerDefaultExhaustiveForUnions: true }, ], '@typescript-eslint/consistent-type-definitions': 'error', '@typescript-eslint/consistent-generic-constructors': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', '@typescript-eslint/only-throw-error': 'error', '@typescript-eslint/no-unused-vars': [ 'error', { varsIgnorePattern: '^_.', argsIgnorePattern: '^_.', ignoreRestSiblings: true, }, ], '@typescript-eslint/no-unused-expressions': [ 'error', { allowShortCircuit: true, allowTernary: true }, ], '@typescript-eslint/no-for-in-array': 'error', '@typescript-eslint/consistent-indexed-object-style': 'off', '@typescript-eslint/no-floating-promises': 'off', '@typescript-eslint/require-await': 'off', '@typescript-eslint/no-unsafe-enum-comparison': 'off', '@typescript-eslint/prefer-nullish-coalescing': [ 'off', { ignoreConditionalTests: true, ignoreTernaryTests: false, ignoreMixedLogicalExpressions: true, ignorePrimitives: { boolean: true, number: false, string: true, }, }, ], '@typescript-eslint/no-unsafe-argument': 'error', '@typescript-eslint/no-unsafe-assignment': 'error', '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-redundant-type-constituents': 'off', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', 'array-func/prefer-array-from': 'off', 'react/no-unused-prop-types': 'off', 'css-modules/no-undef-class': 'off', 'sonarjs/cognitive-complexity': 'off', 'sonarjs/no-small-switch': 'off', 'sonarjs/no-duplicate-string': 'off', 'sonarjs/prefer-immediate-return': 'off', 'sonarjs/no-nested-switch': 'off', 'sonarjs/no-nested-template-literals': 'off', '@eslint-react/no-array-index-key': 'off', '@eslint-react/no-unstable-default-props': 'off', '@eslint-react/dom/no-dangerously-set-innerhtml': 'off', '@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 'off', '@eslint-react/hooks-extra/no-direct-set-state-in-use-layout-effect': 'off', '@eslint-react/prefer-read-only-props': 'off', '@eslint-react/naming-convention/ref-name': 'off', '@eslint-react/purity': 'off', '@eslint-react/set-state-in-effect': 'off', '@eslint-react/use-state': 'off', '@eslint-react/component-hook-factories': 'off', // These are redundant with react-hooks rules '@eslint-react/rules-of-hooks': 'off', '@eslint-react/exhaustive-deps': 'off', // This is busted right now '@eslint-react/naming-convention/use-state': 'off', }, }, { files: ['src/**/*.cjs'], rules: { '@typescript-eslint/no-require-imports': 'off', }, }, { // The process-worker needs to be extra fast, so we disable a few rules here. files: ['src/app/loadout-builder/process-worker/**/*.ts'], rules: { '@typescript-eslint/prefer-for-of': 'off', }, }, { name: 'tests', files: ['**/*.test.ts', 'src/testing/**/*.ts'], rules: { // We don't want to allow importing test modules in app modules, but of course you can do it in other test modules. 'no-restricted-imports': 'off', }, }, ); ================================================ FILE: i18next-scanner.config.cjs ================================================ const fs = require('fs'); const path = require('path'); const typescript = require('typescript'); module.exports = { input: ['src/app/**/*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}', 'src/browsercheck.js'], output: './', options: { debug: false, removeUnusedKeys: true, sort: true, func: { extensions: ['.js', '.jsx', '.ts', '.tsx'], }, lngs: ['en'], ns: ['translation'], defaultLng: 'en', resource: { loadPath: 'config/i18n.json', savePath: 'src/locale/en.json', jsonIndent: 2, lineEnding: '\n', }, context: true, contextFallback: true, contextDefaultValues: ['male', 'female'], allowDynamicKeys: true, }, transform: function customTransform(file, enc, done) { 'use strict'; const tsExts = ['.ts', '.tsx']; const parser = this.parser; const { base, ext } = path.parse(file.path); let content = fs.readFileSync(file.path, enc); const isTs = tsExts.includes(ext) && !base.includes('.d.ts'); if (isTs) { const { outputText } = typescript.transpileModule(content, { compilerOptions: { target: 'es2018', jsx: 'preserve', }, fileName: path.basename(file.path), }); content = outputText; } // prettier-ignore const contexts = { compact: ['compact'], max: ['Max'], }; // prettier-ignore const keys = { buckets: { list: ['General', 'Inventory', 'Postmaster', 'Progress', 'Unknown'] }, difficulty: { list: ['Normal', 'Hard'] }, progress: { list: ['Bounties', 'Items', 'Quests'] }, sockets: { list: ['Mod', 'Ability', 'Shader', 'Ornament', 'Fragment', 'Aspect', 'Projection', 'Transmat', 'Super'] } }; const dimTransformer = (key, options) => { if (options.metadata?.context) { // Add context based on metadata delete options.context; const context = contexts[options.metadata?.context]; parser.set(key, options); for (let i = 0; i < context?.length; i++) { parser.set(`${key}${parser.options.contextSeparator}${context[i]}`, options); } } if (options.metadata?.keys) { // Add keys based on metadata (dynamic or otherwise) const list = keys[options.metadata?.keys].list; for (let i = 0; i < list?.length; i++) { parser.set(`${key}${list[i]}`, options); } } // Add all other non-metadata related keys w/ default options if (!options.metadata) { parser.set(key, options); } }; parser.parseFuncFromString(content, { list: ['t', 'tl', 'DimError'] }, dimTransformer); done(); }, }; ================================================ FILE: icons/build_icons.cjs ================================================ #!/usr/bin/env node const { execSync } = require('child_process'); const rimraf = require('rimraf'); const fs = require('fs'); const splash = require('./splash.json'); const CACHEBREAKER = '6-2018'; // Generate all our icon images from SVG. Requires a mac (or a system w/ a shell and rsvg-convert installed). execSync('which rsvg-convert || brew install librsvg'); for (const VERSION of ['release', 'beta', 'dev', 'pr']) { rimraf.sync(`./${VERSION}`); fs.mkdirSync(VERSION); for (const size of [16, 32, 96, 48]) { execSync( `rsvg-convert -w ${size} -h ${size} -o "${VERSION}/favicon-${size}x${size}.png" "favicon-${VERSION}.svg"`, ); } const color = { release: '#ee6d0d', beta: '#5bb1ce', dev: '#172025', pr: '#FF64E7', }[VERSION]; execSync( `rsvg-convert -w 180 -h 180 -o "${VERSION}/apple-touch-icon.png" "apple-touch-icon-${VERSION}.svg"`, ); execSync( `rsvg-convert -w 180 -h 180 -o "${VERSION}/apple-touch-icon-${CACHEBREAKER}.png" "apple-touch-icon-${VERSION}.svg"`, ); execSync( `rsvg-convert -w 192 -h 192 -o "${VERSION}/android-chrome-192x192-${CACHEBREAKER}.png" "android-icon-${VERSION}.svg"`, ); execSync( `rsvg-convert -w 512 -h 512 -o "${VERSION}/android-chrome-512x512-${CACHEBREAKER}.png" "android-icon-${VERSION}.svg"`, ); execSync( `rsvg-convert -w 512 -h 512 -b "${color}" -o "${VERSION}/android-chrome-mask-512x512-${CACHEBREAKER}.png" "android-icon-${VERSION}.svg"`, ); execSync( `convert ${VERSION}/favicon-48x48.png -define icon:auto-resize=48,32,16 ${VERSION}/favicon.ico`, ); rimraf.sync(`${VERSION}/favicon-48x48.png`); } rimraf.sync('splash'); fs.mkdirSync('splash'); // Generate all splash screens for (const [_a, _b, _c, _d, w, h] of splash) { execSync(`rsvg-convert -w ${w} -h ${h} -a -o "splash/splash-${w}x${h}.png" "splash.svg"`); execSync( `convert splash/splash-${w}x${h}.png -background "#313233" -gravity center -extent ${w}x${h} splash/splash-${w}x${h}.png`, ); } ================================================ FILE: icons/splash.json ================================================ [ [320, 568, 2, "landscape", 1136, 640], [375, 812, 3, "landscape", 2436, 1125], [414, 896, 2, "landscape", 1792, 828], [414, 896, 2, "portrait", 828, 1792], [375, 667, 2, "landscape", 1334, 750], [414, 896, 3, "portrait", 1242, 2688], [414, 736, 3, "landscape", 2208, 1242], [375, 812, 3, "portrait", 1125, 2436], [414, 736, 3, "portrait", 1242, 2208], [1024, 1366, 2, "landscape", 2732, 2048], [414, 896, 3, "landscape", 2688, 1242], [834, 1112, 2, "landscape", 2224, 1668], [375, 667, 2, "portrait", 750, 1334], [1024, 1366, 2, "portrait", 2048, 2732], [834, 1194, 2, "landscape", 2388, 1668], [834, 1112, 2, "portrait", 1668, 2224], [320, 568, 2, "portrait", 640, 1136], [834, 1194, 2, "portrait", 1668, 2388], [768, 1024, 2, "landscape", 2048, 1536], [768, 1024, 2, "portrait", 1536, 2048] ] ================================================ FILE: jest.config.js ================================================ import { pathsToModuleNameMapper } from 'ts-jest'; import tsconfig from './tsconfig.json' with { type: 'json' }; const tsconfigPaths = { ...tsconfig.compilerOptions.paths }; delete tsconfigPaths['*']; export default { testEnvironment: 'jsdom', reporters: ['default', 'jest-junit'], verbose: true, testTimeout: 60000, roots: [''], modulePaths: tsconfig.compilerOptions.baseUrl ? [tsconfig.compilerOptions.baseUrl] : [], moduleNameMapper: { '\\.(jpg|jpeg|a?png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)(\\?react)?$': '/src/__mocks__/fileMock.js', // Automatically include paths from tsconfig ...pathsToModuleNameMapper(tsconfigPaths, { prefix: '/' }), '^.+\\.s?css$': 'identity-obj-proxy', 'Library\\.mjs$': 'identity-obj-proxy', }, setupFiles: ['./src/testing/jest-setup.cjs'], // Babel transform is required to handle some es modules? transformIgnorePatterns: [ 'node_modules/.pnpm/(?!bungie-api-ts|@destinyitemmanager|@popper|@react-hook)', ], globals: { $BROWSERS: [], $DIM_FLAVOR: 'test', $DIM_WEB_API_KEY: 'xxx', $DIM_API_KEY: 'xxx', $DIM_VERSION: '1.0.0', $featureFlags: { dimApi: true, runLoInBackground: true, sentry: false, }, }, }; ================================================ FILE: package.json ================================================ { "name": "dim", "version": "8.122.0", "description": "An item manager for Destiny.", "main": "app/index.html", "private": true, "engines": { "node": ">=18", "pnpm": ">=8" }, "type": "module", "scripts": { "test": "jest -i src/testing/precache-manifest.test.ts && jest --verbose --testPathIgnorePatterns=precache-manifest.test.ts", "lint": "pnpm run '/^lint:.*/'", "lint:eslint": "eslint src", "lint:prettier": "prettier \"src/**/*.{js,ts,tsx,cjs,mjs,cts,mts,scss}\" --check", "lint:stylelint": "stylelint \"src/**/*.scss\"", "lintcached": "pnpm run '/^lintcached:.*/'", "lintcached:eslint": "pnpm lint:eslint --cache --cache-location .eslintcache --cache-strategy content", "lint-report": "pnpm lint:eslint --output-file eslint_report.json --format json", "lint-report:cached": "pnpm lintcached:eslint --output-file eslint.results.json --format json", "typecheck": "tsc --noEmit", "fix": "pnpm run --sequential '/^fix:.*/'", "fix:eslint": "pnpm lint:eslint --fix", "fix:json": "jsonsort src/locale", "fix:stylelint": "pnpm lint:stylelint --fix", "fix:prettier": "prettier \"src/**/*.{js,ts,tsx,cjs,mjs,cts,mts,scss}\" --write", "bundle": "rspack --config ./config/webpack.ts", "build:release": "pnpm -s bundle --env=release --node-env=production", "build:beta": "pnpm -s bundle --env=beta --node-env=production", "build:pr": "pnpm -s bundle --env=pr --node-env=production", "build:dev": "pnpm -s bundle --env=dev", "i18n": "i18next-scanner --config ./i18next-scanner.config.cjs", "start": "pnpm run --reporter-hide-prefix --stream '/^(watch:i18n|devserver)$/'", "devserver": "nodemon --delay 2 --watch config/webpack.ts --watch config/feature-flags.ts --watch config/content-security-policy.ts --watch pnpm-lock.yaml -e ts,js,json,lock --exec \"pnpm install --frozen-lockfile --prefer-offline && rspack serve --config ./config/webpack.ts --env=dev || pnpm touch\"", "touch": "node -e \"require('fs').utimes('config/webpack.ts', new Date(), new Date(), () => {})\"", "watch:i18n": "nodemon --watch config/i18n.json --exec \"pnpm i18n\"", "syntax": "find dist -name \"*.js\" | xargs -n 1 node --check", "install": "git submodule update --init --recursive", "dead-code": "ts-unused-exports tsconfig.json --allowUnusedTypes --allowUnusedEnums --showLineNumber | grep -v cssExports", "bcu": "node src/build-browsercheck-utils.js && prettier --write src/browsercheck-utils.js", "fa-subset": "node src/fa-subset.js", "prepare": "husky", "eslint-inspect": "pnpm dlx @eslint/config-inspector", "deps:stale": "pnpm outdated --long", "deps:update": "pnpm up -i --latest", "deps:pnpm": "corepack use 'pnpm@*'" }, "jest-junit": { "outputName": "junit.xml", "ancestorSeparator": " › ", "uniqueOutputName": "false", "suiteNameTemplate": "{filepath}", "classNameTemplate": "{classname}", "titleTemplate": "{title}" }, "repository": { "type": "git", "url": "https://github.com/DestinyItemManager/DIM.git" }, "author": "", "license": "MIT", "bugs": { "url": "https://github.com/DestinyItemManager/DIM/issues" }, "homepage": "https://destinyitemmanager.com", "browserslist": [ "Chrome >= 126", "Edge >= 126", "last 2 ChromeAndroid versions", "last 2 FirefoxAndroid versions", "last 2 Firefox versions", "Firefox ESR", "last 2 Safari versions", "iOS >= 16.4", "last 2 Opera versions" ], "resolutions": { "@types/react": "$@types/react", "react-redux": "$react-redux", "redux": "$redux", "react-is": "^18.3.1" }, "devDependencies": { "@aaroon/workbox-rspack-plugin": "^0.3.3", "@babel/core": "^7.28.6", "@babel/plugin-transform-react-constant-elements": "^7.27.1", "@babel/plugin-transform-react-inline-elements": "^7.27.1", "@babel/plugin-transform-runtime": "^7.28.5", "@babel/plugin-transform-typescript": "^7.28.6", "@babel/preset-env": "^7.28.6", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@babel/register": "^7.28.6", "@bhollis/css-modules-typescript-loader": "^1.0.1", "@bhollis/eslint-plugin-css-modules": "^1.0.1", "@bundle-stats/plugin-webpack-filter": "^4.21.10", "@eslint-react/eslint-plugin": "^3.0.0", "@eslint/compat": "^2.0.1", "@eslint/js": "^10.0.1", "@rspack/cli": "^1.7.7", "@rspack/core": "^1.7.7", "@rspack/plugin-react-refresh": "^1.6.1", "@svgr/webpack": "^8.1.0", "@testing-library/react": "^16.3.2", "@types/dom-chromium-installation-events": "^101.0.4", "@types/dom-screen-wake-lock": "^1.0.3", "@types/generate-json-webpack-plugin": "^0.3.8", "@types/jest": "^30.0.0", "@types/node": "22.0.2", "@types/papaparse": "^5.5.2", "@types/prop-types": "^15.7.15", "@types/react": "19.2.14", "@types/react-dom": "^19.2.3", "@types/ua-parser-js": "^0.7.39", "@types/use-subscription": "^1.0.2", "@types/webpack": "^5.28.5", "@types/webpack-env": "^1.18.8", "@types/webpack-stats-plugin": "^0.3.5", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "babel-jest": "^30.2.0", "babel-loader": "^10.0.0", "babel-plugin-object-to-json-parse": "^0.2.3", "babel-plugin-optimize-clsx": "^2.6.2", "browserslist": "^4.28.1", "chalk": "^5.6.2", "clean-webpack-plugin": "^4.0.0", "compression-webpack-plugin": "^12.0.0", "content-security-policy-builder": "^2.3.0", "css-loader": "^7.1.4", "eslint": "^10.1.0", "eslint-plugin-array-func": "^5.0.2", "eslint-plugin-github": "^6.0.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-redux": "^4.2.2", "eslint-plugin-regexp": "^3.0.0", "eslint-plugin-sonarjs": "^1.0.4", "fontawesome-subset": "^4.6.0", "generate-json-webpack-plugin": "^2.0.0", "globals": "^17.4.0", "html-loader": "^5.1.0", "html-webpack-plugin": "^5.6.6", "husky": "^9.1.7", "i18next-scanner": "^4.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-environment-jsdom": "^30.2.0", "jest-fetch-mock": "^3.0.3", "jest-junit": "^16.0.0", "json-sort-cli": "^4.1.3", "lint-staged": "^16.3.2", "markdown-loader": "^8.0.0", "marked": "^17.0.4", "mini-svg-data-uri": "^1.4.4", "mkcert": "^3.2.0", "nodemon": "^3.1.14", "prettier": "^3.8.1", "prettier-plugin-organize-imports": "^4.3.0", "react-refresh": "^0.18.0", "sass": "^1.97.3", "sass-loader": "^16.0.7", "sonda": "^0.11.1", "source-map-loader": "^5.0.0", "style-loader": "^4.0.0", "stylelint": "^17.4.0", "stylelint-config-css-modules": "^4.6.0", "stylelint-config-standard-scss": "^17.0.0", "stylelint-order": "^8.1.1", "svgo": "^4.0.1", "svgo-loader": "^4.0.0", "ts-checker-rspack-plugin": "^1.3.0", "ts-jest": "^29.4.6", "ts-loader": "^9.5.4", "ts-unused-exports": "^11.0.1", "typescript": "^6.0.2", "typescript-eslint": "^8.56.1", "webpack-stats-plugin": "^1.1.3" }, "dependencies": { "@babel/runtime": "^7.28.6", "@beyond-js/md5": "^0.0.1", "@destinyitemmanager/dim-api-types": "^1.40.0", "@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/react-fontawesome": "^3.2.0", "@popperjs/core": "^2.11.8", "@react-hook/resize-observer": "^2.0.2", "@sentry/browser": "^10.42.0", "@sentry/react": "^10.42.0", "@sentry/types": "^10.42.0", "@tanstack/react-virtual": "^3.13.19", "@textcomplete/core": "^0.1.13", "@textcomplete/textarea": "^0.1.13", "@textcomplete/utils": "^0.1.13", "bungie-api-ts": "^5.10.0", "caniuse-lite": "^1.0.30001776", "clsx": "^2.1.1", "comlink": "^4.4.2", "core-js": "^3.48.0", "dnd-core": "^16.0.1", "downshift": "^9.3.2", "es-toolkit": "^1.45.1", "fast-equals": "^6.0.0", "i18next": "^25.8.14", "i18next-http-backend": "^3.0.5", "immer": "^11.1.4", "memoize-one": "^6.0.0", "motion": "^12.35.0", "papaparse": "^5.5.3", "react": "^19.2.4", "react-aria": "^3.47.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dnd-multi-backend": "^9.0.0", "react-dnd-touch-backend": "^16.0.1", "react-dom": "^19.2.4", "react-dropzone": "^15.0.0", "react-redux": "^9.2.0", "react-router": "^7.13.1", "react-textarea-autosize": "^8.5.9", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.1", "typesafe-actions": "^5.1.0", "ua-parser-js": "^1.0.39", "use-subscription": "^1.12.0", "workbox-cacheable-response": "^7.4.0", "workbox-core": "^7.4.0", "workbox-expiration": "^7.4.0", "workbox-precaching": "^7.4.0", "workbox-routing": "^7.4.0", "workbox-strategies": "^7.4.0" }, "lint-staged": { "*.{css,scss}": "stylelint --fix", "*.{js,jsx,ts,tsx,cjs,mjs,cts,mts,css,scss,md}": "prettier --write" }, "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264", "pnpm": { "peerDependencyRules": { "allowedVersions": { "@typescript-eslint/parser": "^6.2.0", "stylelint": "^16.0.2" } }, "allowedDeprecatedVersions": { "@fortawesome/fontawesome-svg-core": "*" }, "updateConfig": { "ignoreDependencies": [ "@fortawesome/fontawesome-free", "@fortawesome/fontawesome-svg-core", "eslint-plugin-sonarjs", "ua-parser-js" ] }, "onlyBuiltDependencies": [ "@parcel/watcher", "unrs-resolver" ], "ignoredBuiltDependencies": [ "@fortawesome/fontawesome-common-types", "@fortawesome/fontawesome-free", "@fortawesome/fontawesome-svg-core", "@parcel/watcher", "core-js", "unrs-resolver" ] } } ================================================ FILE: src/404.html ================================================ Not Found

Not Found

That URL doesn't go anywhere

================================================ FILE: src/@types/i18next.d.ts ================================================ // import the original type declarations import type en from 'config/i18n.json' with { type: 'json' }; import 'i18next'; declare module 'i18next' { interface CustomTypeOptions { defaultNS: 'translation'; resources: { translation: typeof en }; } } ================================================ FILE: src/Index.tsx ================================================ // organize-imports-ignore // We want our main CSS to load before all other CSS. import './app/main.scss'; // Pull the sheet CSS up so it is at the top of the stylesheet and can be easily overridden. import './app/dim-ui/Sheet.m.scss'; import './app/utils/sentry'; import { createSaveAccountsObserver } from 'app/accounts/observers'; import { createItemSizeObserver, createOrnamentDisplayObserver, createThemeObserver, createTilesPerCharColumnObserver, setCssVariableEventListeners, } from 'app/css-variables'; import { loadDimApiData } from 'app/dim-api/actions'; import { createSaveItemInfosObserver } from 'app/inventory/observers'; import store from 'app/store/store'; import { lazyLoadStreamDeck, startStreamDeckConnection } from 'app/stream-deck/stream-deck'; import { infoLog } from 'app/utils/log'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { StorageBroken, storageTest } from './StorageTest'; import Root from './app/Root'; import setupRateLimiter from './app/bungie-api/rate-limit-config'; import { initGoogleAnalytics } from './app/google'; import { createLanguageObserver, initi18n } from './app/i18n'; import registerServiceWorker from './app/register-service-worker'; import { safariTouchFix } from './app/safari-touch-fix'; import { createWishlistObserver } from './app/wishlists/observers'; import { observe } from 'app/store/observerMiddleware'; infoLog( 'app', `DIM v${$DIM_VERSION} (${$DIM_FLAVOR}) - Please report any errors to https://www.github.com/DestinyItemManager/DIM/issues`, ); initGoogleAnalytics(); safariTouchFix(); if ($DIM_FLAVOR !== 'dev') { registerServiceWorker(); } setupRateLimiter(); const i18nPromise = initi18n(); (async () => { const root = createRoot(document.getElementById('app')!); // Block on testing that we can use LocalStorage and IDB, before everything starts trying to use it const storageWorks = await storageTest(); if (!storageWorks) { // Make sure localization is loaded await i18nPromise; root.render( , ); return; } if ($featureFlags.wishLists) { store.dispatch(observe(createWishlistObserver())); } store.dispatch(observe(createSaveAccountsObserver())); store.dispatch(observe(createItemSizeObserver())); store.dispatch(observe(createOrnamentDisplayObserver())); store.dispatch(observe(createThemeObserver())); store.dispatch(observe(createTilesPerCharColumnObserver())); setCssVariableEventListeners(); store.dispatch(observe(createSaveItemInfosObserver())); store.dispatch(loadDimApiData()); if ($featureFlags.elgatoStreamDeck && store.getState().streamDeck.enabled) { await lazyLoadStreamDeck(); store.dispatch(startStreamDeckConnection()); } // Make sure localization is loaded await i18nPromise; // Update the language in both i18n and local storage when the user // changes the language setting. store.dispatch(observe(createLanguageObserver())); root.render(); })(); ================================================ FILE: src/StorageTest.tsx ================================================ import { t } from 'app/i18next-t'; import ErrorPanel from 'app/shell/ErrorPanel'; import { deleteDatabase, get, set } from 'app/storage/idb-keyval'; import { errorLog } from 'app/utils/log'; import { reportException } from 'app/utils/sentry'; const TAG = 'storage'; export function StorageBroken() { return (
); } export async function storageTest() { try { localStorage.setItem('test', 'true'); } catch (e) { errorLog(TAG, 'Failed localStorage Test', e); return false; } if (!window.indexedDB) { errorLog(TAG, 'IndexedDB not available'); return false; } try { await set('idb-test', true); } catch (e) { errorLog(TAG, 'Failed IndexedDB Set Test - trying to delete database', e); try { await deleteDatabase(); await set('idb-test', true); // Report to sentry, I want to know if this ever works reportException('deleting database fixed IDB set', e); } catch (e2) { errorLog(TAG, 'Failed IndexedDB Set Test - deleting database did not help', e2); } reportException('Failed IndexedDB Set Test', e); return false; } try { const idbValue = await get('idb-test'); return idbValue; } catch (e) { errorLog(TAG, 'Failed IndexedDB Get Test - trying to delete database', e); try { await deleteDatabase(); const idbValue = await get('idb-test'); if (idbValue) { // Report to sentry, I want to know if this ever works reportException('deleting database fixed IDB get', e); } return idbValue; } catch (e2) { errorLog(TAG, 'Failed IndexedDB Get Test - deleting database did not help', e2); } reportException('Failed IndexedDB Get Test', e); return false; } } ================================================ FILE: src/__mocks__/fileMock.js ================================================ module.exports = 'test-file-stub'; ================================================ FILE: src/app/App.m.scss ================================================ @use 'variables.scss' as *; .app { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); @include phone-portrait { --inventory-column-padding: 12px; } @media (min-width: 1440px) { --inventory-column-padding: 24px; --column-padding: calc(2 * var(--inventory-column-padding) - var(--item-margin)); } &::before { background: var(--theme-app-bg); background-position: center top; background-repeat: no-repeat; content: ''; left: 0; right: 0; bottom: 0; position: fixed; will-change: transform; z-index: -1; @include below-header; } } ================================================ FILE: src/app/App.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'app': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/App.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { RootState } from 'app/store/types'; import clsx from 'clsx'; import { Suspense, lazy } from 'react'; import { useSelector } from 'react-redux'; import { Navigate, Route, Routes, useLocation } from 'react-router'; import * as styles from './App.m.scss'; import Developer from './developer/Developer'; import AutoRefresh from './dim-ui/AutoRefresh'; import ClickOutsideRoot from './dim-ui/ClickOutsideRoot'; import ErrorBoundary from './dim-ui/ErrorBoundary'; import PageLoading from './dim-ui/PageLoading'; import ShowPageLoading from './dim-ui/ShowPageLoading'; import HotkeysCheatSheet from './hotkeys/HotkeysCheatSheet'; import { t } from './i18next-t'; import Login from './login/Login'; import NotificationsContainer from './notifications/NotificationsContainer'; import DefaultAccount from './shell/DefaultAccount'; import Destiny from './shell/Destiny'; import GATracker from './shell/GATracker'; import Header from './shell/Header'; import ScrollToTop from './shell/ScrollToTop'; import SneakyUpdates from './shell/SneakyUpdates'; const WhatsNew = lazy( () => import(/* webpackChunkName: "about-whatsnew-privacy-debug" */ './whats-new/WhatsNew'), ); const SettingsPage = lazy( () => import(/* webpackChunkName: "settings" */ './settings/SettingsPage'), ); const Debug = lazy( () => import(/* webpackChunkName: "about-whatsnew-privacy-debug" */ './debug/Debug'), ); const Privacy = lazy( () => import(/* webpackChunkName: "about-whatsnew-privacy-debug" */ './shell/Privacy'), ); const About = lazy( () => import(/* webpackChunkName: "about-whatsnew-privacy-debug" */ './shell/About'), ); export default function App() { const language = useSelector(settingSelector('language')); const itemQuality = useSelector(settingSelector('itemQuality')); const charColMobile = useSelector(settingSelector('charColMobile')); const needsLogin = useSelector((state: RootState) => state.accounts.needsLogin); const needsDeveloper = useSelector((state: RootState) => state.accounts.needsDeveloper); const { pathname, search } = useLocation(); return (
}> } /> } /> } /> } /> } /> } /> {$DIM_FLAVOR === 'dev' && ( } /> )} {needsLogin ? ( ) : ( ) } /> ) : ( <> } /> } /> } /> )}
); } ================================================ FILE: src/app/Root.tsx ================================================ import { withProfiler } from '@sentry/react'; import { LocationSwitcher } from 'app/shell/LocationSwitcher'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { DndProvider, MultiBackendOptions, PointerTransition, TouchTransition, } from 'react-dnd-multi-backend'; import { TouchBackend } from 'react-dnd-touch-backend'; import { Provider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router'; import App from './App'; import store from './store/store'; import { isNativeDragAndDropSupported } from './utils/browsers'; // Wrap App with Sentry profiling const ProfiledApp = withProfiler(App); function Root() { const options: MultiBackendOptions = { backends: // If we have native DnD then use it. iOS 15+ supports both touch and // native dnd, and without this it'd switch to touch. isNativeDragAndDropSupported() ? [{ id: 'html5', backend: HTML5Backend }] : [ { id: 'html5', backend: HTML5Backend, transition: PointerTransition }, // We can drop this after we only support iOS 15+ and Chrome 108+ { id: 'touch', backend: TouchBackend, options: { enableMouseEvents: true, delayTouchStart: 150 }, preview: true, transition: TouchTransition, }, ], }; return ( ); } export default Root; ================================================ FILE: src/app/_variables.scss ================================================ @use 'sass:color'; // Variables and mixins that can be used from any SCSS file $dim-brand: #e8a534; $red: #ff3232; // For warnings/errors $green: #51a351; // For success statuses or positive deltas $stat-modded: #68a0b7; // For highlighting the difference in modded item stats $stat-masterworked: #e8a534; $new-notification-dot: #cf0707; $upgrade-notification-dot: #e8a534; // Blue used to denote community-provided data $communityBlue: #0d7fad; // Green used to indicate the subject has been acquired $acquiredGreen: #44bd32; // Elemental damage types $arc: #79bbe8; $solar: #f0631e; $void: #8e749e; $stasis: #4d88ff; $strand: #35e366; $prismatic: #e3619b; // Item rarities $common: #dcdcdc; $uncommon: #366e42; $rare: #5076a3; $legendary: #513065; $exotic: #c3a019; // The color of XP bars $xp: #5ea16a; // Border for completed items/quests/bounties $gold: #f5dc56; $power: $gold; // The in-game color for the shaped weapon icon $shaped: #d25336; // Colors for the titles below guardians' names $sealtitle: #e1c2ec; $gildedtitle: #f9deb8; // Color of the item border for masterworks $masterwork-border-color: #eade8b; $deepsight-border-color: $shaped; $gilded-triumph-border-color: #a78355; // Yellow used for enhanced weapon perks $enhancedYellow: #f3cf55; // Item tiles' border $item-border-width: 1px; // Border around equipped items $equipped-item-border: 1px; $equipped-item-padding: 2px; $equipped-item-total-outset: #{2 * ($equipped-item-border + $equipped-item-padding)}; // Full tile size including borders $badge-font-size: '(var(--item-size) / 5)'; $badge-height: '(#{$badge-font-size} + 4px)'; // The height of our "issue banner" for charities/causes $issue-banner-height: 101px; // From ceaser-easing package $easeInCubic: cubic-bezier(0.55, 0.055, 0.675, 0.19); $easeOutCubic: cubic-bezier(0.215, 0.61, 0.355, 1); $easeInOutCubic: cubic-bezier(0.645, 0.045, 0.355, 1); // Theme style properties that remain consistent across all themes // Tooltips and arrows $theme-tooltip-arrow-size: 8px; // Ensure this stays in sync with 'popperArrowSize' in 'usePopper.ts' $theme-tooltip-arrow-size-mini: 5px; $theme-tooltip-corner-radius: 6px; // Search $theme-corner-radius-search: 6px; $search-bar-height: 28px; // Character Tile emblem native size - beyond this the emblem gets distorted $emblem-width: 237px; $emblem-height: 48px; // The z-index for our "temp container" where all temporary items get placed. $tempContainerZindex: 1000; // A mixin that allows targeting styles only when in phone-portrait display mode @mixin phone-portrait { // This seems like a good breakpoint for portrait based on https://material.io/devices/ // We can't use orientation:portrait because Android Chrome messes up when the keyboard is shown: https://www.chromestatus.com/feature/5656077370654720 @media (max-width: 540px) { @content; } } // The opposite of phone-portrait, for when you want to default to mobile styles // and only override on larger screens. @mixin desktop { @media (min-width: 541px) { @content; } } // Position something directly below the header @mixin below-header { top: var(--header-height); } // A header for items or perks that matches the in-game display @mixin destiny-header { text-transform: uppercase; font-weight: 600; font-family: Helvetica, Arial, sans-serif; } @mixin draggable-hover-border { @include desktop { outline: 1px solid var(--theme-item-polaroid-hover-border); } } // Utility functions to allow for augmenting a hex color value with an alpha component. // e.g. // --my-base-color-rgb: #{dim-hex-to-rgb-values(#ff7b00)}; // background-color: dim-rgb-values-to-rgba(var(--my-base-color-rgb)); // border-color: dim-rgb-values-to-rgba(var(--my-base-color-rgb), $alpha: 0.75); // produces // --my-base-color-rgb: 255, 128, 0; // background-color: rgb(255, 128, 0, 1); // border-color: rgb(255, 128, 0, 0.75); // Converts a 6-character hex color into a comma-separated list of RGB values, to use within the 'rgba' function. @function dim-hex-to-rgb-values($hex) { @return color.channel($hex, 'red', $space: rgb), color.channel($hex, 'green', $space: rgb), color.channel($hex, 'blue', $space: rgb); } // Produces an 'rgba' function usage for the specified comma-separated RGB values and alpha. @function dim-rgb-values-to-rgba($values, $alpha: 1) { @return #{'rgba(' + $values + ', ' + $alpha + ')'}; } // Converts a pixel value into a size that scales with the item size setting. // The px value should be in terms of the default item size of 50px. This lets // you design styles with your setting set to the default, but then it'll work // for other sizes. This should be used within calc(). @function dim-item-px($px) { @return #{'(' + $px + ' / 50) * var(--item-size)'}; } @mixin interactive( $hover: false, $focus: false, $focusWithin: false, $active: false, $selectedClass: '' ) { // Declares hover rules only if a device can hover over elements. This avoids issues on mobile // browsers where tapping an element triggers and persists its hover state. @if $hover == true { @media (any-hover: hover) { &:hover { @content; } } } @if $focus == true { &:focus-visible { @content; } } @if $focusWithin == true { &:focus-within { @content; } } @if $active == true { &:active { @content; } } @if $selectedClass != '' { &#{$selectedClass} { @content; } } } ================================================ FILE: src/app/accounts/Account.m.scss ================================================ @use '../variables.scss' as *; .account { cursor: pointer; line-height: 15px; text-align: left; position: relative; font-size: 16px; margin: 1px 0 0 0; white-space: nowrap; display: flex; flex-direction: row; align-items: center; /* Match the "D2" branding */ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; :global(.app-icon) { margin: 0 0 0 4px; &.first { border-left: 1px solid #666; padding-left: 0.25em; margin-left: 0.375em; } } } .selectedAccount { border-left: 4px solid var(--theme-accent-primary); padding-left: calc(1rem - 4px) !important; padding-right: 24px; height: 100%; box-sizing: border-box; } ================================================ FILE: src/app/accounts/Account.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'account': string; 'first': string; 'selectedAccount': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/accounts/Account.tsx ================================================ import { compareBy } from 'app/utils/comparators'; import { BungieMembershipType } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { AppIcon } from '../shell/icons'; import * as styles from './Account.m.scss'; import { DestinyAccount, PLATFORM_ICONS, PLATFORM_LABELS } from './destiny-account'; /** * Accounts that appear in the hamburger menu. */ export default function Account({ account, selected, className, }: { account: DestinyAccount; selected?: boolean; className?: string; }) { return (
Destiny {account.destinyVersion} {account.platforms .filter((p) => account.platforms.length === 1 || p !== BungieMembershipType.TigerStadia) .sort(compareBy((p) => account.originalPlatformType !== p)) .map((platformType, index) => platformType in PLATFORM_ICONS ? ( ) : ( PLATFORM_LABELS[platformType] ), )}
); } ================================================ FILE: src/app/accounts/MenuAccounts.m.scss ================================================ @use '../variables.scss' as *; .accountSelect { color: var(--theme-text); height: auto; flex: 1 0 auto; display: flex; flex-direction: column; justify-content: flex-end; padding-bottom: 1em; a { text-decoration: none; > div { padding: 10px 2rem 10px 1rem; @include interactive($hover: true, $focus: true) { color: var(--theme-text-invert); background-color: var(--theme-accent-primary); } } } } .logout { composes: account from './Account.m.scss'; composes: resetButton from '../dim-ui/common.m.scss'; text-align: left; padding: 10px 2rem 10px 1rem; :global(.app-icon) { margin: 0; } } .accountName { all: initial; font: inherit; font-size: 12px; color: var(--theme-text-secondary); } ================================================ FILE: src/app/accounts/MenuAccounts.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'accountName': string; 'accountSelect': string; 'logout': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/accounts/MenuAccounts.tsx ================================================ import { t } from 'app/i18next-t'; import { accountRoute } from 'app/routes'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { chainComparator, compareBy, reverseComparator } from 'app/utils/comparators'; import React from 'react'; import { useSelector } from 'react-redux'; import { Link, useNavigate } from 'react-router'; import { AppIcon, signOutIcon } from '../shell/icons'; import Account from './Account'; import * as styles from './MenuAccounts.m.scss'; import { logOut } from './platforms'; import { accountsSelector, currentAccountSelector } from './selectors'; /** * The accounts list in the sidebar menu. */ export default function MenuAccounts({ closeDropdown, }: { closeDropdown: (e: React.MouseEvent) => void; }) { const dispatch = useThunkDispatch(); const currentAccount = useSelector(currentAccountSelector); const accounts = useSelector(accountsSelector); const navigate = useNavigate(); const onLogOut = async () => { await dispatch(logOut()); await navigate('/login'); }; const sortedAccounts = accounts.toSorted( chainComparator( reverseComparator(compareBy((a) => a.destinyVersion)), // 2 before 1 reverseComparator(compareBy((a) => a.lastPlayed.getTime())), ), ); const bungieName = sortedAccounts[0]?.displayName; return (

{t('Accounts.Title')} {bungieName}

{sortedAccounts.map((account) => ( ))}
); } ================================================ FILE: src/app/accounts/SelectAccount.m.scss ================================================ @use '../variables.scss' as *; .accountSelect { margin: 0 10px; } .accountList { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; align-items: flex-start; } .account { display: block; line-height: 15px; text-align: left; position: relative; padding: 24px; border: 1px solid white; text-decoration: none; @include interactive($hover: true, $focus: true) { outline: none; border-color: var(--theme-accent-primary); color: var(--theme-accent-primary); background-color: rgb(255, 255, 255, 0.1); } } .accountDetails { font-size: 16px; padding: 0; :global(.app-icon) { margin: 0 0 0 6px; } } ================================================ FILE: src/app/accounts/SelectAccount.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'account': string; 'accountDetails': string; 'accountList': string; 'accountSelect': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/accounts/SelectAccount.tsx ================================================ import { t } from 'app/i18next-t'; import { accountRoute } from 'app/routes'; import { AppIcon, signOutIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { chainComparator, compareBy, reverseComparator } from 'app/utils/comparators'; import { useSelector } from 'react-redux'; import { Link, useNavigate } from 'react-router'; import Account from './Account'; import * as styles from './SelectAccount.m.scss'; import { logOut } from './platforms'; import { accountsSelector } from './selectors'; /** * The large "select accounts" page shown when the user has not yet selected an account. */ export default function SelectAccount({ path }: { path?: string }) { const accounts = useSelector(accountsSelector); const sortedAccounts = accounts.toSorted( chainComparator( reverseComparator(compareBy((a) => a.destinyVersion)), // 2 before 1 reverseComparator(compareBy((a) => a.lastPlayed.getTime())), ), ); const bungieName = sortedAccounts[0].displayName; const dispatch = useThunkDispatch(); const navigate = useNavigate(); const onLogOut = async () => { await dispatch(logOut()); await navigate('/login'); }; return (

{t('Accounts.Choose', { bungieName })}

{sortedAccounts.map((account) => ( ))}

{t('Accounts.MissingAccountWarning')} {t('Accounts.SwitchAccounts')}

  {t('Settings.LogOut')}
); } ================================================ FILE: src/app/accounts/actions.ts ================================================ import { FatalTokenError } from 'app/bungie-api/authenticated-fetch'; import { ThunkResult } from 'app/store/types'; import { DimError } from 'app/utils/dim-error'; import { createAction } from 'typesafe-actions'; import { DestinyAccount } from './destiny-account'; export const accountsLoaded = createAction('accounts/ACCOUNTS_LOADED')(); export const setCurrentAccount = createAction('accounts/SET_CURRENT_ACCOUNT')(); export const loadFromIDB = createAction('accounts/LOAD_FROM_IDB')(); export const error = createAction('accounts/ERROR')(); export const loggedOut = createAction('accounts/LOG_OUT')(); export const needsDeveloper = createAction('accounts/DEV_INFO_NEEDED')(); /** * Inspect an error and potentially log out the user or send them to the developer page */ export function handleAuthErrors(e: unknown): ThunkResult { return async (dispatch) => { // This means we don't have an API key or the API key is wrong if ($DIM_FLAVOR === 'dev' && e instanceof DimError && e.code === 'BungieService.DevVersion') { dispatch(needsDeveloper()); } else if ( e instanceof Error && (e instanceof FatalTokenError || (e instanceof DimError && (e.code === 'BungieService.NotLoggedIn' || e.cause instanceof FatalTokenError))) ) { dispatch(loggedOut()); } }; } ================================================ FILE: src/app/accounts/bungie-account.ts ================================================ import { getToken } from '../bungie-api/oauth-tokens'; /** * A Bungie account is an account on Bungie.net, which is associated * with one or more Destiny accounts. */ export interface BungieAccount { /** Bungie.net membership ID */ membershipId: string; } /** * Get the Bungie accounts for this DIM user. For now, we only have one (or none if you're not logged in). * * A DIM user may associate one or more Bungie.net accounts with their * DIM account. These accounts are identified with a membership ID, * and have references to one or more Destiny accounts. */ export function getBungieAccount(): BungieAccount | undefined { const token = getToken(); if (token?.bungieMembershipId) { return { membershipId: token.bungieMembershipId, }; } } ================================================ FILE: src/app/accounts/destiny-account.test.ts ================================================ import { Tokens } from 'app/bungie-api/oauth-tokens'; import { BungieMembershipType, DestinyLinkedProfilesResponse, PlatformErrorCodes, } from 'bungie-api-ts/destiny2'; import { UserInfoCard } from 'bungie-api-ts/user'; import d1Profile from 'testing/data/d1profiles-2022-10-24.json'; import linkedAccounts from 'testing/data/linkedaccounts-2025-07-15.json'; import { generatePlatforms } from './destiny-account'; jest.mock('app/bungie-api/oauth-tokens', () => ({ getToken: (): Tokens => ({ accessToken: { value: 'foo' }, }) as Tokens, hasTokenExpired: () => false, })); // This relies on knowing what the accounts that go with the linkedaccounts data are beforeEach(() => { // One D1 account exists fetchMock.mockIf( 'https://www.bungie.net/D1/Platform/Destiny/2/Account/4611686018433092312/', JSON.stringify(d1Profile), ); // One doesn't fetchMock.mockIf( 'https://www.bungie.net/D1/Platform/Destiny/1/Account/4611686018429726245/', '{"ErrorCode":1601,"ThrottleSeconds":0,"ErrorStatus":"DestinyAccountNotFound","Message":"We were unable to find your Destiny account information. If you have a valid Destiny Account, let us know.","MessageData":{}}', ); }); describe('generatePlatforms', () => { it('gets one D2 account and one D1 account', async () => { const platforms = await generatePlatforms( linkedAccounts as unknown as DestinyLinkedProfilesResponse, ); expect(platforms.length).toBe(2); const d2account = platforms.find((platform) => platform.destinyVersion === 2)!; const d1account = platforms.find((platform) => platform.destinyVersion === 1)!; expect(d2account).not.toBeUndefined(); expect(d1account).not.toBeUndefined(); expect(d2account.displayName).toBe('VidBoi#9226'); expect(d1account.displayName).toBe('VidBoi#9226'); expect(d2account.originalPlatformType).toBe(BungieMembershipType.TigerPsn); expect(d1account.originalPlatformType).toBe(BungieMembershipType.TigerPsn); expect(d2account.platforms.length).toBeGreaterThan(1); expect(d2account.lastPlayed.getTime()).toBeGreaterThan(d1account.lastPlayed.getTime()); expect(d2account.lastPlayed).not.toBe(0); expect(d1account.lastPlayed).not.toBe(0); }); it('handles when D2 accounts are in profilesWithErrors and error code DestinyUnexpectedError', async () => { const originalAccounts = linkedAccounts as unknown as DestinyLinkedProfilesResponse; const errorAccounts: DestinyLinkedProfilesResponse = { ...originalAccounts, profiles: [], profilesWithErrors: [ ...originalAccounts.profilesWithErrors, ...originalAccounts.profiles.map((p) => ({ errorCode: PlatformErrorCodes.DestinyUnexpectedError, infoCard: p as unknown as UserInfoCard, })), ], }; const platforms = await generatePlatforms(errorAccounts); expect(platforms.length).toBe(2); const d2account = platforms.find((platform) => platform.destinyVersion === 2)!; const d1account = platforms.find((platform) => platform.destinyVersion === 1)!; expect(d2account).not.toBeUndefined(); expect(d1account).not.toBeUndefined(); expect(d2account.displayName).toBe('VidBoi#9226'); expect(d1account.displayName).toBe('VidBoi#9226'); expect(d2account.originalPlatformType).toBe(BungieMembershipType.TigerPsn); expect(d1account.originalPlatformType).toBe(BungieMembershipType.TigerPsn); expect(d2account.platforms.length).toBeGreaterThan(1); // No use checking the dates, they'll be wrong }); it('does not return D2 account when they are in profilesWithErrors and error code DestinyAccountNotFound', async () => { const originalAccounts = linkedAccounts as unknown as DestinyLinkedProfilesResponse; const errorAccounts: DestinyLinkedProfilesResponse = { ...originalAccounts, profiles: [], profilesWithErrors: [ ...originalAccounts.profilesWithErrors, ...originalAccounts.profiles.map((p) => ({ errorCode: PlatformErrorCodes.DestinyAccountNotFound, infoCard: p as unknown as UserInfoCard, })), ], }; const platforms = await generatePlatforms(errorAccounts); expect(platforms.length).toBe(1); const d1account = platforms.find((platform) => platform.destinyVersion === 1)!; expect(d1account).not.toBeUndefined(); expect(d1account.displayName).toBe('VidBoi#9226'); }); }); ================================================ FILE: src/app/accounts/destiny-account.ts ================================================ import { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; import { D1Character } from 'app/destiny1/d1-manifest-types'; import { t } from 'app/i18next-t'; import { epicIcon, faPlayStation, faSteam, faXbox } from 'app/shell/icons'; import { ThunkResult } from 'app/store/types'; import { compact } from 'app/utils/collections'; import { DimError } from 'app/utils/dim-error'; import { errorLog } from 'app/utils/log'; import { LookupTable } from 'app/utils/util-types'; import { BungieMembershipType, DestinyLinkedProfilesResponse, DestinyProfileUserInfoCard, PlatformErrorCodes, } from 'bungie-api-ts/destiny2'; import { UserInfoCard } from 'bungie-api-ts/user'; import { getCharacters } from '../bungie-api/destiny1-api'; import { getLinkedAccounts } from '../bungie-api/destiny2-api'; import { removeToken } from '../bungie-api/oauth-tokens'; import { showNotification } from '../notifications/notifications'; import { reportException } from '../utils/sentry'; import { loggedOut } from './actions'; // See https://github.com/Bungie-net/api/wiki/FAQ:-Cross-Save-pre-launch-testing,-and-how-it-may-affect-you for more info /** * Platform types (membership types) in the Bungie API. */ export const PLATFORM_LABELS: Record = { [BungieMembershipType.None]: 'None', [BungieMembershipType.All]: 'All', [BungieMembershipType.TigerXbox]: 'Xbox', [BungieMembershipType.TigerPsn]: 'PlayStation', [BungieMembershipType.TigerBlizzard]: 'Blizzard', [BungieMembershipType.TigerDemon]: 'Demon', [BungieMembershipType.TigerSteam]: 'Steam', [BungieMembershipType.TigerStadia]: 'Stadia', [BungieMembershipType.TigerEgs]: 'Epic', [BungieMembershipType.BungieNext]: 'Bungie.net', [BungieMembershipType.GoliathGame]: 'Marathon', }; export const PLATFORM_ICONS: LookupTable = { [BungieMembershipType.TigerXbox]: faXbox, [BungieMembershipType.TigerPsn]: faPlayStation, [BungieMembershipType.TigerSteam]: faSteam, [BungieMembershipType.TigerEgs]: epicIcon, }; /** A specific Destiny account (one per platform and Destiny version) */ export interface DestinyAccount { /** Bungie Name */ readonly displayName: string; /** The platform type this account started on. It may not be exclusive to this platform anymore, but this is what gets used to call APIs. */ readonly originalPlatformType: BungieMembershipType; /** readable platform name */ readonly platformLabel: string; /** Destiny platform membership ID. */ readonly membershipId: string; /** Which version of Destiny is this account for? */ readonly destinyVersion: DestinyVersion; /** All the platforms this account plays on (post-Cross-Save) */ readonly platforms: BungieMembershipType[]; /** When was this account last used? */ readonly lastPlayed: Date; } /** * Get all Destiny accounts associated with a Bungie account. * * Each Bungie.net account may be linked with one Destiny 1 account * per platform (Xbox, PS4) and one Destiny 2 account per platform (Xbox, PS4, PC). * This account is indexed by a Destiny membership ID and is how we access their characters. * * We don't know whether or not the account is associated with D1 or D2 characters until we * try to load them. * * @param bungieMembershipId Bungie.net membership ID */ export function getDestinyAccountsForBungieAccount( bungieMembershipId: string, ): ThunkResult { return async (dispatch) => { try { const linkedAccounts = await getLinkedAccounts(bungieMembershipId); const platforms = await generatePlatforms(linkedAccounts); if (platforms.length === 0) { showNotification({ type: 'warning', title: t('Accounts.NoCharacters'), }); removeToken(); dispatch(loggedOut()); } return platforms; } catch (e) { reportException('getDestinyAccountsForBungieAccount', e); throw e; } }; } /** * Could this account have a D1 account associated with it? */ function couldBeD1Account(destinyAccount: DestinyProfileUserInfoCard | UserInfoCard) { // D1 was only available for PS/Xbox return ( destinyAccount.membershipType === BungieMembershipType.TigerXbox || destinyAccount.membershipType === BungieMembershipType.TigerPsn ); } function formatBungieName(destinyAccount: DestinyProfileUserInfoCard | UserInfoCard) { return ( destinyAccount.bungieGlobalDisplayName + (destinyAccount.bungieGlobalDisplayNameCode ? `#${destinyAccount.bungieGlobalDisplayNameCode.toString().padStart(4, '0')}` : '') ); } /** * @param accounts raw Bungie API accounts response */ export async function generatePlatforms( accounts: DestinyLinkedProfilesResponse, ): Promise { // accounts with errors could have had D1 characters! const accountPromises = accounts.profiles .flatMap((destinyAccount) => { const account: DestinyAccount = { displayName: formatBungieName(destinyAccount), originalPlatformType: destinyAccount.membershipType, membershipId: destinyAccount.membershipId, platformLabel: PLATFORM_LABELS[destinyAccount.membershipType], destinyVersion: 2, platforms: destinyAccount.applicableMembershipTypes, lastPlayed: new Date(destinyAccount.dateLastPlayed), }; // For accounts that were folded into Cross Save, only consider them as D1 accounts. if (destinyAccount.isOverridden) { return couldBeD1Account(destinyAccount) ? [findD1Characters(account)] : []; } return couldBeD1Account(destinyAccount) ? [account, findD1Characters(account)] : [account]; }) .concat( // Profiles with errors could be D1 accounts // Consider both D1 and D2 accounts with errors, save profile errors and show on page // unless it's a specific error like DestinyAccountNotFound accounts.profilesWithErrors.flatMap((errorProfile) => { const destinyAccount = errorProfile.infoCard; const account: DestinyAccount = { displayName: formatBungieName(destinyAccount), originalPlatformType: destinyAccount.membershipType, membershipId: destinyAccount.membershipId, platformLabel: PLATFORM_LABELS[destinyAccount.membershipType], destinyVersion: 2, platforms: destinyAccount.applicableMembershipTypes, lastPlayed: new Date(0), }; if ( errorProfile.errorCode === PlatformErrorCodes.DestinyAccountNotFound || errorProfile.errorCode === PlatformErrorCodes.DestinyLegacyPlatformInaccessible ) { // If the error positively identifies this as not being a D2 account, only look for D1 accounts return couldBeD1Account(destinyAccount) ? [findD1Characters(account)] : []; } else { // Otherwise, this could be a D2 account while the API is having trouble. return couldBeD1Account(destinyAccount) ? [account, findD1Characters(account)] : [account]; } }), ); // Yes, this knowingly mixes promises and non-promises // eslint-disable-next-line @typescript-eslint/await-thenable return compact(await Promise.all(accountPromises)); } async function findD1Characters(account: DestinyAccount): Promise { try { const { characters } = await getCharacters(account); if (characters?.length) { return { ...account, destinyVersion: 1, // D1 didn't support cross-save! platforms: [account.originalPlatformType], lastPlayed: getLastPlayedD1Character(characters), }; } return null; } catch (e) { const code = e instanceof DimError ? e.bungieErrorCode() : undefined; if ( code === PlatformErrorCodes.DestinyAccountNotFound || code === PlatformErrorCodes.DestinyLegacyPlatformInaccessible ) { return null; } errorLog('accounts', 'Error getting D1 characters for', account, e); reportException('findD1Characters', e); // Return the account as if it had succeeded so it shows up in the menu return { ...account, destinyVersion: 1, // D1 didn't support cross-save! platforms: [account.originalPlatformType], lastPlayed: new Date(0), }; } } /** * Find the date of the most recently played character. */ function getLastPlayedD1Character(characters: D1Character[]): Date { return characters.reduce((memo, character) => { const d1 = new Date(character.characterBase.dateLastPlayed ?? 0); return memo ? (d1 >= memo ? d1 : memo) : d1; }, new Date(0)); } /** * @return whether the accounts represent the same account */ export function compareAccounts( account1: DestinyAccount | undefined, account2: DestinyAccount | undefined, ): boolean { return Boolean( account1 === account2 || (account1 && account1.membershipId === account2?.membershipId && account1.destinyVersion === account2.destinyVersion), ); } ================================================ FILE: src/app/accounts/observers.ts ================================================ import { set } from 'app/storage/idb-keyval'; import { StoreObserver } from 'app/store/observerMiddleware'; import { shallowEqual } from 'fast-equals'; import { AccountsState } from './reducer'; export function createSaveAccountsObserver(): StoreObserver< Pick > { return { id: 'save-accounts-observer', equals: shallowEqual, getObserved: (rootState) => ({ loaded: rootState.accounts.loaded, accounts: rootState.accounts.accounts, }), sideEffect: ({ current }) => { if (current.loaded) { set('accounts', current.accounts); } }, }; } ================================================ FILE: src/app/accounts/platforms.ts ================================================ import { loadDimApiData } from 'app/dim-api/actions'; import { deleteDimApiToken } from 'app/dim-api/dim-api-helper'; import { del, get } from 'app/storage/idb-keyval'; import { ThunkResult } from 'app/store/types'; import { convertToError } from 'app/utils/errors'; import { errorLog } from 'app/utils/log'; import { dedupePromise } from 'app/utils/promises'; import { removeToken } from '../bungie-api/oauth-tokens'; import { loadingTracker } from '../shell/loading-tracker'; import * as actions from './actions'; import { getBungieAccount } from './bungie-account'; import { DestinyAccount, compareAccounts, getDestinyAccountsForBungieAccount, } from './destiny-account'; import { accountsLoadedSelector, accountsSelector, currentAccountSelector } from './selectors'; const loadAccountsFromIndexedDBAction: ThunkResult = dedupePromise(async (dispatch) => { const accounts = await get('accounts'); dispatch(actions.loadFromIDB(accounts || [])); }); /** * Load data about available accounts. */ export const getPlatforms: ThunkResult = dedupePromise(async (dispatch, getState) => { let realAccountsPromise: Promise | null = null; if (!getState().accounts.loaded) { // Kick off a load from bungie.net in the background realAccountsPromise = dispatch(loadAccountsFromBungieNet()); } if (!getState().accounts.loadedFromIDB) { try { await dispatch(loadAccountsFromIndexedDBAction); } catch (e) { errorLog('accounts', 'Unable to load accounts from IDB', e); } } if (!accountsLoadedSelector(getState()) && realAccountsPromise) { // Fall back to Bungie.net try { await realAccountsPromise; } catch (e) { dispatch(actions.error(convertToError(e))); errorLog('accounts', 'Unable to load accounts from Bungie.net', e); } } }); const loadAccountsFromBungieNetAction: ThunkResult = dedupePromise( async (dispatch): Promise => { const bungieAccount = getBungieAccount(); if (!bungieAccount) { // We're not logged in, don't bother dispatch(actions.loggedOut()); return []; } const membershipId = bungieAccount.membershipId; return loadingTracker.addPromise(dispatch(loadPlatforms(membershipId))); }, ); function loadAccountsFromBungieNet(): ThunkResult { return loadAccountsFromBungieNetAction; } /** * Switch the current account to the given account. Lots of things depend on the current account * to calculate their info. This also saves information about the last used account so we can restore * it next time. This should be called when switching accounts or navigating to an account-specific page. */ export function setActivePlatform( account: DestinyAccount | undefined, ): ThunkResult { return async (dispatch, getState) => { if (account) { const currentAccount = currentAccountSelector(getState()); if (!currentAccount || !compareAccounts(currentAccount, account)) { localStorage.setItem('dim-last-membership-id', account.membershipId); localStorage.setItem('dim-last-destiny-version', account.destinyVersion.toString()); dispatch(actions.setCurrentAccount(account)); dispatch(loadDimApiData()); } } return account; }; } function loadPlatforms(membershipId: string): ThunkResult { return async (dispatch, getState) => { try { const destinyAccounts = await dispatch(getDestinyAccountsForBungieAccount(membershipId)); dispatch(actions.accountsLoaded(destinyAccounts)); } catch (e) { if (!accountsSelector(getState()).length) { dispatch(actions.handleAuthErrors(e)); throw e; } } return accountsSelector(getState()); }; } export function logOut(): ThunkResult { return async (dispatch) => { removeToken(); deleteDimApiToken(); localStorage.removeItem('dim-last-membership-id'); localStorage.removeItem('dim-last-destiny-version'); del('accounts'); // remove saved accounts from IDB dispatch(actions.loggedOut()); }; } ================================================ FILE: src/app/accounts/reducer.ts ================================================ import { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { API_KEY as BUNGIE_API_KEY } from 'app/bungie-api/bungie-api-utils'; import { hasValidAuthTokens } from 'app/bungie-api/oauth-tokens'; import { API_KEY as DIM_API_KEY } from 'app/dim-api/dim-api-helper'; import { deepEqual } from 'fast-equals'; import { Reducer } from 'redux'; import { ActionType, getType } from 'typesafe-actions'; import * as actions from './actions'; import { DestinyAccount } from './destiny-account'; export interface AccountsState { /** * A list of all accounts loaded from Bungie.net. */ readonly accounts: readonly DestinyAccount[]; /** * Platform Membership ID of the currently selected account. This may not * correspond to any account in the accounts array! */ readonly currentAccountMembershipId: string | undefined; /** Destiny version of the currently selected account */ readonly currentAccountDestinyVersion: DestinyVersion | undefined; /** Have we loaded from Bungie.net? */ readonly loaded: boolean; /** Have we loaded a cached version from IndexedDB? */ readonly loadedFromIDB: boolean; /** Any error loading from Bungie.net */ // TODO: is this overused? readonly accountsError?: Error; /** Do we need the user to log in? */ readonly needsLogin: boolean; /** Do we need the user to input developer info (dev only)? */ readonly needsDeveloper: boolean; } export type AccountsAction = ActionType; function getLastAccountFromLocalStorage() { const currentAccountMembershipId = localStorage.getItem('dim-last-membership-id') ?? undefined; const destinyVersionStr = localStorage.getItem('dim-last-destiny-version') ?? undefined; const currentAccountDestinyVersion = destinyVersionStr ? (parseInt(destinyVersionStr, 10) as DestinyVersion) : 2; return { currentAccountMembershipId, currentAccountDestinyVersion }; } const initialState: AccountsState = { accounts: [], ...getLastAccountFromLocalStorage(), loaded: false, loadedFromIDB: false, needsLogin: !hasValidAuthTokens(), needsDeveloper: !DIM_API_KEY || !BUNGIE_API_KEY || ($DIM_FLAVOR === 'dev' && (!localStorage.getItem('oauthClientId') || !localStorage.getItem('oauthClientSecret'))), }; export const accounts: Reducer = ( state: AccountsState = initialState, action: AccountsAction, ): AccountsState => { switch (action.type) { case getType(actions.accountsLoaded): // TODO: Maybe merge them? if there's D1 but no D2... return { ...state, accounts: deepEqual(action.payload, state.accounts) ? state.accounts : action.payload || [], loaded: true, accountsError: undefined, }; case getType(actions.setCurrentAccount): { const { membershipId, destinyVersion } = action.payload; const changed = membershipId !== state.currentAccountMembershipId || destinyVersion !== state.currentAccountDestinyVersion; return changed ? { ...state, currentAccountMembershipId: membershipId, currentAccountDestinyVersion: destinyVersion, } : state; } case getType(actions.loadFromIDB): // TODO: maybe merge them? return state.loaded ? state : { ...state, accounts: deepEqual(action.payload, state.accounts) ? state.accounts : action.payload || [], loadedFromIDB: true, }; case getType(actions.error): return { ...state, accountsError: action.payload, }; case getType(actions.loggedOut): return { ...initialState, needsLogin: true, }; case getType(actions.needsDeveloper): return { ...state, needsDeveloper: true, }; default: return state; } }; ================================================ FILE: src/app/accounts/selectors.ts ================================================ import { RootState } from 'app/store/types'; export const accountsSelector = (state: RootState) => state.accounts.accounts; /** * Full details about the currently loaded account. This may be undefined if * there is no selected account, or if the selected account doesn't appear in * the list of loaded accounts. */ export const currentAccountSelector = (state: RootState) => state.accounts.currentAccountMembershipId ? accountsSelector(state).find( (a) => a.membershipId === state.accounts.currentAccountMembershipId && a.destinyVersion === state.accounts.currentAccountDestinyVersion, ) : undefined; export const currentAccountMembershipIdSelector = (state: RootState) => state.accounts.currentAccountMembershipId; export const destinyVersionSelector = (state: RootState) => state.accounts.currentAccountDestinyVersion ?? 2; /** Are the accounts loaded enough to use? */ export const accountsLoadedSelector = (state: RootState) => state.accounts.loaded || (state.accounts.loadedFromIDB && accountsSelector(state).length > 0); export const hasD1AccountSelector = (state: RootState) => accountsSelector(state).some((a) => a.destinyVersion === 1); ================================================ FILE: src/app/armory/AllWishlistRolls.m.scss ================================================ @use 'sass:color'; @use '../variables.scss' as *; .roll { display: flex; flex-direction: row; gap: 2px; } .orGroup { border: 1px solid #333; border-radius: calc(#{dim-item-px(16)} + 2px); padding: 2px; } .notes { max-width: 50em; line-height: 1.4; text-wrap: pretty; white-space: pre-wrap; } .invalidPlug { color: $red; border: 1px solid $red; background: color.scale($red, $lightness: -90%); border-radius: 100%; height: calc(#{dim-item-px(30)}); width: calc(#{dim-item-px(30)}); box-sizing: border-box; display: flex; align-items: center; justify-content: center; margin: 1px; } .rollGroup { h3 { &:has(+ .subtitle) { margin-bottom: 0; } } } .subtitle { color: var(--theme-text-secondary); margin-top: 0.1em; margin-bottom: 0.5em; } ================================================ FILE: src/app/armory/AllWishlistRolls.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'invalidPlug': string; 'notes': string; 'orGroup': string; 'roll': string; 'rollGroup': string; 'subtitle': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/armory/AllWishlistRolls.tsx ================================================ import ExternalLink from 'app/dim-ui/ExternalLink'; import { PressTip } from 'app/dim-ui/PressTip'; import { t } from 'app/i18next-t'; import { DimItem, DimPlug, DimSocket } from 'app/inventory/item-types'; import Plug from 'app/item-popup/Plug'; import { useD2Definitions } from 'app/manifest/selectors'; import { faExclamationTriangle } from 'app/shell/icons'; import AppIcon from 'app/shell/icons/AppIcon'; import { compareBy } from 'app/utils/comparators'; import { isEnhancedPerkHash } from 'app/utils/perk-utils'; import { wishListInfosSelector, wishListRollsForItemHashSelector } from 'app/wishlists/selectors'; import { WishListRoll } from 'app/wishlists/types'; import { partition } from 'es-toolkit'; import { useSelector } from 'react-redux'; import * as styles from './AllWishlistRolls.m.scss'; import { getCraftingTemplate } from './crafting-utils'; import { consolidateRollsForOneWeapon, consolidateSecondaryPerks } from './wishlist-collapser'; /** * List out all the known wishlist rolls for a given item. * * This is currently only used with a fake definitions-built item, * that has every perk available in each perk socket * (with some overrides to set some as "plugged", when spawned from a real item). * This would render much weirder if it were fed an owned inventory item. */ export default function AllWishlistRolls({ item, realAvailablePlugHashes, }: { item: DimItem; /** * non-plugged, but available, plugs, from the real item this was spawned from. * used to mark sockets as available */ realAvailablePlugHashes?: number[]; }) { const wishlistRolls = useSelector(wishListRollsForItemHashSelector(item.hash)); const [goodRolls, badRolls] = partition(wishlistRolls, (r) => !r.isUndesirable); return ( <> {goodRolls.length > 0 && ( <>

{t('Armory.WishlistedRolls', { count: goodRolls.length })}

)} {badRolls.length > 0 && ( <>

{t('Armory.TrashlistedRolls', { count: badRolls.length })}

)} ); } function WishlistRolls({ wishlistRolls, item, realAvailablePlugHashes, }: { wishlistRolls: WishListRoll[]; item: DimItem; /** * non-plugged, but available, plugs, from the real item this was spawned from. * used to mark sockets as available */ realAvailablePlugHashes?: number[]; }) { const defs = useD2Definitions()!; const wishlistInfos = useSelector(wishListInfosSelector); const groupedWishlistRolls = Object.groupBy(wishlistRolls, (r) => r.notes || t('Armory.NoNotes')); const templateSockets = getCraftingTemplate(defs, item.hash)?.sockets?.socketEntries; const socketByPerkHash: Record = {}; const plugByPerkHash: Record = {}; // the order, within their column, that perks appear. for sorting barrels mags etc. const columnOrderByPlugHash: Record = {}; if (item.sockets) { for (const s of item.sockets.allSockets) { if (s.isReusable) { for (const p of s.plugOptions) { socketByPerkHash[p.plugDef.hash] = s; plugByPerkHash[p.plugDef.hash] = p; } // if this is a crafted item, use its template's plug order. otherwise fall back to its reusable or randomized plugsets const plugSetHash = templateSockets?.[s.socketIndex].reusablePlugSetHash ?? (s.socketDefinition.randomizedPlugSetHash || s.socketDefinition.reusablePlugSetHash); if (plugSetHash) { const plugItems = defs.PlugSet.get(plugSetHash).reusablePlugItems; for (let i = 0; i < plugItems.length; i++) { const plugItem = plugItems[i]; if (plugItem.currentlyCanRoll) { columnOrderByPlugHash[plugItem.plugItemHash] = i; } } } } } } // TODO: group by making a tree of least cardinality -> most? const spentTitles = new Set(); function spendTitle(roll: WishListRoll) { if (roll.title && !spentTitles.has(roll.title)) { spentTitles.add(roll.title); const url = wishlistInfos?.[roll.sourceWishListIndex ?? -1]?.url; return ( <>

{url ? {roll.title} : roll.title}

{roll.description &&

{roll.description}

} ); } } return ( <> {Object.entries(groupedWishlistRolls).map(([notes, rolls]) => { const consolidatedRolls = consolidateRollsForOneWeapon(defs, item, rolls); return (
{spendTitle(rolls[0])}

{notes}

    {consolidatedRolls.map((cr) => { // groups [outlaw, enhanced outlaw, rampage] // into { // "3": [outlaw, enhanced outlaw] // "4": [rampage] // } const primariesGroupedByColumn = Object.groupBy( cr.commonPrimaryPerks, (h) => socketByPerkHash[h]?.socketIndex ?? -1, ); // turns the above into // [[outlaw, enhanced outlaw], [rampage]] const primaryBundles = cr.rolls[0].primarySocketIndices.map((socketIndex) => primariesGroupedByColumn[socketIndex ?? -1].sort( // establish a consistent base -> enhanced perk order compareBy((h) => Number(isEnhancedPerkHash(h))), ), ); // i.e. // [ // [[drop mag], [smallbore, extended barrel]], // [[tac mag], [rifled barrel, extended barrel]] // ] const consolidatedSecondaries = consolidateSecondaryPerks(cr.rolls); // if there were no secondary perks in any of the rolls, // consolidateSecondaryPerks will *correctly* return an array with no permutations. // if so, we'll add a blank dummy one so there's something to iterate below. if (!consolidatedSecondaries.length) { consolidatedSecondaries.push([]); } return consolidatedSecondaries.map((secondaryBundle) => { const bundles = [...secondaryBundle, ...primaryBundles]; return (
  • b.join()).join()} className={styles.roll}> {bundles.map((hashes) => (
    {hashes .sort( compareBy( // unrecognized/unrollable perks sort to last (h) => columnOrderByPlugHash[h] ?? 9999, ), ) .map((h) => { const socket = socketByPerkHash[h]; const plug = plugByPerkHash[h]; return plug && socket ? ( ) : ( ); })}
    ))}
  • ); }); })}
); })} ); } function InvalidPlug({ hash }: { hash: number }) { const defs = useD2Definitions(); const perkName = defs?.InventoryItem.get(hash)?.displayProperties.name; return ( ); } ================================================ FILE: src/app/armory/Armory.m.scss ================================================ @use '../variables' as *; .armory { background-repeat: no-repeat; background-position: top center; background-size: 100%; padding: 16px; box-sizing: border-box; background-color: #0b0c0f; aspect-ratio: 16/9; user-select: text; :global(.item-details) { margin-left: 0; margin-right: 0; max-width: 300px; @include phone-portrait { max-width: 100%; } } h2 { > button { margin-left: 1em; } } } .season { display: flex; align-items: center; .header &:nth-child(n + 1) { border-left: 1px solid #ccc; padding-left: 8px; margin-left: 4px; } > img { margin-right: 2px; } } .source { font-style: italic; opacity: 0.7; } .header { --item-size: 100px; display: grid; grid-template-columns: min-content 1fr; grid-template-areas: 'item title' 'item subtitle'; gap: 0 16px; margin-bottom: 16px; @include phone-portrait { --item-size: 50px; grid-template-areas: 'item title' 'subtitle subtitle'; gap: 8px 8px; } h1 { grid-area: title; margin: 0 0 4px 0; @include destiny-header; } :global(.item) { grid-area: item; img { // We're scaling up, keep it sharp image-rendering: pixelated; } } p { margin: 8px 0 0 0; white-space: pre-wrap; } } .headerContent { grid-area: subtitle; } .subtitle { display: flex; flex-wrap: wrap; align-items: center; margin-bottom: 4px; gap: 4px; > * { margin: 0; } } .element { height: 16px; width: 16px; } .flavor { font-style: italic; } .section { width: 300px; margin-bottom: 10px; @include phone-portrait { width: 100%; } } .list { composes: flexColumn from '../dim-ui/common.m.scss'; gap: 16px; } .alternate { composes: flexRow from '../dim-ui/common.m.scss'; gap: 8px; text-align: left; width: max-content; max-width: 100%; > *:first-child { flex-shrink: 0; } b { text-transform: uppercase; font-weight: 600; font-family: Helvetica, Arial, sans-serif; font-size: 14px; } } .alternateButton { composes: resetButton from '../dim-ui/common.m.scss'; composes: item from global; } .alternateWishlist { text-transform: lowercase; } ================================================ FILE: src/app/armory/Armory.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'alternate': string; 'alternateButton': string; 'alternateWishlist': string; 'armory': string; 'element': string; 'flavor': string; 'header': string; 'headerContent': string; 'list': string; 'season': string; 'section': string; 'source': string; 'subtitle': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/armory/Armory.tsx ================================================ import ItemGrid from 'app/armory/ItemGrid'; import { addCompareItem } from 'app/compare/actions'; import { stripAdept } from 'app/compare/compare-utils'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { languageSelector } from 'app/dim-api/selectors'; import BungieImage, { bungieNetPath } from 'app/dim-ui/BungieImage'; import { DestinyTooltipText } from 'app/dim-ui/DestinyTooltipText'; import ElementIcon from 'app/dim-ui/ElementIcon'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { t } from 'app/i18next-t'; import ItemIcon, { DefItemIcon } from 'app/inventory/ItemIcon'; import { DimItem } from 'app/inventory/item-types'; import { allItemsSelector, createItemContextSelector } from 'app/inventory/selectors'; import { getQuestLineInfo, makeFakeItem } from 'app/inventory/store/d2-item-factory'; import { SocketOverrides, applySocketOverrides, useSocketOverrides, } from 'app/inventory/store/override-sockets'; import { getEvent, getSeason } from 'app/inventory/store/season'; import { AmmoIcon } from 'app/item-popup/AmmoIcon'; import BreakerType from 'app/item-popup/BreakerType'; import EmblemPreview from 'app/item-popup/EmblemPreview'; import ItemSockets from 'app/item-popup/ItemSockets'; import ItemStats from 'app/item-popup/ItemStats'; import MetricCategories from 'app/item-popup/MetricCategories'; import { hideItemPopup } from 'app/item-popup/item-popup'; import { useD2Definitions } from 'app/manifest/selectors'; import Objective from 'app/progress/Objective'; import { Reward } from 'app/progress/Reward'; import { AppIcon, compareIcon, faMinusSquare, faPlusSquare, thumbsUpIcon } from 'app/shell/icons'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { chainComparator, compareBy, reverseComparator } from 'app/utils/comparators'; import { emptyArray } from 'app/utils/empty'; import { localizedListFormatter } from 'app/utils/intl'; import { getItemYear, itemTypeName } from 'app/utils/item-utils'; import { wishListsByHashSelector } from 'app/wishlists/selectors'; import { DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { D2EventInfo } from 'data/d2/d2-event-info-v2'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import AllWishlistRolls from './AllWishlistRolls'; import * as styles from './Armory.m.scss'; import ArmorySheet from './ArmorySheet'; import Links from './Links'; import WishListEntry from './WishListEntry'; export default function Armory({ itemHash, realItemSockets, realAvailablePlugHashes, }: { itemHash: number; /** this is used to pass a real DimItem's current "plugged" plugs, into the fake DimItem that Armory creates */ realItemSockets?: SocketOverrides; /** * non-plugged, but available, plugs, from the real item this was spawned from. * used to mark sockets as available */ realAvailablePlugHashes?: number[]; }) { const dispatch = useThunkDispatch(); const defs = useD2Definitions()!; const allItems = useSelector(allItemsSelector); const isPhonePortrait = useIsPhonePortrait(); const [socketOverrides, onPlugClicked] = useSocketOverrides(); const itemCreationContext = useSelector(createItemContextSelector); const [armoryItemHash, setArmoryItemHash] = useState(undefined); const wishlistsByHash = useSelector(wishListsByHashSelector); const itemDef = defs.InventoryItem.get(itemHash); const itemWithoutSockets = makeFakeItem(itemCreationContext, itemHash, { allowWishList: true }); if (!itemWithoutSockets) { return (

{t('Armory.Unknown')}

); } const item = applySocketOverrides(itemCreationContext, itemWithoutSockets, { // Start with the item's current sockets ...realItemSockets, // Then apply whatever the user chose in the Armory UI ...socketOverrides, }); const storeItems = allItems.filter((i) => i.hash === itemHash); const collectible = item.collectibleHash ? defs.Collectible.get(item.collectibleHash) : undefined; // Use the ornament's screenshot if available const ornamentSocket = item.sockets?.allSockets.find((s) => s.plugged?.plugDef.screenshot); const screenshot = ornamentSocket?.plugged?.plugDef.screenshot || itemDef.screenshot; const flavorText = itemDef.flavorText || itemDef.displaySource; // TODO: Show Catalyst benefits for exotics const alternates = getAlternateItems(item, defs); const seasonNum = getSeason(item); return (

{item.name}

{item.destinyVersion === 2 && item.ammoType > 0 && }
{itemTypeName(item)}
{item.pursuit?.questLine && (
{t('MovePopup.Subtitle.QuestProgress', { questStepNum: item.pursuit.questLine.questStepNum, questStepsTotal: item.pursuit.questLine.questStepsTotal ?? '?', })}
)} {seasonNum >= 0 && }
{item.classified &&
{t('ItemService.Classified2')}
} {collectible?.sourceString && (
{collectible?.sourceString}
)} {item.description && (

)} {flavorText &&

{flavorText}

}
{isPhonePortrait && screenshot && (
)} {defs.isDestiny2 && item.itemCategoryHashes.includes(ItemCategoryHashes.Emblems) && (
)} {defs.isDestiny2 && item.availableMetricCategoryNodeHashes && (
)} {item.stats && !item.bucket.inArmor && (
)} {item.sockets && (
)} {item.pursuit && ( <> {defs && item.objectives && (
{item.objectives.map((objective) => ( ))}
)} {defs.isDestiny2 && item.pursuit.rewards.length !== 0 && (
{t('MovePopup.Rewards')}
{item.pursuit.rewards.map((reward) => ( ))}
)} {item.pursuit?.questLine?.description && (

)} {itemDef.setData?.itemList && (
    {itemDef.setData.itemList.map((h) => { const stepItem = makeFakeItem(itemCreationContext, h.itemHash); return ( stepItem && (
  1. ) ); })}
)} )} {!isPhonePortrait && item.wishListEnabled && } {alternates.length > 0 && ( <>

{t('Armory.AlternateItems')}

{alternates.map((alternate) => { const altSeasonNum = getSeason(alternate); const questLine = getQuestLineInfo(alternate); return (
{alternate.displayProperties.name} {questLine && (
{t('MovePopup.Subtitle.QuestProgress', { questStepNum: questLine.questStepNum, questStepsTotal: questLine.questStepsTotal ?? '?', })}
)} {altSeasonNum >= 0 && ( )} {wishlistsByHash.has(alternate.hash) && (
{' '} {t('Armory.WishlistedRolls', { count: wishlistsByHash.get(alternate.hash)?.length ?? 0, })}
)} {altSeasonNum === seasonNum ? ( ) : (
{t('Armory.DifferentSeason')}
)}
); })}
)} {storeItems.length > 0 && ( <>

{t('Armory.YourItems')} {storeItems[0].comparable && ( )}

)} {item.wishListEnabled && ( )} {armoryItemHash !== undefined && ( setArmoryItemHash(undefined)} /> )}
); } /** Find the definitions for other versions of this item. */ function getAlternateItems( item: DimItem, defs: D2ManifestDefinitions, ): DestinyInventoryItemDefinition[] { const alternates: DestinyInventoryItemDefinition[] = []; const allDefs = defs.InventoryItem.getAll(); for (const hash in allDefs) { const i = allDefs[hash]; if ( i.hash !== item.hash && i.inventory?.bucketTypeHash === item.bucket.hash && !i.itemCategoryHashes?.includes(ItemCategoryHashes.Dummies) && stripAdept(i.displayProperties.name) === stripAdept(item.name) ) { alternates.push(i); } } alternates.sort( chainComparator( reverseComparator(compareBy((i) => getSeason(i, defs) ?? 0)), compareBy((i) => getQuestLineInfo(i)?.questStepNum ?? 0), compareBy((i) => i.displayProperties.name), ), ); return alternates; } const typeHashes = new Set([1215804696, 1215804697, 3993098925]); function AlternatePerkDiffs({ itemDef, alternate, defs, }: { itemDef: DestinyInventoryItemDefinition; alternate: DestinyInventoryItemDefinition; defs: D2ManifestDefinitions; }) { const diffs = compareItemPerks(itemDef, alternate, defs); const language = useSelector(languageSelector); const listFormat = localizedListFormatter(language); return ( <> {diffs.map(([i1, i2]) => { const removals = i1 .map((h) => defs.InventoryItem.get(h).displayProperties.name) .filter(Boolean); const additions = i2 .map((h) => defs.InventoryItem.get(h).displayProperties.name) .filter(Boolean); return ( (additions.length > 0 || removals.length > 0) && ( {removals.length > 0 && (
{listFormat.format(removals)}
)} {additions.length > 0 && (
{listFormat.format(additions)}
)}
) ); })} ); } /** * Compare the perks of two item definitions. Returns a tuple for each socket * with a list of perks exclusive to the first item, and exclusive to the second * item. */ function compareItemPerks( itemDef1: DestinyInventoryItemDefinition, itemDef2: DestinyInventoryItemDefinition, defs: D2ManifestDefinitions, ): [firstItemExclusivePerkHashes: number[], secondItemExclusivePerkHashes: number[]][] { if (!itemDef1.sockets || !itemDef2.sockets) { return emptyArray(); } const sockets1 = itemDef1.sockets.socketEntries.filter((s) => typeHashes.has(s.socketTypeHash)); const sockets2 = itemDef2.sockets.socketEntries.filter((s) => typeHashes.has(s.socketTypeHash)); const diff: [number[], number[]][] = []; for (let i = 0; i < Math.max(sockets1.length, sockets2.length); i++) { const rph1 = i < sockets1.length ? sockets1[i].randomizedPlugSetHash || sockets1[i].reusablePlugSetHash : undefined; const rph2 = i < sockets2.length ? sockets2[i].randomizedPlugSetHash || sockets2[i].reusablePlugSetHash : undefined; const ps1 = new Set( (rph1 && defs.PlugSet.get(rph1).reusablePlugItems.map((pi) => pi.plugItemHash)) || [], ); const ps2 = new Set( (rph2 && defs.PlugSet.get(rph2).reusablePlugItems.map((pi) => pi.plugItemHash)) || [], ); for (const set of [ps1, ps2]) { for (const h of set) { if (ps1.has(h) && ps2.has(h)) { ps1.delete(h); ps2.delete(h); } } } if (ps1.size || ps2.size) { diff.push([[...ps1], [...ps2]]); } } return diff; } function SeasonInfo({ defs, item, className, seasonNum, }: { item: DestinyInventoryItemDefinition | DimItem; defs: D2ManifestDefinitions; className?: string; seasonNum: number; }) { const season = Object.values(defs.Season.getAll()).find((s) => s.seasonNumber === seasonNum); const event = 'displayProperties' in item ? undefined : getEvent(item); return ( season && (
{season.displayProperties.hasIcon && ( )}{' '} {season.displayProperties.name} ( {t('Armory.Season', { season: season.seasonNumber, year: getItemYear(item) ?? '?', })} ){Boolean(event) && ` - ${D2EventInfo[event!].name}`}
) ); } ================================================ FILE: src/app/armory/ArmoryPage.tsx ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { useLoadStores } from 'app/inventory/store/hooks'; import { usePageTitle } from 'app/utils/hooks'; import { useLocation, useParams } from 'react-router'; import Armory from './LazyArmory'; export default function ArmoryPage({ account }: { account: DestinyAccount }) { usePageTitle(t('Armory.Armory')); const { itemHash: itemHashString } = useParams(); const itemHash = parseInt(itemHashString ?? '', 10); const { search } = useLocation(); const storesLoaded = useLoadStores(account); if (!storesLoaded) { return ; } const searchParams = new URLSearchParams(search); const perksString = searchParams.get('perks') ?? ''; const sockets = perksString.split(',').reduce<{ [index: number]: number }>((memo, n, i) => { const perkHash = parseInt(n, 10); if (perkHash !== 0) { memo[i] = perkHash; } return memo; }, {}); return (
); } ================================================ FILE: src/app/armory/ArmorySheet.m.scss ================================================ .sheet { max-width: 900px; margin: 0 auto; } ================================================ FILE: src/app/armory/ArmorySheet.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'sheet': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/armory/ArmorySheet.tsx ================================================ import ClickOutsideRoot from 'app/dim-ui/ClickOutsideRoot'; import Sheet from 'app/dim-ui/Sheet'; import { DimItem } from 'app/inventory/item-types'; import { filterMap } from 'app/utils/collections'; import focusingItemOutputs from 'data/d2/focusing-item-outputs.json'; import { Suspense, useMemo } from 'react'; import * as styles from './ArmorySheet.m.scss'; import Armory from './LazyArmory'; export default function ArmorySheet({ item, itemHash, onClose, }: { onClose: () => void } & ( | { item: DimItem; itemHash?: undefined } | { itemHash: number; item?: undefined } )) { const realItemSockets = useMemo( () => item?.sockets ? Object.fromEntries( filterMap(item.sockets.allSockets, (s) => s.plugged ? [s.socketIndex, s.plugged.plugDef.hash] : undefined, ), ) : {}, [item?.sockets], ); const realAvailablePlugHashes = useMemo( () => item?.sockets?.allSockets.flatMap((s) => s.plugOptions.map((p) => p.plugDef.hash)) ?? [], [item?.sockets], ); // If we're opening a dummy weapon from a Vendor (like for item focusing), // try to find the definition of what a user would expect. const betterItemHash = item?.vendor && focusingItemOutputs[item.hash]; return ( ); } ================================================ FILE: src/app/armory/ItemGrid.tsx ================================================ /** * A simple item grid that manages its own item popup separate from the global popup. Useful for showing items within a sheet. */ import ItemPopup from 'app/item-popup/ItemPopup'; import React, { JSX, useCallback, useRef, useState } from 'react'; import '../inventory-page/StoreBucket.scss'; import ConnectedInventoryItem from '../inventory/ConnectedInventoryItem'; import { DimItem } from '../inventory/item-types'; export interface PopupState { item: DimItem; element: HTMLElement; } export default function ItemGrid({ items, noLink, }: { items: DimItem[]; /** Don't allow opening Armory from the header link */ noLink?: boolean; }) { const [popup, setPopup] = useState(); return (
{items.map((i) => ( {(ref, showPopup) => } ))} {popup && ( setPopup(undefined)} item={popup.item} element={popup.element} noLink={noLink} /> )}
); } export function BasicItemTrigger({ item, onShowPopup, children, }: { item: DimItem; onShowPopup: (state: PopupState) => void; children: ( ref: React.Ref, showPopup: (e: React.MouseEvent) => void, ) => React.ReactNode; }) { const ref = useRef(null); const clicked = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); onShowPopup({ item, element: ref.current! }); }, [item, onShowPopup], ); return children(ref, clicked) as JSX.Element; } ================================================ FILE: src/app/armory/LazyArmory.ts ================================================ import { lazy } from 'react'; export default lazy(() => import('./Armory' /* webpackChunkName: "item-popup-armory" */)); ================================================ FILE: src/app/armory/Links.m.scss ================================================ @use '../variables' as *; .links { display: flex; flex-direction: column; float: right; margin: 0 0 0 32px; padding: 0; list-style: none; gap: 4px; @include phone-portrait { margin-right: 38px; } @media (max-width: 675px) { margin: 0 0 8px 0; float: none; flex-flow: row wrap; justify-content: space-between; > li { padding: 8px; } } li { white-space: nowrap; img { vertical-align: text-bottom; margin-right: 0.2em; } } } ================================================ FILE: src/app/armory/Links.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'links': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/armory/Links.tsx ================================================ import { languageSelector } from 'app/dim-api/selectors'; import ExternalLink from 'app/dim-ui/ExternalLink'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { compact } from 'app/utils/collections'; import { isKillTrackerSocket } from 'app/utils/item-utils'; import { getSocketsWithStyle, isWeaponMasterworkSocket } from 'app/utils/socket-utils'; import { DestinySocketCategoryStyle } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes, PlugCategoryHashes } from 'data/d2/generated-enums'; import destinysets from 'images/destinysets.svg'; import logo from 'images/dimlogo.svg'; import foundry from 'images/foundry.png'; import ishtarLogo from 'images/ishtar-collective.svg'; import lightgg from 'images/lightgg.png'; import { useSelector } from 'react-redux'; import * as styles from './Links.m.scss'; export default function Links({ item }: { item: DimItem }) { const language = useSelector(languageSelector); const isPhonePortrait = useIsPhonePortrait(); const links = [ { name: 'DIM', icon: logo, link: `/armory/${item.hash}?perks=${buildSocketParam(item)}`, }, { name: 'Light.gg', icon: lightgg, link: `https://www.light.gg/db/${language}/items/${item.hash}${buildLightGGSockets(item)}`, }, item.bucket.inWeapons && { name: 'D2Foundry', icon: foundry, link: `https://d2foundry.gg/w/${item.hash}${buildFoundrySockets(item)}`, }, !isPhonePortrait && { name: 'data.destinysets.com', icon: destinysets, link: `https://data.destinysets.com/i/InventoryItem:${item.hash}?lang=${language}`, }, item.loreHash && { name: t('MovePopup.ReadLoreLink'), icon: ishtarLogo, link: `http://www.ishtar-collective.net/entries/${item.loreHash}`, }, ]; return (
    {compact(links).map(({ link, name, icon }) => (
  • {name}
  • ))}
); } /** * Build a comma-separated list of perks where each entry in the list corresponds to a socket ID and the value is the plugged item hash. A zero corresponds to "no choice". */ function buildSocketParam(item: DimItem): string { const perkValues: number[] = []; if (item.sockets) { for (const socket of item.sockets.allSockets) { perkValues[socket.socketIndex] = socket.plugged?.plugDef.hash ?? 0; } } // Fill in those empty array elements for (let i = 0; i < perkValues.length; i++) { perkValues[i] ||= 0; } return perkValues.join(','); } /** * Light.gg's socket format is highly similar to that of D2Gunsmith: [...base perks, masterwork, weapon mod].join(',') */ function buildLightGGSockets(item: DimItem) { const perkValues = getWeaponSocketInfo(item); if (perkValues) { return `?p=${[...perkValues.largePerks, ...perkValues.traits, perkValues.masterwork, perkValues.weaponMod].map((s) => String(s)).join(',')}`; } return ''; } /** * Foundry's socket format is: ?p=perkHashes,...&m=weaponMod&mw=masterworkStatHash */ function buildFoundrySockets(item: DimItem) { const perkValues = getWeaponSocketInfo(item); if (perkValues) { const primaryMasterworkStat = item.sockets?.allSockets.find(isWeaponMasterworkSocket)?.plugged?.plugDef .investmentStats?.[0]; const mwHash = primaryMasterworkStat?.statTypeHash || perkValues.largePerks[0] || 0; // `mw` for crafted exo intrinsic const modHash = perkValues.weaponMod || perkValues.masterwork || 0; // `m` for non-crafted exo mw return `?p=${perkValues.traits.join(',')}&m=${modHash || ''}&mw=${mwHash || ''}`; } return ''; } /** * Gathers general socket information for link generation in D2Gunsmith and Light.gg. */ function getWeaponSocketInfo(item: DimItem): null | { traits: number[]; masterwork: number; weaponMod: number; largePerks: number[]; } { if (item.sockets && item.bucket?.inWeapons) { // TODO: Map enhanced intrinsic frames with their corresponding stat masterworks const masterworkSocket = item.sockets.allSockets.find( (s) => isWeaponMasterworkSocket(s) && !s.isReusable, ); const masterwork = masterworkSocket?.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.CraftingPlugsFrameIdentifiers ? 0 : (masterworkSocket?.plugged?.plugDef.hash ?? 0); const weaponModSocket = item.sockets.allSockets.find((s) => s.plugged?.plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.WeaponModsDamage), ); const weaponMod = weaponModSocket?.plugged!.plugDef.hash ?? 0; const trackerSocket = item.sockets.allSockets.find(isKillTrackerSocket); const largePerks = getSocketsWithStyle(item.sockets, DestinySocketCategoryStyle.LargePerk) .filter((s) => s.hasRandomizedPlugItems) .map((s) => s.plugged?.plugDef.hash ?? 0); const perkSockets = getSocketsWithStyle(item.sockets, DestinySocketCategoryStyle.Reusable); const traits = perkSockets .filter( (s) => ![trackerSocket?.socketIndex, weaponModSocket?.socketIndex].includes(s.socketIndex), ) .map((s) => s.plugged?.plugDef.hash ?? 0); return { traits, masterwork, weaponMod, largePerks }; } return null; } ================================================ FILE: src/app/armory/WishListEntry.m.scss ================================================ .wishlist { width: 100%; display: flex; gap: 8px; align-items: center; > input { flex: 1; } } ================================================ FILE: src/app/armory/WishListEntry.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'wishlist': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/armory/WishListEntry.tsx ================================================ import HelpLink from 'app/dim-ui/HelpLink'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { showNotification } from 'app/notifications/notifications'; import { wishListGuideLink } from 'app/shell/links'; import { filterMap } from 'app/utils/collections'; import { isKillTrackerSocket } from 'app/utils/item-utils'; import { getSocketsWithStyle } from 'app/utils/socket-utils'; import { DestinySocketCategoryStyle } from 'bungie-api-ts/destiny2'; import * as styles from './WishListEntry.m.scss'; /** * Add a control for people to copy out the wish list line for the currently configured roll. */ export default function WishListEntry({ item }: { item: DimItem }) { const wishlistLine = createWishListRollString(item); const handleFocusWishlist = (e: React.FocusEvent) => e.target.select(); const handleButtonClick = () => { navigator.clipboard.writeText(wishlistLine); showNotification({ type: 'success', title: t('WishListRoll.CopiedLine'), }); }; return (
); } function createWishListRollString(item: DimItem) { let perkHashes: number[] = []; if (item.sockets) { const sockets = getSocketsWithStyle(item.sockets, DestinySocketCategoryStyle.Reusable); perkHashes = filterMap(sockets, (socket) => isKillTrackerSocket(socket) || socket.plugOptions.length <= 1 ? undefined : socket.plugged?.plugDef.hash, ); } return `dimwishlist:item=${item.hash}&perks=${perkHashes.join(',')}`; } ================================================ FILE: src/app/armory/crafting-utils.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2'; import memoizeOne from 'memoize-one'; const buildTemplateLookup = memoizeOne((defs: D2ManifestDefinitions) => { const results: NodeJS.Dict = {}; const invItemTable = defs.InventoryItem.getAll(); for (const h in invItemTable) { const i = invItemTable[h]; if (i.crafting) { results[i.crafting.outputItemHash] = i; } } return results; }); /** * input a weapon's hash. if it's a craftable weapon, this returns the crafting template item. * the template contains its possible perks/plugs/etc in proper order. * * for instance, template 2335939410 outputs weapon 2856514843. * only 2335939410 has the barrels & magazines in the right order, and with level requirements attached. * * KEEP IN MIND: this will be the wrong item hash for, say, wishlists. wishlists are pointed at the *output* item hash. */ export function getCraftingTemplate(defs: D2ManifestDefinitions, itemHash: number) { return buildTemplateLookup(defs)[itemHash]; } ================================================ FILE: src/app/armory/trait-to-enhanced-trait.d.ts ================================================ declare module 'data/d2/trait-to-enhanced-trait.json' { const x: { readonly [hash: number]: number | undefined }; export default x; } ================================================ FILE: src/app/armory/wishlist-collapser.test.ts ================================================ import { consolidateSecondaryPerks } from './wishlist-collapser'; describe('perkConsolidator', () => { it('does its thing', () => { expect( consolidateSecondaryPerks([ { secondaryPerksMap: { 1: 4134353779, 2: 1482024992 }, secondarySocketIndices: [1, 2], primaryPerksList: [], primarySocketIndices: [], primaryPerkIdentifier: '', primaryPerkIdentifierNormalized: '', secondaryPerkIdentifier: '', }, { secondaryPerksMap: { 1: 4134353779, 2: 1467527085 }, secondarySocketIndices: [1, 2], primaryPerksList: [], primarySocketIndices: [], primaryPerkIdentifier: '', primaryPerkIdentifierNormalized: '', secondaryPerkIdentifier: '', }, { secondaryPerksMap: { 1: 106909392, 2: 1332244541 }, secondarySocketIndices: [1, 2], primaryPerksList: [], primarySocketIndices: [], primaryPerkIdentifier: '', primaryPerkIdentifierNormalized: '', secondaryPerkIdentifier: '', }, { secondaryPerksMap: { 1: 106909392, 2: 1467527085 }, secondarySocketIndices: [1, 2], primaryPerksList: [], primarySocketIndices: [], primaryPerkIdentifier: '', primaryPerkIdentifierNormalized: '', secondaryPerkIdentifier: '', }, ]), ).toMatchInlineSnapshot(` [ [ [ 4134353779, ], [ 1482024992, ], ], [ [ 106909392, ], [ 1332244541, ], ], [ [ 106909392, 4134353779, ], [ 1467527085, ], ], ] `); }); }); ================================================ FILE: src/app/armory/wishlist-collapser.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimItem } from 'app/inventory/item-types'; import { compareBy } from 'app/utils/comparators'; import { enhancedVersion, unenhancedVersion } from 'app/utils/perk-utils'; import { WishListRoll } from 'app/wishlists/types'; import { DestinyInventoryItemDefinition, TierType } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import { partition } from 'es-toolkit'; interface Roll { /** rampage, outlaw, etc. */ primaryPerksList: number[]; /** fast access to primaryPerks keys */ primarySocketIndices: number[]; /** string to quickly measure primaryPerks equality */ primaryPerkIdentifier: string; /** string to quickly measure primaryPerks equality, where rampage and enhanced rampage are the same perk */ primaryPerkIdentifierNormalized: string; /** barrels, magazines, etc. object keyed by socket hash */ secondaryPerksMap: Record; /** fast access to secondaryPerks keys */ secondarySocketIndices: number[]; /** string to quickly measure secondaryPerks equality */ secondaryPerkIdentifier: string; } export function consolidateRollsForOneWeapon( defs: D2ManifestDefinitions, item: DimItem, rolls: WishListRoll[], ) { const socketIndexByPerkHash: Record = {}; if (item.sockets) { for (const s of item.sockets.allSockets) { if (s.isReusable) { for (const p of s.plugOptions) { socketIndexByPerkHash[p.plugDef.hash] = s.socketIndex; } } } } const allRolls: Roll[] = rolls.map((roll) => { const [primaryPerksList, secondaryPerksList] = partition( Array.from(roll.recommendedPerks), (h) => isMajorPerk(defs.InventoryItem.get(h)), ); // important sorting to generate comparably join()ed strings primaryPerksList.sort((a, b) => socketIndexByPerkHash[a] - socketIndexByPerkHash[b]); const primarySocketIndices = primaryPerksList.map((h) => socketIndexByPerkHash[h]); const secondaryPerksMap: Record = {}; for (const h of secondaryPerksList) { secondaryPerksMap[socketIndexByPerkHash[h]] = h; } // important sorting to generate comparably join()ed strings secondaryPerksList.sort((a, b) => socketIndexByPerkHash[a] - socketIndexByPerkHash[b]); const secondarySocketIndices = secondaryPerksList.map((h) => socketIndexByPerkHash[h]); return { primaryPerksList, primarySocketIndices, primaryPerkIdentifier: primaryPerksList.join(), primaryPerkIdentifierNormalized: primaryPerksList.map(normalizePerkKey).join(), secondaryPerksMap, secondarySocketIndices, secondaryPerkIdentifier: secondaryPerksList.join(), }; }); const rollsGroupedByPrimaryNormalizedPerks = Object.groupBy( allRolls, (roll) => roll.primaryPerkIdentifierNormalized, ); const rollsGroupedByPrimaryPerks: Record< string, { commonPrimaryPerks: number[]; rolls: Roll[]; } > = {}; for (const normalizedPrimaryPerkKey in rollsGroupedByPrimaryNormalizedPerks) { // within these braces, we're only looking at a situation like rampage/outlaw, // and its enhanced permutations. so we can make some assumptions const rollGroup = rollsGroupedByPrimaryNormalizedPerks[normalizedPrimaryPerkKey]; if (!normalizedPrimaryPerkKey.includes('/')) { // this roll group is normal (rollsGroupedByPrimaryPerks[normalizedPrimaryPerkKey] ??= { commonPrimaryPerks: rollGroup[0].primaryPerksList, rolls: [], }).rolls.push(...rollGroup); } else { // this group needs enhancedness grouping // these rolls can be clumped into groups that have the same secondary perks const rollsGroupedBySecondaryStuff = Object.groupBy( rollGroup, (r) => r.secondaryPerkIdentifier, ); for (const secondaryPerkKey in rollsGroupedBySecondaryStuff) { const rollsWithSameSecondaryPerks = rollsGroupedBySecondaryStuff[secondaryPerkKey]; const commonPrimaryPerks = [ ...new Set(rollsWithSameSecondaryPerks.flatMap((r) => r.primaryPerksList)), ].sort(compareBy((h) => socketIndexByPerkHash[h])); const commonPrimaryPerksKey = commonPrimaryPerks.join(); if ( rollsWithSameSecondaryPerks.length === 1 || // if there's 2 rolls, if they have something in common, // i.e. "base/enh" and "base/base" have a "base" in the same column // it's safe to combine, (rollsWithSameSecondaryPerks.length === 2 && rollsWithSameSecondaryPerks[0].primaryPerksList.some( (h, i) => h === rollsWithSameSecondaryPerks[1].primaryPerksList[i], )) || // if there's 4 separate rolls, this is a full permutation of base/base, base/enh, enh/base, enh/enh rollsWithSameSecondaryPerks.length === 4 ) { const rollGroup = (rollsGroupedByPrimaryPerks[commonPrimaryPerksKey] ??= { commonPrimaryPerks, rolls: [], }); rollGroup.rolls.push(...rollsWithSameSecondaryPerks); } // otherwise, this is a unique set of rows. deliver them as-is, keyed by their non-grouped perks else { const theseRollsGroupedByPrimaryPerks = Object.groupBy( allRolls, (roll) => roll.primaryPerkIdentifier, ); for (const primaryPerkKey in theseRollsGroupedByPrimaryPerks) { const rollsWithSamePrimaryPerks = theseRollsGroupedByPrimaryPerks[primaryPerkKey]; (rollsGroupedByPrimaryPerks[primaryPerkKey] ??= { commonPrimaryPerks: rollsWithSamePrimaryPerks[0].primaryPerksList, rolls: [], }).rolls.push(...rollsWithSamePrimaryPerks); } } } } } // Because a base perk in the wish list matches an enhanced perk on the weapon, // add enhanced perks to the wish list rolls if the weapon can have them and the // roll doesn't specify them for (const roll of Object.values(rollsGroupedByPrimaryPerks)) { for (const perk of roll.commonPrimaryPerks) { const enhancedPerk = enhancedVersion(perk); if (enhancedPerk && !roll.commonPrimaryPerks.includes(enhancedPerk)) { const socketIndex = socketIndexByPerkHash[perk]; if ( socketIndex !== undefined && item.sockets?.allSockets.some( (s) => s.socketIndex === socketIndex && s.plugOptions.some((p) => p.plugDef.hash === enhancedPerk), ) ) { roll.commonPrimaryPerks.push(enhancedPerk); } } } } return Object.values(rollsGroupedByPrimaryPerks); } function isMajorPerk(item?: DestinyInventoryItemDefinition) { return Boolean( item && (item.inventory!.tierType === TierType.Common || item.itemCategoryHashes?.includes(ItemCategoryHashes.WeaponModsFrame) || item.itemCategoryHashes?.includes(ItemCategoryHashes.WeaponModsIntrinsic)), ); } // input // [ // [drop mag, smallbore], // [drop mag, extended barrel], // [tac mag, rifled barrel], // [tac mag, extended barrel] // ] // return // [ // [[drop mag], [smallbore, extended barrel]], // [[tac mag], [rifled barrel, extended barrel]] // ] export function consolidateSecondaryPerks(initialRolls: Roll[]) { // these are legit socketIndices according the item def. this might be like, [3, 4] const allSecondarySocketIndices = Array.from( new Set(initialRolls.flatMap((r) => r.secondarySocketIndices)), ).sort((a, b) => a - b); // newClusteredRolls collapses perks into an array with no blank spaces, // so we'll use this to iterate our new structure. // if above is [3, 4], this would be [0, 1]. basically array.keys const rollIndices = allSecondarySocketIndices.map((_, i) => i); let newClusteredRolls = initialRolls // ignore rolls with no secondary perks in them .filter((r) => r.secondarySocketIndices.length) .map((r) => allSecondarySocketIndices.map((i) => { const perkHash = r.secondaryPerksMap[i]; return perkHash ? { perks: [perkHash], key: `${perkHash}` } : { perks: [], key: `` }; }), ); // we iterate through the perk columns, looking for stuff to collapse for (const index of rollIndices) { // we repeatedly look for things to collapse until there are none while (true) { // find a bundle that matches another bundle, in every column except our current one const perkBundleToConsolidate = newClusteredRolls.find((r1) => newClusteredRolls.some( (r2) => r1 !== r2 && rollIndices.every((i) => i === index || r1[i].key === r2[i].key), ), ); // if nothing's found, we've collapsed as much as we can if (!perkBundleToConsolidate) { break; } const [bundlesToCombine, bundlesToLeaveAlone] = partition(newClusteredRolls, (r) => rollIndices.every((i) => i === index || perkBundleToConsolidate[i].key === r[i].key), ); // set aside the uninvolved bundles newClusteredRolls = bundlesToLeaveAlone; // build a new bundle with the same other columns, but add together the perks in this column const newPerkBundle = perkBundleToConsolidate.with( index, combineColumns(bundlesToCombine.map((b) => b[index])), ); newClusteredRolls.push(newPerkBundle); } } return newClusteredRolls.map((c) => c.map((r) => r.perks)); } interface PerkMeta { hash: number; type: 'curated' | 'both' | 'rolled'; } export type PerkColumnsMeta = PerkMeta[][]; function getBaseEnhancedPerkPair(perkHash: number) { let base = unenhancedVersion(perkHash); let enhanced = enhancedVersion(perkHash); if (!base && !enhanced) { return; } if (!enhanced) { enhanced = enhancedVersion(base!)!; } if (!base) { base = unenhancedVersion(enhanced)!; } return { base, enhanced }; } // given an enhanceable/enhanced perk, returns a key referring to both. // given anything else, returns just a stringified hash function normalizePerkKey(perkHash: number) { const bep = getBaseEnhancedPerkPair(perkHash); return bep ? `${bep.base}/${bep.enhanced}` : `${perkHash}`; } function combineColumns( columns: { perks: number[]; key: string; }[], ) { const perks = [...new Set(columns.flatMap((c) => c.perks))].sort(); return { perks, key: perks.join(), }; } ================================================ FILE: src/app/bungie-api/README.md ================================================ # Bungie API Helpers These files export methods for dealing with the Bungie API (D1, D2, User, etc) in terms of DIM concepts. They include error handling and retry logic. ================================================ FILE: src/app/bungie-api/__snapshots__/http-client.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`check Request builder for [Function getItem] 1`] = ` { "body": null, "headers": Headers { Symbol(map): { "X-API-Key": [ "123", ], }, }, "method": "GET", "url": "https://www.bungie.net/Platform/Destiny2/3/Profile/123456/Item/0987654321/?components=7", } `; exports[`check Request builder for [Function getLinkedProfiles] 1`] = ` { "body": null, "headers": Headers { Symbol(map): { "X-API-Key": [ "123", ], }, }, "method": "GET", "url": "https://www.bungie.net/Platform/Destiny2/3/Profile/123456/LinkedProfiles/?getAllMemberships=true", } `; exports[`check Request builder for [Function getProfile] 1`] = ` { "body": null, "headers": Headers { Symbol(map): { "X-API-Key": [ "123", ], }, }, "method": "GET", "url": "https://www.bungie.net/Platform/Destiny2/3/Profile/123456/?components=1%2C2%2C3", } `; exports[`check Request builder for [Function getVendor] 1`] = ` { "body": null, "headers": Headers { Symbol(map): { "X-API-Key": [ "123", ], }, }, "method": "GET", "url": "https://www.bungie.net/Platform/Destiny2/3/Profile/123456/Character/1234658790/Vendors/45674576/?components=400%2C402%2C300%2C301%2C304%2C305%2C306%2C307%2C600%2C308%2C310%2C309", } `; exports[`check Request builder for [Function pullFromPostmaster] 1`] = ` { "body": { "data": [ 123, 34, 99, 104, 97, 114, 97, 99, 116, 101, 114, 73, 100, 34, 58, 34, 49, 50, 51, 52, 54, 53, 56, 55, 57, 48, 34, 44, 34, 109, 101, 109, 98, 101, 114, 115, 104, 105, 112, 84, 121, 112, 101, 34, 58, 51, 44, 34, 105, 116, 101, 109, 73, 100, 34, 58, 34, 48, 57, 56, 55, 54, 53, 52, 51, 50, 49, 34, 44, 34, 105, 116, 101, 109, 82, 101, 102, 101, 114, 101, 110, 99, 101, 72, 97, 115, 104, 34, 58, 52, 53, 54, 55, 52, 53, 55, 54, 44, 34, 115, 116, 97, 99, 107, 83, 105, 122, 101, 34, 58, 55, 44, 34, 116, 114, 97, 110, 115, 102, 101, 114, 84, 111, 86, 97, 117, 108, 116, 34, 58, 116, 114, 117, 101, 125, ], "type": "Buffer", }, "headers": Headers { Symbol(map): { "Content-Type": [ "application/json", ], "X-API-Key": [ "123", ], }, }, "method": "POST", "url": "https://www.bungie.net/Platform/Destiny2/Actions/Items/PullFromPostmaster/", } `; ================================================ FILE: src/app/bungie-api/authenticated-fetch.ts ================================================ import { t } from 'app/i18next-t'; import { infoLog, warnLog } from 'app/utils/log'; import { PlatformErrorCodes } from 'bungie-api-ts/user'; import { HttpStatusError } from './http-client'; import { getAccessTokenFromRefreshToken } from './oauth'; import { Tokens, getToken, hasTokenExpired, removeAccessToken } from './oauth-tokens'; /** * A fatal token error means we have to log in again. */ export class FatalTokenError extends Error { constructor(msg: string) { super(msg); this.name = 'FatalTokenError'; } } /** * A wrapper around "fetch" that implements Bungie's OAuth scheme. This either * includes a cached token, refreshes a token then includes the refreshed token, * or bounces us back to login. */ export async function fetchWithBungieOAuth( request: RequestInfo | URL, options?: RequestInit, triedRefresh = false, ): Promise { if (!(request instanceof Request)) { request = new Request(request); } try { const token = await getActiveToken(); request.headers.set('Authorization', `Bearer ${token.accessToken.value}`); } catch (e) { if (e instanceof FatalTokenError) { warnLog('bungie auth', 'Unable to get auth token', e); } throw e; } // clone is us trying to work around "Body has already been consumed." in retry. const response = await fetch(request.clone(), options); if (await responseIndicatesBadToken(response)) { if (triedRefresh) { // Give up throw new FatalTokenError( "Access token expired, and we've already tried to refresh. Failing.", ); } // OK, Bungie has told us our access token is expired or // invalid. Refresh it and try again. infoLog('bungie auth', 'Access token expired, removing access token and trying again'); removeAccessToken(); return fetchWithBungieOAuth(request, options, true); } return response; } async function responseIndicatesBadToken(response: Response) { if (response.status === 401) { return true; } try { const data = (await response.clone().json()) as { ErrorCode: PlatformErrorCodes } | undefined; return Boolean( data && (data.ErrorCode === PlatformErrorCodes.AccessTokenHasExpired || data.ErrorCode === PlatformErrorCodes.WebAuthRequired || // (also means the access token has expired) data.ErrorCode === PlatformErrorCodes.WebAuthModuleAsyncFailed || data.ErrorCode === PlatformErrorCodes.AuthorizationRecordRevoked || data.ErrorCode === PlatformErrorCodes.AuthorizationRecordExpired || data.ErrorCode === PlatformErrorCodes.AuthorizationCodeStale || data.ErrorCode === PlatformErrorCodes.AuthorizationCodeInvalid), ); } catch {} return false; } export async function getActiveToken(): Promise { const token = getToken(); if (!token) { throw new FatalTokenError('No auth token exists, redirect to login'); } const accessTokenIsValid = token && !hasTokenExpired(token.accessToken); if (accessTokenIsValid) { return token; } // Get a new token from refresh token const refreshTokenIsValid = token && !hasTokenExpired(token.refreshToken); if (!refreshTokenIsValid) { throw new FatalTokenError('Refresh token invalid, clearing auth tokens & going to login'); } try { return await getAccessTokenFromRefreshToken(token.refreshToken!); } catch (e) { return handleRefreshTokenError(e); } } function handleRefreshTokenError(error: unknown): Promise { if (error instanceof TypeError) { warnLog( 'bungie auth', "Error getting auth token from refresh token because there's no internet connection (or a permissions issue). Not clearing token.", error, ); throw error; } if (!(error instanceof HttpStatusError)) { warnLog( 'bungie auth', 'Other error getting auth token from refresh token. Not clearing auth tokens', error, ); throw error; } let data; if (error.responseBody) { try { data = JSON.parse(error.responseBody) as { error?: string; error_description?: string; ErrorCode?: PlatformErrorCodes; }; } catch {} } if (data) { if (data.error === 'server_error') { switch (data.error_description) { case 'SystemDisabled': throw new Error(t('BungieService.Maintenance')); case 'RefreshTokenNotYetValid': case 'AccessTokenHasExpired': case 'AuthorizationCodeInvalid': case 'AuthorizationRecordExpired': case 'AuthorizationRecordRevoked': case 'AuthorizationCodeStale': throw new FatalTokenError( `Refresh token expired or not valid, platform error ${data.error_description}`, ); default: throw new Error( `Unknown error getting response token: ${data.error}, ${data.error_description}`, ); } } if (data.ErrorCode) { switch (data.ErrorCode) { case PlatformErrorCodes.RefreshTokenNotYetValid: case PlatformErrorCodes.AccessTokenHasExpired: case PlatformErrorCodes.AuthorizationCodeInvalid: case PlatformErrorCodes.AuthorizationRecordExpired: case PlatformErrorCodes.AuthorizationRecordRevoked: case PlatformErrorCodes.AuthorizationCodeStale: throw new FatalTokenError( `Refresh token expired or not valid, platform error ${data.ErrorCode}`, ); default: break; } } } switch (error.status) { case -1: throw new Error( "Error getting auth token from refresh token because there's no internet connection. Not clearing token.", ); case 401: case 403: { throw new FatalTokenError(`Refresh token expired or not valid, status ${error.status}`); } } throw new Error( `Unknown error getting response token. status: ${error.status}, response: ${ error.responseBody ?? 'No response body' }`, ); } ================================================ FILE: src/app/bungie-api/bungie-api-utils.ts ================================================ import { HttpClientConfig, HttpQueryParams } from 'bungie-api-ts/http'; export const API_KEY = $DIM_FLAVOR !== 'dev' ? $DIM_WEB_API_KEY : localStorage.getItem('apiKey')!; export function bungieApiUpdate(path: string, data?: Record): HttpClientConfig { return { method: 'POST', url: `https://www.bungie.net${path}`, body: data, }; } export function bungieApiQuery(path: string, params?: HttpQueryParams): HttpClientConfig { return { method: 'GET', url: `https://www.bungie.net${path}`, params, }; } export function oauthClientId(): string { return $DIM_FLAVOR !== 'dev' ? $DIM_WEB_CLIENT_ID : localStorage.getItem('oauthClientId')!; } export function oauthClientSecret(): string { return $DIM_FLAVOR !== 'dev' ? $DIM_WEB_CLIENT_SECRET : localStorage.getItem('oauthClientSecret')!; } ================================================ FILE: src/app/bungie-api/bungie-core-api.ts ================================================ import { CoreSettingsConfiguration, getCommonSettings, getGlobalAlerts as getGlobalAlertsApi, GlobalAlert, } from 'bungie-api-ts/core'; import { unauthenticatedHttpClient } from './bungie-service-helper'; /** * Get global alerts (like maintenance warnings) from Bungie. */ export async function getGlobalAlerts(): Promise { const response = await getGlobalAlertsApi(unauthenticatedHttpClient, {}); return response.Response; } /** * Get Bungie.net settings, which includes constants about Destiny 2. */ export async function getBungieNetSettings(): Promise { const response = await getCommonSettings(unauthenticatedHttpClient); return response.Response; } ================================================ FILE: src/app/bungie-api/bungie-service-helper.ts ================================================ import { t } from 'app/i18next-t'; import { showNotification } from 'app/notifications/notifications'; import { DimError } from 'app/utils/dim-error'; import { errorLog, infoLog } from 'app/utils/log'; import { PlatformErrorCodes } from 'bungie-api-ts/destiny2'; import { HttpClient, HttpClientConfig } from 'bungie-api-ts/http'; import { throttle } from 'es-toolkit'; import { DimItem } from '../inventory/item-types'; import { FatalTokenError, fetchWithBungieOAuth } from './authenticated-fetch'; import { API_KEY } from './bungie-api-utils'; import { BungieError, HttpStatusError, createFetchWithNonStoppingTimeout, createHttpClient, responsivelyThrottleHttpClient, } from './http-client'; import { rateLimitedFetch } from './rate-limiter'; const TIMEOUT = 15000; const notifyTimeout = throttle( (startTime: number, timeout: number) => { // Only notify if the timeout fired around the right time - this guards against someone pausing // the tab and coming back in an hour, for example if (navigator.onLine && Math.abs(Date.now() - (startTime + timeout)) <= 1000) { showNotification({ type: 'warning', title: t('BungieService.Slow'), body: t('BungieService.SlowDetails'), duration: 15000, }); } }, 5 * 60 * 1000, // 5 minutes { edges: ['leading'] }, ); const logThrottle = (timesThrottled: number, waitTime: number, url: string) => infoLog( 'bungie api', 'Throttled', timesThrottled, 'times, waiting', waitTime, 'ms before calling', url, ); // it would be really great if they implemented the pipeline operator soon /** used for most Bungie API requests */ export const authenticatedHttpClient = dimErrorHandledHttpClient( responsivelyThrottleHttpClient( createHttpClient( rateLimitedFetch( createFetchWithNonStoppingTimeout(fetchWithBungieOAuth, TIMEOUT, notifyTimeout), ), API_KEY, ), logThrottle, ), ); /** used to get manifest and global alerts */ export const unauthenticatedHttpClient = dimErrorHandledHttpClient( responsivelyThrottleHttpClient( createHttpClient(createFetchWithNonStoppingTimeout(fetch, TIMEOUT, notifyTimeout), API_KEY), logThrottle, ), ); /** * wrap HttpClient in handling specific to DIM, using i18n strings, bounce to login, etc */ function dimErrorHandledHttpClient(httpClient: HttpClient): HttpClient { return async (config: HttpClientConfig) => { try { return await httpClient(config); } catch (e) { handleErrors(e); } }; } /** * if HttpClient throws an error (js, Bungie, http) this enriches it with DIM concepts and then re-throws it */ export function handleErrors(error: unknown): never { if (error instanceof DOMException && error.name === 'AbortError') { throw ( navigator.onLine ? new DimError('BungieService.SlowResponse') : new DimError('BungieService.NotConnected') ).withError(error); } if (error instanceof SyntaxError) { errorLog('bungie api', 'Error parsing Bungie.net response', error); throw new DimError('BungieService.Difficulties').withError(error); } if (error instanceof TypeError) { // fetch throws this when the user is offline (and a number of other more static cases) // https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#exceptions throw ( navigator.onLine ? new DimError('BungieService.NotConnectedOrBlocked') : new DimError('BungieService.NotConnected') ).withError(error); } if (error instanceof FatalTokenError) { throw new DimError('BungieService.NotLoggedIn').withError(error); } if (error instanceof HttpStatusError) { // Token expired and other auth maladies if (error.status === 401 || error.status === 403) { throw new DimError('BungieService.NotLoggedIn').withError(error); } // 526 = cloudflare // We don't catch 500s because the Bungie.net API started returning 500 for legitimate game conditions if (error.status >= 502 && error.status <= 526) { throw new DimError('BungieService.Difficulties').withError(error); } // if no specific other http error throw new DimError( 'BungieService.NetworkError', t('BungieService.NetworkError', { status: error.status, statusText: error.message, }), ).withError(error); } // See https://github.com/DestinyDevs/BungieNetPlatform/wiki/Enums#platformerrorcodes if (error instanceof BungieError) { switch (error.code ?? -1) { case PlatformErrorCodes.DestinyVendorNotFound: throw new DimError('BungieService.VendorNotFound').withError(error); case PlatformErrorCodes.AuthorizationCodeInvalid: case PlatformErrorCodes.AccessNotPermittedByApplicationScope: throw new DimError('BungieService.AppNotPermitted').withError(error); case PlatformErrorCodes.SystemDisabled: throw new DimError('BungieService.Maintenance').withError(error); case PlatformErrorCodes.ThrottleLimitExceededMinutes: case PlatformErrorCodes.ThrottleLimitExceededMomentarily: case PlatformErrorCodes.ThrottleLimitExceededSeconds: case PlatformErrorCodes.PerApplicationThrottleExceeded: case PlatformErrorCodes.PerApplicationAnonymousThrottleExceeded: case PlatformErrorCodes.PerApplicationAuthenticatedThrottleExceeded: case PlatformErrorCodes.PerUserThrottleExceeded: throw new DimError('BungieService.Throttled').withError(error); case PlatformErrorCodes.DestinyThrottledByGameServer: throw new DimError('BungieService.Difficulties').withError(error); case PlatformErrorCodes.AccessTokenHasExpired: case PlatformErrorCodes.WebAuthRequired: case PlatformErrorCodes.WebAuthModuleAsyncFailed: // means the access token has expired throw new DimError('BungieService.NotLoggedIn').withError(error); case PlatformErrorCodes.DestinyAccountNotFound: if (error.endpoint.includes('/Account/') && !error.endpoint.includes('/Character/')) { throw new DimError('BungieService.NoAccount').withError(error); } else { throw new DimError('BungieService.Difficulties').withError(error); } case PlatformErrorCodes.DestinyLegacyPlatformInaccessible: throw new DimError('BungieService.DestinyLegacyPlatform').withError(error); // These just need a custom error message because people ask questions all the time case PlatformErrorCodes.DestinyCannotPerformActionAtThisLocation: throw new DimError('BungieService.DestinyCannotPerformActionAtThisLocation').withError( error, ); case PlatformErrorCodes.DestinyItemUnequippable: throw new DimError('BungieService.DestinyItemUnequippable').withError(error); case PlatformErrorCodes.ApiInvalidOrExpiredKey: case PlatformErrorCodes.ApiKeyMissingFromRequest: case PlatformErrorCodes.OriginHeaderDoesNotMatchKey: if ($DIM_FLAVOR === 'dev') { throw new DimError('BungieService.DevVersion').withError(error); } else { throw new DimError('BungieService.Difficulties').withError(error); } case PlatformErrorCodes.DestinyUnexpectedError: throw new DimError('BungieService.Difficulties').withError(error); default: { throw new DimError( 'BungieService.UnknownError', t('BungieService.UnknownError', { message: error.message }), ).withError(error); } } } // Any other error errorLog('bungie api', 'No response data:', error); throw new DimError('BungieService.Difficulties').withError(error); } // Handle "DestinyUniquenessViolation" (1648) export function handleUniquenessViolation(error: unknown, item: DimItem): never { if ( error instanceof BungieError && error.code === PlatformErrorCodes.DestinyUniquenessViolation ) { throw new DimError( 'BungieService.ItemUniquenessExplanation', t('BungieService.ItemUniquenessExplanation', { name: item.name, }), ).withError(error); } throw error; } ================================================ FILE: src/app/bungie-api/destiny1-api.ts ================================================ import { Vendor } from 'app/destiny1/vendors/vendor.service'; import { t } from 'app/i18next-t'; import { compareBy } from 'app/utils/comparators'; import { DimError } from 'app/utils/dim-error'; import { errorLog } from 'app/utils/log'; import { DestinyEquipItemResults, PlatformErrorCodes, ServerResponse, } from 'bungie-api-ts/destiny2'; import { DestinyAccount } from '../accounts/destiny-account'; import { D1GetAccountResponse, D1GetAdvisorsResponse, D1GetInventoryResponse, D1GetProgressionResponse, D1GetVaultInventoryResponse, D1StoresData, } from '../destiny1/d1-manifest-types'; import { DimItem } from '../inventory/item-types'; import { D1Store, DimStore } from '../inventory/store-types'; import { bungieApiQuery, bungieApiUpdate } from './bungie-api-utils'; import { authenticatedHttpClient, handleUniquenessViolation } from './bungie-service-helper'; /** * APIs for interacting with Destiny 1 game data. * * DestinyService at https://destinydevs.github.io/BungieNetPlatform/docs/Endpoints */ export async function getCharacters(account: DestinyAccount) { const response = await authenticatedHttpClient>( bungieApiQuery( `/D1/Platform/Destiny/${account.originalPlatformType}/Account/${account.membershipId}/`, ), ); if (!response || Object.keys(response.Response).length === 0) { throw new DimError( 'BungieService.NoAccountForPlatform', t('BungieService.NoAccountForPlatform', { account: account.platformLabel, }), ); } return response.Response.data; } export async function getStores(account: DestinyAccount): Promise { const { characters, inventory: profileInventory } = await getCharacters(account); const characterIds = characters.map((c) => c.characterBase.characterId); const [vaultInventory, characterInventories, characterProgressions, characterAdvisors] = await Promise.all([ getVaultInventory(account), getDestinyInventories(account, characterIds), getDestinyProgression(account, characterIds) // Don't let failure of progression fail other requests. .catch((e) => { errorLog('bungie api', 'Failed to load character progression', e); return []; }), getDestinyAdvisors(account, characterIds) // Don't let failure of advisors fail other requests. .catch((e) => { errorLog('bungie api', 'Failed to load advisors', e); return []; }), ] as const); return { characters: characters.map((c, i) => ({ id: characterIds[i], character: c, inventory: characterInventories[i], progression: characterProgressions[i], advisors: characterAdvisors[i], })), profileInventory, vaultInventory, }; } function getDestinyInventories(account: DestinyAccount, characterIds: string[]) { // Guardians const promises = characterIds.map(async (characterId) => { const response = await authenticatedHttpClient>( bungieApiQuery( `/D1/Platform/Destiny/${account.originalPlatformType}/Account/${account.membershipId}/Character/${characterId}/Inventory/`, ), ); return response.Response.data; }); return Promise.all(promises); } async function getVaultInventory(account: DestinyAccount) { const response = await authenticatedHttpClient>( bungieApiQuery(`/D1/Platform/Destiny/${account.originalPlatformType}/MyAccount/Vault/`), ); return response.Response.data; } async function getDestinyProgression(account: DestinyAccount, characterIds: string[]) { const promises = characterIds.map(async (characterId) => { const response = await authenticatedHttpClient>( bungieApiQuery( `/D1/Platform/Destiny/${account.originalPlatformType}/Account/${account.membershipId}/Character/${characterId}/Progression/`, ), ); return response.Response.data; }); return Promise.all(promises); } async function getDestinyAdvisors(account: DestinyAccount, characterIds: string[]) { const promises = characterIds.map(async (characterId) => { const response = await authenticatedHttpClient>( bungieApiQuery( `/D1/Platform/Destiny/${account.originalPlatformType}/Account/${account.membershipId}/Character/${characterId}/Advisors/V2/`, ), ); return response.Response.data; }); return Promise.all(promises); } export async function getVendorForCharacter( account: DestinyAccount, character: D1Store, vendorHash: number, ): Promise { const response = await authenticatedHttpClient>( bungieApiQuery( `/D1/Platform/Destiny/${account.originalPlatformType}/MyAccount/Character/${character.id}/Vendor/${vendorHash}/`, ), ); return response.Response.data; } export async function transfer( account: DestinyAccount, item: DimItem, store: DimStore, amount: number, ) { try { return await authenticatedHttpClient>( bungieApiUpdate('/D1/Platform/Destiny/TransferItem/', { characterId: store.isVault ? item.owner : store.id, membershipType: account.originalPlatformType, itemId: item.id, itemReferenceHash: item.hash, stackSize: amount || item.amount, transferToVault: store.isVault, }), ); } catch (e) { return handleUniquenessViolation(e, item); } } export function equip(account: DestinyAccount, item: DimItem) { return authenticatedHttpClient>( bungieApiUpdate('/D1/Platform/Destiny/EquipItem/', { characterId: item.owner, membershipType: account.originalPlatformType, itemId: item.id, }), ); } /** * Equip items in bulk. Returns a mapping from item ID to error code for each item. */ export async function equipItems( account: DestinyAccount, store: DimStore, items: DimItem[], ): Promise<{ [itemInstanceId: string]: PlatformErrorCodes }> { // Sort exotics to the end. See https://github.com/DestinyItemManager/DIM/issues/323 const itemIds = items.toSorted(compareBy((i) => i.isExotic)).map((i) => i.id); const response = await authenticatedHttpClient>( bungieApiUpdate('/D1/Platform/Destiny/EquipItems/', { characterId: store.id, membershipType: account.originalPlatformType, itemIds, }), ); const data = response.Response; return Object.fromEntries(data.equipResults.map((r) => [r.itemInstanceId, r.equipStatus])); } export function setItemState( account: DestinyAccount, item: DimItem, storeId: string, lockState: boolean, type: 'lock' | 'track', ) { let method; switch (type) { case 'lock': method = 'SetLockState'; break; case 'track': method = 'SetQuestTrackedState'; break; } return authenticatedHttpClient( bungieApiUpdate(`/D1/Platform/Destiny/${method}/`, { characterId: storeId, membershipType: account.originalPlatformType, itemId: item.id, state: lockState, }), ); } ================================================ FILE: src/app/bungie-api/destiny2-api.ts ================================================ import { t } from 'app/i18next-t'; import { InGameLoadout } from 'app/loadout/loadout-types'; import { compareBy } from 'app/utils/comparators'; import { DimError } from 'app/utils/dim-error'; import { errorLog } from 'app/utils/log'; import { AwaAuthorizationResult, AwaType, BungieMembershipType, DestinyComponentType, DestinyLinkedProfilesResponse, DestinyManifest, DestinyProfileResponse, DestinyVendorResponse, DestinyVendorsResponse, PlatformErrorCodes, ServerResponse, awaGetActionToken, awaInitializeRequest, clearLoadout, equipItem, equipItems as equipItemsApi, equipLoadout, getDestinyManifest, getLinkedProfiles, getProfile as getProfileApi, getVendor as getVendorApi, getVendors as getVendorsApi, pullFromPostmaster, setItemLockState, setQuestTrackedState, snapshotLoadout, transferItem, updateLoadoutIdentifiers, } from 'bungie-api-ts/destiny2'; import { DestinyAccount } from '../accounts/destiny-account'; import { DimItem } from '../inventory/item-types'; import { DimStore } from '../inventory/store-types'; import { reportException } from '../utils/sentry'; import { authenticatedHttpClient, handleUniquenessViolation, unauthenticatedHttpClient, } from './bungie-service-helper'; /** * APIs for interacting with Destiny 2 game data. * * Destiny2 Service at https://destinydevs.github.io/BungieNetPlatform/docs/Endpoints */ /** * Get the information about the current manifest. */ export async function getManifest(): Promise { const response = await getDestinyManifest(unauthenticatedHttpClient); return response.Response; } export async function getLinkedAccounts( bungieMembershipId: string, ): Promise { const response = await getLinkedProfiles(authenticatedHttpClient, { membershipId: bungieMembershipId, membershipType: BungieMembershipType.BungieNext, getAllMemberships: true, }); return response.Response; } /** * Get the user's stores on this platform. This includes characters, vault, and item information. */ export function getStores(platform: DestinyAccount): Promise { const components = [ DestinyComponentType.Profiles, DestinyComponentType.ProfileInventories, DestinyComponentType.ProfileCurrencies, DestinyComponentType.Characters, DestinyComponentType.CharacterInventories, DestinyComponentType.CharacterProgressions, DestinyComponentType.CharacterEquipment, // TODO: consider loading less item data, and then loading item details on click? Makes searches hard though. DestinyComponentType.ItemInstances, DestinyComponentType.ItemObjectives, DestinyComponentType.ItemSockets, DestinyComponentType.ItemCommonData, DestinyComponentType.Collectibles, DestinyComponentType.ItemPlugStates, DestinyComponentType.ItemReusablePlugs, // TODO: We should try to defer this until the popup is open! DestinyComponentType.ItemPlugObjectives, // TODO: we should defer this unless you're on the collections screen DestinyComponentType.Records, DestinyComponentType.Metrics, DestinyComponentType.StringVariables, DestinyComponentType.ProfileProgression, DestinyComponentType.Transitory, DestinyComponentType.CharacterLoadouts, DestinyComponentType.PresentationNodes, // This is a lot of data and currently not used. // DestinyComponentType.Craftables, ]; return getProfile(platform, ...components); } /** * Get just character info for all a user's characters on the given platform. No inventory, just enough to refresh stats. */ export function getCharacters(platform: DestinyAccount): Promise { return getProfile(platform, DestinyComponentType.Characters); } /** * Get parameterized profile information for the whole account. Pass in components to select what * you want. This can handle just characters, full inventory, vendors, kiosks, activities, etc. */ async function getProfile( platform: DestinyAccount, ...components: DestinyComponentType[] ): Promise { const response = await getProfileApi(authenticatedHttpClient, { destinyMembershipId: platform.membershipId, membershipType: platform.originalPlatformType, components, }); // TODO: what does it actually look like to not have an account? if (Object.keys(response.Response).length === 0) { throw new DimError( 'BungieService.NoAccountForPlatform', t('BungieService.NoAccountForPlatform', { platform: platform.platformLabel, }), ); } return response.Response; } export async function getVendors( account: DestinyAccount, characterId: string, ): Promise { const response = await getVendorsApi(authenticatedHttpClient, { characterId, destinyMembershipId: account.membershipId, membershipType: account.originalPlatformType, components: [ DestinyComponentType.Vendors, DestinyComponentType.VendorSales, DestinyComponentType.ItemCommonData, DestinyComponentType.CurrencyLookups, ], }); return response.Response; } /** a single-vendor API fetch, focused on getting the sale item details. see loadAllVendors */ export async function getVendorSaleComponents( account: DestinyAccount, characterId: string, vendorHash: number, ): Promise { const response = await getVendorApi(authenticatedHttpClient, { characterId, destinyMembershipId: account.membershipId, membershipType: account.originalPlatformType, components: [ DestinyComponentType.ItemInstances, DestinyComponentType.ItemObjectives, DestinyComponentType.ItemSockets, DestinyComponentType.ItemPlugStates, DestinyComponentType.ItemReusablePlugs, // TODO: We should try to defer this until the popup is open! DestinyComponentType.ItemPlugObjectives, ], vendorHash, }); return response.Response; } /** * Transfer an item to another store. */ export async function transfer( account: DestinyAccount, item: DimItem, store: DimStore, amount: number, ): Promise> { const request = { characterId: store.isVault || item.location.inPostmaster ? item.owner : store.id, membershipType: account.originalPlatformType, itemId: item.id, itemReferenceHash: item.hash, stackSize: amount || item.amount, transferToVault: store.isVault, }; const response = item.location.inPostmaster ? pullFromPostmaster(authenticatedHttpClient, request) : transferItem(authenticatedHttpClient, request); try { return await response; } catch (e) { return handleUniquenessViolation(e, item); } } export function equip(account: DestinyAccount, item: DimItem): Promise> { if (item.owner === 'vault') { // TODO: trying to track down https://sentry.io/destiny-item-manager/dim/issues/541412672/?query=is:unresolved errorLog('bungie api', 'Cannot equip to vault!'); reportException('equipVault', new Error('Cannot equip to vault')); return Promise.resolve({}) as Promise>; } return equipItem(authenticatedHttpClient, { characterId: item.owner, membershipType: account.originalPlatformType, itemId: item.id, }); } /** * Equip items in bulk. Returns a mapping from item ID to error code for each item */ export async function equipItems( account: DestinyAccount, store: DimStore, items: DimItem[], ): Promise<{ [itemInstanceId: string]: PlatformErrorCodes }> { // TODO: test if this is still broken in D2 // Sort exotics to the end. See https://github.com/DestinyItemManager/DIM/issues/323 const itemIds = items.toSorted(compareBy((i) => i.isExotic)).map((i) => i.id); const response = await equipItemsApi(authenticatedHttpClient, { characterId: store.id, membershipType: account.originalPlatformType, itemIds, }); return Object.fromEntries( response.Response.equipResults.map((r) => [r.itemInstanceId, r.equipStatus]), ); } /** * Set the lock state of an item. */ export function setLockState( account: DestinyAccount, storeId: string, item: DimItem, lockState: boolean, ): Promise> { return setItemLockState(authenticatedHttpClient, { characterId: storeId, membershipType: account.originalPlatformType, itemId: item.id, state: lockState, }); } /** * Set the tracked state of an item. */ export function setTrackedState( account: DestinyAccount, storeId: string, item: DimItem, trackedState: boolean, ): Promise> { if (!item.trackable) { throw new Error("Can't track non-trackable items"); } return setQuestTrackedState(authenticatedHttpClient, { characterId: storeId, membershipType: account.originalPlatformType, itemId: item.id, state: trackedState, }); } export async function requestAdvancedWriteActionToken( account: DestinyAccount, action: AwaType, storeId: string, item?: DimItem, ): Promise { const awaInitResult = await awaInitializeRequest(authenticatedHttpClient, { type: action, membershipType: account.originalPlatformType, affectedItemId: item ? item.id : undefined, characterId: storeId, }); const awaTokenResult = await awaGetActionToken(authenticatedHttpClient, { correlationId: awaInitResult.Response.correlationId, }); return awaTokenResult.Response; } export async function equipInGameLoadout(account: DestinyAccount, loadout: InGameLoadout) { const result = equipLoadout(authenticatedHttpClient, { loadoutIndex: loadout.index, characterId: loadout.characterId, membershipType: account.originalPlatformType, }); return result; } export async function snapshotInGameLoadout(account: DestinyAccount, loadout: InGameLoadout) { const result = snapshotLoadout(authenticatedHttpClient, { loadoutIndex: loadout.index, characterId: loadout.characterId, membershipType: account.originalPlatformType, colorHash: loadout.colorHash, iconHash: loadout.iconHash, nameHash: loadout.nameHash, }); return result; } export async function clearInGameLoadout(account: DestinyAccount, loadout: InGameLoadout) { const result = clearLoadout(authenticatedHttpClient, { loadoutIndex: loadout.index, characterId: loadout.characterId, membershipType: account.originalPlatformType, }); return result; } export async function editInGameLoadout(account: DestinyAccount, loadout: InGameLoadout) { const result = updateLoadoutIdentifiers(authenticatedHttpClient, { loadoutIndex: loadout.index, characterId: loadout.characterId, membershipType: account.originalPlatformType, colorHash: loadout.colorHash, iconHash: loadout.iconHash, nameHash: loadout.nameHash, }); return result; } ================================================ FILE: src/app/bungie-api/error-toaster.tsx ================================================ import { t } from 'app/i18next-t'; import { bungieHelpAccount, bungieHelpLink } from 'app/shell/links'; import ExternalLink from '../dim-ui/ExternalLink'; import { NotifyInput } from '../notifications/notifications'; import { AppIcon, mastodonIcon } from '../shell/icons'; /** * Generates parameters for a toaster based on an error, including DIM and Bungie social links. * * Use this for when you suspect Bungie.net is down. */ export function bungieErrorToaster(errorMessage: string | undefined): NotifyInput { return { type: 'error', title: t('BungieService.ErrorTitle'), body: ( <> {errorMessage ?? t('BungieService.Difficulties')}{' '}
{t('BungieService.Twitter')}{' '} {bungieHelpAccount}{' '}
), }; } export function dimErrorToaster(title: string, message: string, errorMessage: string): NotifyInput { return { type: 'error', title, body: ( <>
{message}
{errorMessage}
), duration: 60_000, }; } ================================================ FILE: src/app/bungie-api/http-client.test.ts ================================================ import { DestinyComponentType, getItem, getLinkedProfiles, getProfile, getVendor, pullFromPostmaster, } from 'bungie-api-ts/destiny2'; import { createHttpClient } from './http-client'; const errors = { DestinyNoRoomInDestination: { Response: 0, ErrorCode: 1642, ThrottleSeconds: 0, ErrorStatus: 'DestinyNoRoomInDestination', Message: 'There are no item slots available to transfer this item.', MessageData: {}, }, SystemDisabled: { Response: 0, ErrorCode: 5, ThrottleSeconds: 0, ErrorStatus: 'SystemDisabled', Message: 'This system is temporarily disabled for maintenance.', MessageData: {}, }, }; interface TroubleshootingResponse { req: Request; ErrorCode: number; } const makePretendFetch = (response?: any) => (req: any) => ({ // eslint-disable-next-line @typescript-eslint/no-unsafe-return json: () => ({ req: req as Request, ErrorCode: 1, ...response }), }); const pretendHttpClient = (response?: any) => createHttpClient(makePretendFetch(response) as any as typeof fetch, '123'); const cases: [(...params: any) => any, object | undefined][] = [ [ getItem, { destinyMembershipId: '123456', membershipType: 3, itemInstanceId: '0987654321', components: [7], }, ], [ getProfile, { destinyMembershipId: '123456', membershipType: 3, components: [1, 2, 3], }, ], [ getLinkedProfiles, { membershipId: '123456', membershipType: 3, getAllMemberships: true, }, ], [ getVendor, { characterId: '1234658790', destinyMembershipId: '123456', membershipType: 3, components: [ DestinyComponentType.Vendors, DestinyComponentType.VendorSales, DestinyComponentType.ItemInstances, DestinyComponentType.ItemObjectives, DestinyComponentType.ItemStats, DestinyComponentType.ItemSockets, DestinyComponentType.ItemTalentGrids, DestinyComponentType.ItemCommonData, DestinyComponentType.CurrencyLookups, DestinyComponentType.ItemPlugStates, DestinyComponentType.ItemReusablePlugs, // TODO: We should try to defer this until the popup is open! DestinyComponentType.ItemPlugObjectives, ], vendorHash: 45674576, }, ], [ pullFromPostmaster, { characterId: '1234658790', membershipType: 3, itemId: '0987654321', itemReferenceHash: 45674576, stackSize: 7, transferToVault: true, }, ], ]; test.each(cases)('check Request builder for %p', async (apiFunc, apiFuncParams) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const request: TroubleshootingResponse = await apiFunc(pretendHttpClient(), apiFuncParams); const { headers, method, url, body } = request.req; expect({ headers, method, url, body }).toMatchSnapshot(); }); test('should throw an error if there is no room in the destination', async () => { expect.assertions(1); await expect(async () => { (await pullFromPostmaster(pretendHttpClient(errors.DestinyNoRoomInDestination), { characterId: '1234658790', membershipType: 3, itemId: '0987654321', itemReferenceHash: 45674576, stackSize: 7, })) as any; }).rejects.toMatchInlineSnapshot( `[BungieError: There are no item slots available to transfer this item.]`, ); }); test('should throw an error if API is down for maintenance', async () => { expect.assertions(1); await expect(async () => { (await pullFromPostmaster(pretendHttpClient(errors.SystemDisabled), { characterId: '1234658790', membershipType: 3, itemId: '0987654321', itemReferenceHash: 45674576, stackSize: 7, })) as any; }).rejects.toMatchInlineSnapshot( `[BungieError: This system is temporarily disabled for maintenance.]`, ); }); ================================================ FILE: src/app/bungie-api/http-client.ts ================================================ import { convertToError } from 'app/utils/errors'; import { delay } from 'app/utils/promises'; import { PlatformErrorCodes, ServerResponse } from 'bungie-api-ts/destiny2'; import { HttpClient, HttpClientConfig } from 'bungie-api-ts/http'; /** * an error indicating a non-200 response code */ export class HttpStatusError extends Error { status: number; responseBody?: string; constructor(response: Response, responseBody?: string) { super(responseBody ?? response.statusText); this.status = response.status; this.responseBody = responseBody; } } export async function toHttpStatusError(response: Response) { try { const responseBody = await response.text(); return new HttpStatusError(response, responseBody); } catch { return new HttpStatusError(response); } } /** * an error indicating the Bungie API sent back a parseable response, * and that response indicated the request was not successful */ export class BungieError extends Error { code?: PlatformErrorCodes; status?: string; endpoint: string; constructor( response: Partial, 'Message' | 'ErrorCode' | 'ErrorStatus'>>, request: Request, ) { super(response.Message ?? 'Unknown Bungie Error'); this.name = 'BungieError'; this.code = response.ErrorCode; this.status = response.ErrorStatus; this.endpoint = request.url; } } /** * this is a non-affecting pass-through for successful http requests, * but throws JS errors for a non-200 response */ async function throwHttpError(response: Response) { if (response.status < 200 || response.status >= 400) { throw await toHttpStatusError(response); } } /** * sometimes what you have looks like a Response but it's actually an Error * * this is a non-affecting pass-through for successful API interactions, * but throws JS errors for "successful" fetches with Bungie error information */ function throwBungieError(serverResponse: T | undefined, request: Request) { if (!serverResponse || typeof serverResponse !== 'object') { return serverResponse; } // There's an alternate error response that can be returned during maintenance const eMessage = 'error' in serverResponse && 'error_description' in serverResponse && (serverResponse.error_description as string); if (eMessage) { throw new BungieError( { Message: eMessage, ErrorCode: PlatformErrorCodes.DestinyUnexpectedError, ErrorStatus: eMessage, }, request, ); } if ('ErrorCode' in serverResponse && serverResponse.ErrorCode !== PlatformErrorCodes.Success) { throw new BungieError(serverResponse as Partial>, request); } return serverResponse; } // // FETCH UTILS // /** * returns a fetch-like that will run a function if the request is taking a long time, * e.g. generate a "still waiting!" notification * * @param fetchFunction use this function to make the request * @param timeout run onTimeout after this many milliseconds * @param onTimeout the request's startTime and timeout will be passed to this */ export function createFetchWithNonStoppingTimeout( fetchFunction: typeof fetch, timeout: number, onTimeout: (startTime: number, timeout: number) => void, ): typeof fetch { return async (...[input, init]: Parameters) => { const startTime = Date.now(); const timer = setTimeout(() => onTimeout(startTime, timeout), timeout); try { return await fetchFunction(input, init); } finally { if (timer !== undefined) { clearTimeout(timer); } } }; } // // HTTPCLIENT UTILS // export function createHttpClient(fetchFunction: typeof fetch, apiKey: string): HttpClient { return async (config: HttpClientConfig) => { let url = config.url; if (config.params) { url = `${url}?${new URLSearchParams(config.params).toString()}`; } const fetchOptions = new Request(url, { method: config.method, body: config.body ? JSON.stringify(config.body) : undefined, headers: { 'X-API-Key': apiKey, ...(config.body ? { 'Content-Type': 'application/json' } : undefined), }, credentials: 'omit', }); if ($featureFlags.simulateBungieMaintenance) { throw new BungieError( { ErrorCode: PlatformErrorCodes.SystemDisabled, ErrorStatus: 'SystemDisabled', Message: 'This system is temporarily disabled for maintenance.', }, fetchOptions, ); } const response = await fetchFunction(fetchOptions); let data: T | undefined; let parseError: Error | undefined; try { data = (await response.json()) as T; } catch (e) { parseError = convertToError(e); } // try throwing bungie errors, which have more information, first throwBungieError(data, fetchOptions); // then throw errors on generic http error codes await throwHttpError(response); if (parseError) { throw parseError; } return data!; // At this point it's not undefined, there would've been a parse error }; } let timesThrottled = 0; /** * accepts an HttpClient and returns it with added throttling. throttles by increasing amounts * as it encounters Bungie API responses that indicate we should back off the requests, and * passes any thrown errors upstream * * @param httpClient use this client to make the API request * @param onThrottle run this when throttling happens. information about the throttling is passed in */ export function responsivelyThrottleHttpClient( httpClient: HttpClient, onThrottle: (timesThrottled: number, waitTime: number, url: string) => void, ): HttpClient { return async (config: HttpClientConfig): Promise => { if (timesThrottled > 0) { // Double the wait time, starting with 1 second, until we reach 5 minutes. const waitTime = Math.min(5 * 60 * 1000, Math.pow(2, timesThrottled) * 500); onThrottle(timesThrottled, waitTime, config.url); await delay(waitTime); } try { const result = await httpClient(config); // Quickly heal from being throttled timesThrottled = Math.floor(timesThrottled / 2); return result; } catch (e) { if (e instanceof BungieError) { switch (e.code) { case PlatformErrorCodes.ThrottleLimitExceededMinutes: case PlatformErrorCodes.ThrottleLimitExceededMomentarily: case PlatformErrorCodes.ThrottleLimitExceededSeconds: case PlatformErrorCodes.DestinyThrottledByGameServer: case PlatformErrorCodes.PerApplicationThrottleExceeded: case PlatformErrorCodes.PerApplicationAnonymousThrottleExceeded: case PlatformErrorCodes.PerApplicationAuthenticatedThrottleExceeded: case PlatformErrorCodes.PerUserThrottleExceeded: timesThrottled++; break; default: break; } } throw e; } }; } ================================================ FILE: src/app/bungie-api/oauth-tokens.ts ================================================ /* Helpers for storing and retrieving our OAuth tokens from localStorage */ /** * An OAuth token, either authorization or refresh. */ export interface Token { /** The oauth token key */ value: string; /** The token expires this many seconds after it is acquired. */ expires: number; name: 'access' | 'refresh'; /** A UTC epoch milliseconds timestamp representing when the token was acquired. */ inception: number; } export interface Tokens { accessToken: Token; refreshToken?: Token; bungieMembershipId: string; } /** * This service manages storage and management of saved OAuth * authorization and refresh tokens. * * See https://bungie-net.github.io/multi/index.html#about-security for details about * Bungie.net OAuth. */ const localStorageKey = 'authorization'; /** * Get all token information from saved storage. */ export function getToken(): Tokens | null { const tokenString = localStorage.getItem(localStorageKey); return tokenString ? (JSON.parse(tokenString) as Tokens) : null; } /** * Save all the information about access/refresh tokens. */ export function setToken(token: Tokens) { localStorage.setItem(localStorageKey, JSON.stringify(token)); } /** * Clear any saved token information. */ export function removeToken() { localStorage.removeItem(localStorageKey); } /** * Returns whether or not we have a token that could be refreshed. */ export function hasValidAuthTokens() { const token = getToken(); if (!token) { return false; } // Get a new token from refresh token const refreshTokenIsValid = token && !hasTokenExpired(token.refreshToken); return refreshTokenIsValid; } /** * Clear any saved access token information. */ export function removeAccessToken() { const token = getToken(); if (token) { // Force expiration token.accessToken.inception = 0; token.accessToken.expires = 0; setToken(token); } } /** * Get an absolute UTC epoch milliseconds timestamp for either the 'expires' property. * @return UTC epoch milliseconds timestamp */ function getTokenExpiration(token?: Token): number { if (token && 'inception' in token && 'expires' in token) { const inception = token.inception; return inception + token.expires * 1000; } return 0; } /** * Has the token expired, based on its 'expires' property? */ export function hasTokenExpired(token?: Token) { if (!token) { return true; } const expires = getTokenExpiration(token); const now = Date.now(); // if (token) // { log("Expires: " + token.name + " " + ((expires <= now)) + " " + ((expires - now) / 1000 / 60)); } return now > expires; } ================================================ FILE: src/app/bungie-api/oauth.ts ================================================ import { infoLog } from 'app/utils/log'; import { dedupePromise } from 'app/utils/promises'; import { oauthClientId, oauthClientSecret } from './bungie-api-utils'; import { toHttpStatusError } from './http-client'; import { Token, Tokens, setToken } from './oauth-tokens'; // all these api url params don't match our variable naming conventions const TOKEN_URL = 'https://www.bungie.net/platform/app/oauth/token/'; /** * Get a new token given a valid refresh token. This can throw with a * full HTTP response! */ export const getAccessTokenFromRefreshToken = dedupePromise( async (refreshToken: Token): Promise => { const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken.value, client_id: oauthClientId(), client_secret: oauthClientSecret(), }); const response = await fetch(TOKEN_URL, { method: 'POST', body, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); if (response.ok) { const token = handleAccessToken((await response.json()) as OauthTokenResponse); setToken(token); infoLog('bungie auth', 'Successfully updated auth token from refresh token.'); return token; } else { throw await toHttpStatusError(response); } }, ); export async function getAccessTokenFromCode(code: string): Promise { const body = new URLSearchParams({ grant_type: 'authorization_code', code, client_id: oauthClientId(), client_secret: oauthClientSecret(), }); const response = await fetch(TOKEN_URL, { method: 'POST', body, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); if (response.ok) { return handleAccessToken((await response.json()) as OauthTokenResponse); } else { throw await toHttpStatusError(response); } } interface OauthTokenResponse { access_token: string; expires_in: number; membership_id: string; refresh_token?: string; refresh_expires_in: number; } function handleAccessToken(response: OauthTokenResponse | undefined): Tokens { if (response?.access_token) { const data = response; const inception = Date.now(); const accessToken: Token = { value: data.access_token, expires: data.expires_in, name: 'access', inception, }; const tokens: Tokens = { accessToken, bungieMembershipId: data.membership_id, }; if (data.refresh_token) { tokens.refreshToken = { value: data.refresh_token, expires: data.refresh_expires_in, name: 'refresh', inception, }; } return tokens; } else { throw new Error(`No data or access token in response: ${JSON.stringify(response)}`); } } ================================================ FILE: src/app/bungie-api/rate-limit-config.ts ================================================ import { addLimiter, RateLimiterQueue } from './rate-limiter'; export default function setupRateLimiter() { addLimiter(new RateLimiterQueue(/www\.bungie\.net\/D1\/Platform\/Destiny\/TransferItem/, 1000)); addLimiter(new RateLimiterQueue(/www\.bungie\.net\/D1\/Platform\/Destiny\/EquipItem/, 1000)); // Destiny 2 has a faster rate limit! addLimiter( new RateLimiterQueue(/www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/TransferItem/, 100), ); addLimiter( new RateLimiterQueue( /www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/PullFromPostmaster/, 100, ), ); addLimiter( new RateLimiterQueue(/www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/EquipItem/, 100), ); addLimiter( new RateLimiterQueue(/www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/EquipItems/, 100), ); addLimiter( new RateLimiterQueue( /www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/InsertSocketPlugFree/, 500, ), ); addLimiter( new RateLimiterQueue( /www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/InsertSocketPlug/, 500, ), ); addLimiter( new RateLimiterQueue(/www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/SetLockState/, 100), ); addLimiter( new RateLimiterQueue( /www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/SetTrackedState/, 1000, ), ); addLimiter( new RateLimiterQueue( /www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/EquipLoadout/, 1000, ), ); addLimiter( new RateLimiterQueue( /www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/SnapshotLoadout/, 1000, ), ); addLimiter( new RateLimiterQueue( /www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/UpdateLoadoutIdentifiers/, 1000, ), ); addLimiter( new RateLimiterQueue( /www\.bungie\.net\/Platform\/Destiny2\/Actions\/Items\/ClearLoadout/, 1000, ), ); addLimiter( new RateLimiterQueue( /www\.bungie\.net\/Platform\/Destiny2\/\d+\/Profile\/\d+\/Character\/\d+\/Vendors\/\d+\//, 100, ), ); } ================================================ FILE: src/app/bungie-api/rate-limiter.ts ================================================ import { noop } from 'app/utils/functions'; /** * A rate limiter queue applies when the path of a request matches its regex. It will implement the semantics of * Bungie.net's rate limiter (expressed in API docs via ThrottleSecondsBetweenActionPerUser), which requires that * we wait a specified amount between the start of certain actions. */ export class RateLimiterQueue { pattern: RegExp; /** In milliseconds */ timeLimit: number; queue: { fetcher: typeof fetch; request: RequestInfo | URL; options?: RequestInit; resolver: (value?: any) => void; rejecter: (value?: any) => void; }[] = []; /** number of requests in the current period */ count = 0; /** The time the latest request started */ lastRequestTime = window.performance.now(); timer?: number; constructor(pattern: RegExp, timeLimit: number) { this.pattern = pattern; this.timeLimit = timeLimit; } matches(url: string) { return url.match(this.pattern); } // Add a request to the queue, acting on it immediately if possible add(fetcher: typeof fetch, request: RequestInfo | URL, options?: RequestInit): Promise { let resolver: (value?: any) => void = noop; let rejecter: (value?: any) => void = noop; const promise = new Promise((resolve, reject) => { resolver = resolve; rejecter = reject; }); this.queue.push({ fetcher, request, options, resolver, rejecter, }); this.processQueue(); return promise; } // Schedule processing the queue at the next soonest time. scheduleProcessing() { if (!this.timer) { const nextTryIn = Math.max( 0, this.timeLimit - (window.performance.now() - this.lastRequestTime), ); this.timer = window.setTimeout(() => { this.timer = undefined; this.processQueue(); }, nextTryIn); } } processQueue() { if (this.queue.length) { if (this.canProcess()) { const config = this.queue.shift()!; this.count++; this.lastRequestTime = window.performance.now(); config .fetcher(config.request, config.options) .finally(() => { this.count--; this.processQueue(); }) .then(config.resolver, config.rejecter); } else { this.scheduleProcessing(); } } } // Returns whether or not we can process a request right now. canProcess() { const currentRequestTime = window.performance.now(); const timeSinceLastRequest = currentRequestTime - this.lastRequestTime; return timeSinceLastRequest >= this.timeLimit && this.count === 0; } } const limiters: RateLimiterQueue[] = []; export function addLimiter(queue: RateLimiterQueue) { limiters.push(queue); } /** * Produce a version of "fetch" that respects global rate limiting rules. */ export function rateLimitedFetch(fetcher: typeof fetch): typeof fetch { return (request: RequestInfo | URL, options?: RequestInit) => { const url = request instanceof Request ? request.url : request.toString(); let limiter; for (const possibleLimiter of limiters) { if (possibleLimiter.matches(url)) { limiter = possibleLimiter; break; } } if (limiter) { return limiter.add(fetcher, request, options); } else { return fetcher(request, options); } }; } ================================================ FILE: src/app/character-tile/CharacterHeaderXP.m.scss ================================================ @use '../variables.scss' as *; $xp-bar-height: 2px; .xpBar { position: absolute; width: 100%; bottom: 0; } .levelBar { position: absolute; height: $xp-bar-height; width: 100%; left: 0; bottom: -$xp-bar-height; background: #666; } .levelBarProgress { background-color: $xp; height: 100%; &.moteProgress { background-color: rgb(40, 132, 179); } } ================================================ FILE: src/app/character-tile/CharacterHeaderXP.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'levelBar': string; 'levelBarProgress': string; 'moteProgress': string; 'xpBar': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/character-tile/CharacterHeaderXP.tsx ================================================ import { t } from 'app/i18next-t'; import { D1ProgressionHashes } from 'app/search/d1-known-values'; import { percent } from 'app/shell/formatters'; import clsx from 'clsx'; import { PressTip } from '../dim-ui/PressTip'; import { D1Store } from '../inventory/store-types'; import * as styles from './CharacterHeaderXP.m.scss'; function getLevelBar(store: D1Store) { const prestige = store.progressions.find( (p) => p.progressionHash === D1ProgressionHashes.Prestige, ); let levelBar = store?.percentToNextLevel ?? 0; let xpTillMote: string | undefined = undefined; if (prestige) { levelBar = prestige.progressToNextLevel / prestige.nextLevelAt; xpTillMote = t('Stats.Prestige', { level: prestige.level, exp: prestige.nextLevelAt - prestige.progressToNextLevel, }); } return { levelBar, xpTillMote, }; } // This is just a D1 feature, so it only accepts a D1 store. export default function CharacterHeaderXPBar({ store }: { store: D1Store }) { const { levelBar, xpTillMote } = getLevelBar(store); return (
); } ================================================ FILE: src/app/character-tile/CharacterTile.m.scss ================================================ @use '../variables.scss' as *; // Shared styles between vault and character tile .tileCommon { flex: 1; position: relative; height: $emblem-height; max-width: $emblem-width; text-align: left; white-space: nowrap; box-sizing: border-box; // Set the text off from the background text-shadow: 1px 1px 1px rgb(0, 0, 0, 0.5), 0 0 10px rgb(0, 0, 0, 0.5); :global(.app-icon) { filter: drop-shadow(1px 1px 1px rgb(0, 0, 0, 0.5)) drop-shadow(0 0 10px rgb(0, 0, 0, 0.5)); } } .characterTile { composes: tileCommon; display: grid; grid-template-areas: 'emblem class power' 'emblem bottom bottom'; grid-template-columns: 36px 1fr min-content; grid-template-rows: min-content 1fr; gap: 0 6px; padding: 0 6px; // Use the emblem as a background background-size: $emblem-width $emblem-height; background-position: left center; background-repeat: no-repeat; @include phone-portrait { grid-template-areas: 'emblem class power' 'emblem bottom maxTotalPower'; } } .vaultTile { composes: tileCommon; display: grid; grid-template-areas: 'emblem class power' !important; grid-template-columns: 46px 1fr min-content; grid-template-rows: 1fr; align-items: center; box-sizing: border-box; gap: 0 6px; padding: 0 6px; // The vault needs a little border to stand out against some backgrounds border: 1px solid rgb(0, 0, 0, 0.3); border-right: none; background-size: cover; background-repeat: no-repeat; background-color: rgb(49, 50, 51); background-image: url('images/vault-background.svg'); @include phone-portrait { grid-template-areas: 'emblem class vaultCapacity' !important; } } // The "current character" triangle .current::before { content: ''; position: absolute; top: 0; left: 0; border-top: 13px solid var(--theme-accent-primary); border-right: 13px solid transparent; } .bigText { font-size: 18px; margin-top: 2px; } .smallText { font-size: 12px; line-height: 10px; } // Either the equipped title, or the character race .bottom { composes: smallText; grid-area: bottom; min-width: 0; // prevents expanding beyond the grid cell with long contents display: flex; align-items: stretch; } // The emblem is only shown for D1 and the vault - D2 bakes it into the // background .emblem { grid-area: emblem; place-self: center; width: 32px; height: 32px; } .vaultEmblem { grid-area: emblem; place-self: center; height: 40px; width: 40px; } .vaultName { font-size: 16px; grid-area: class; text-overflow: ellipsis; overflow: hidden; color: white; min-width: 0; // prevents expanding beyond the grid cell with long contents } // The class name (Hunter, Titan, etc) .class { composes: bigText; grid-area: class; text-overflow: ellipsis; overflow: hidden; color: white; min-width: 0; // prevents expanding beyond the grid cell with long contents } // Current power level .powerLevel { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Destiny Symbols'; font-weight: 500; color: $power; // The power icon :global(.app-icon) { vertical-align: 70%; font-size: 40%; margin-right: 2px; } } .bigPowerLevel { composes: bigText; grid-area: power; .vaultTile & { font-size: 16px; } } .smallPowerLevel { text-align: right; :global(.app-icon) { font-size: 80%; } } // The max power shown on mobile under the current power .maxTotalPower { composes: smallText; grid-area: maxTotalPower; justify-self: self-end; } // Detailed info about vault capacity, shown on mobile. .vaultCapacity { grid-area: vaultCapacity; display: grid; grid-template-columns: repeat(2, 16px minmax(min-content, 1fr)); grid-auto-rows: 16px; gap: 3px 2px; } // The currently equipped title (from a seal) .title { display: flex; flex-direction: row; font-style: italic; color: $sealtitle; text-shadow: 1px 1px 1px rgb(0, 0, 0, 0.5), 0 0 10px rgb(0, 0, 0, 0.5); width: 100%; > *:first-child { flex-shrink: 1; overflow: hidden; text-overflow: ellipsis; padding-right: 3px; } } .gildedCurrentSeason { color: $gildedtitle; text-shadow: 1px 1px 1px rgb(0, 0, 0, 0.5), 0 0 10px rgb(0, 0, 0, 0.5); } .gildedIcon { font-style: normal; margin-left: 4px; } .gildedNum { font-size: 10px; font-style: normal; margin-left: 1px; vertical-align: super; line-height: 0; } ================================================ FILE: src/app/character-tile/CharacterTile.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bigPowerLevel': string; 'bigText': string; 'bottom': string; 'characterTile': string; 'class': string; 'current': string; 'emblem': string; 'gildedCurrentSeason': string; 'gildedIcon': string; 'gildedNum': string; 'maxTotalPower': string; 'powerLevel': string; 'smallPowerLevel': string; 'smallText': string; 'tileCommon': string; 'title': string; 'vaultCapacity': string; 'vaultEmblem': string; 'vaultName': string; 'vaultTile': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/character-tile/CharacterTile.tsx ================================================ import FractionalPowerLevel from 'app/dim-ui/FractionalPowerLevel'; import type { DimStore, DimTitle } from 'app/inventory/store-types'; import { allPowerLevelsSelector, powerLevelSelector } from 'app/inventory/store/selectors'; import { AppIcon, powerActionIcon } from 'app/shell/icons'; import { useIsPhonePortrait } from 'app/shell/selectors'; import VaultCapacity from 'app/store-stats/VaultCapacity'; import { RootState } from 'app/store/types'; import clsx from 'clsx'; import { FontGlyphs } from 'data/font/d2-font-glyphs'; import { memo } from 'react'; import { useSelector } from 'react-redux'; import * as styles from './CharacterTile.m.scss'; const gildedIcon = String.fromCodePoint(FontGlyphs.gilded_title); /** * Render a basic character tile without any event handlers * This is currently being shared between StoreHeading and CharacterTileButton */ export default memo(function CharacterTile({ store }: { store: DimStore }) { const isPhonePortrait = useIsPhonePortrait(); if (store.isVault) { return ; } return (
{store.destinyVersion === 1 && ( )}
{store.className}
{store.titleInfo ? : store.race} </div> <div className={clsx(styles.powerLevel, styles.bigPowerLevel)}> <AppIcon icon={powerActionIcon} /> {store.powerLevel} </div> {isPhonePortrait && <MaxTotalPower store={store} />} </div> ); }); function VaultTile({ store }: { store: DimStore }) { const isPhonePortrait = useIsPhonePortrait(); const powerLevel = Object.values(useSelector(allPowerLevelsSelector))[0]; return ( <div className={styles.vaultTile}> <img className={styles.vaultEmblem} src={store.icon} height={40} width={40} alt="" /> <div className={styles.vaultName}>{store.className}</div> {!isPhonePortrait && ( <div className={clsx(styles.powerLevel, styles.bigPowerLevel)}> <AppIcon icon={powerActionIcon} /> <FractionalPowerLevel power={powerLevel.dropPower} /> </div> )} {isPhonePortrait && ( <div className={styles.vaultCapacity}> <VaultCapacity /> <span className={clsx(styles.powerLevel, styles.smallPowerLevel)}> <AppIcon icon={powerActionIcon} /> </span> <span className={clsx(styles.powerLevel, styles.smallPowerLevel)}> <FractionalPowerLevel power={powerLevel.dropPower} /> </span> </div> )} </div> ); } function MaxTotalPower({ store }: { store: DimStore }) { const maxTotalPower = useSelector( (state: RootState) => powerLevelSelector(state, store.id)?.maxTotalPower, ); const floorTotalPower = Math.floor(maxTotalPower || store.powerLevel); return <div className={styles.maxTotalPower}>/ {floorTotalPower}</div>; } /** An equipped Title, earned from completing a Seal */ function Title({ titleInfo }: { titleInfo: DimTitle }) { const { title, gildedNum, isGildedForCurrentSeason } = titleInfo; return ( <span className={clsx(styles.title, { [styles.gildedCurrentSeason]: isGildedForCurrentSeason })} > <span>{title}</span> {gildedNum > 0 && ( <> <span className={styles.gildedIcon}>{gildedIcon}</span> {gildedNum > 1 && <span className={styles.gildedNum}>{gildedNum}</span>} </> )} </span> ); } ================================================ FILE: src/app/character-tile/CharacterTileButton.m.scss ================================================ @use '../variables.scss' as *; .character { composes: resetButton from '../dim-ui/common.m.scss'; composes: flexRow from '../dim-ui/common.m.scss'; align-items: stretch; width: 100%; position: relative; box-sizing: border-box; max-width: $emblem-width; // Display an outline when hovering characters @include interactive($hover: true, $active: true, $focus: true) { outline: none; &::after { content: ''; display: block; position: absolute; inset: -4px; border: 1px solid white; pointer-events: none; } } } ================================================ FILE: src/app/character-tile/CharacterTileButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'character': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/character-tile/CharacterTileButton.tsx ================================================ import clsx from 'clsx'; import { DimStore } from '../inventory/store-types'; import CharacterTile from './CharacterTile'; import * as styles from './CharacterTileButton.m.scss'; /** Render a {CharacterTile} as a button */ export default function CharacterTileButton({ character, onClick, children, className, ref, }: { character: DimStore; onClick: (id: string) => void; children?: React.ReactNode; className?: string; ref?: React.Ref<HTMLButtonElement>; }) { const handleClick = onClick ? () => onClick(character.id) : undefined; // TODO: these should really be radio buttons (one exclusive choice among several) and be navigable with arrow keys. return ( <button type="button" onClick={handleClick} className={clsx(styles.character, className)} ref={ref} > <CharacterTile store={character} /> {children} </button> ); } ================================================ FILE: src/app/character-tile/StoreHeading.m.scss ================================================ @use '../variables.scss' as *; .loadoutButton { background-color: black; width: 16px; display: flex; justify-content: center; align-items: center; } .loadoutMenu { composes: visibleScrollbars from '../dim-ui/common.m.scss'; position: fixed; inset: 0 auto auto 0; margin: 0; width: 300px; box-sizing: border-box; max-height: calc(var(--viewport-height) - var(--header-height) - #{62px + 16px}); overflow: hidden; color: rgb(245, 245, 245, 0.6); overscroll-behavior: contain; background-color: var(--theme-dropdown-menu-bg); will-change: transform; // The phone layout version @include phone-portrait { position: fixed; width: 100vw; padding: 0; max-height: calc( var(--viewport-height) - 54px - var(--header-height) - env(safe-area-inset-bottom) ); } [role='button'] { outline: none; } } .characterHeader { max-width: $emblem-width + 16px !important; width: calc(6px + var(--character-column-width) - var(--item-margin)) !important; } ================================================ FILE: src/app/character-tile/StoreHeading.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'characterHeader': string; 'loadoutButton': string; 'loadoutMenu': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/character-tile/StoreHeading.tsx ================================================ import { useFixOverscrollBehavior } from 'app/dim-ui/useFixOverscrollBehavior'; import { usePopper } from 'app/dim-ui/usePopper'; import { t } from 'app/i18next-t'; import { isD1Store } from 'app/inventory/stores-helpers'; import LoadoutPopup from 'app/loadout/loadout-menu/LoadoutPopup'; import { Portal } from 'app/utils/temp-container'; import React, { useCallback, useRef, useState } from 'react'; import ClickOutside from '../dim-ui/ClickOutside'; import { DimStore } from '../inventory/store-types'; import { AppIcon, kebabIcon } from '../shell/icons'; import CharacterHeaderXPBar from './CharacterHeaderXP'; import CharacterTileButton from './CharacterTileButton'; import * as styles from './StoreHeading.m.scss'; // Wrap the {CharacterTile} with a button for the loadout menu and the D1 XP progress bar function CharacterHeader({ store, onClick, ref, }: { store: DimStore; onClick: () => void; ref?: React.Ref<HTMLButtonElement>; }) { return ( <CharacterTileButton ref={ref} character={store} onClick={onClick} className={styles.characterHeader} > <div className={styles.loadoutButton}> <AppIcon icon={kebabIcon} title={t('Loadouts.Loadouts')} /> </div> {!store.isVault && isD1Store(store) && <CharacterHeaderXPBar store={store} />} </CharacterTileButton> ); } /** * This is the character dropdown used at the top of the inventory page. * It will render a {CharacterTile} in addition to a button for the loadout menu */ export default function StoreHeading({ store, selectedStore, onTapped, }: { store: DimStore; /** For mobile, this is whichever store is visible at the time. */ selectedStore?: DimStore; /** Fires if a store other than the selected store is tapped. */ onTapped?: (storeId: string) => void; }) { const [loadoutMenuOpen, setLoadoutMenuOpen] = useState(false); const menuTrigger = useRef<HTMLButtonElement>(null); const handleCloseLoadoutMenu = useCallback(() => { setLoadoutMenuOpen(false); }, []); const useOnTapped = store !== selectedStore && onTapped; const openLoadoutPopup = useCallback(() => { if (useOnTapped) { onTapped(store.id); return; } setLoadoutMenuOpen((open) => !open); }, [onTapped, store.id, useOnTapped]); const loadoutMenu = loadoutMenuOpen && ( <Portal> <LoadoutMenuContents store={store} onClose={handleCloseLoadoutMenu} menuTrigger={menuTrigger} /> </Portal> ); // TODO: aria "open" return ( <> <CharacterHeader store={store} ref={menuTrigger} onClick={openLoadoutPopup} /> {loadoutMenu} </> ); } // This is broken out into its own component so that useFixOverscrollBehavior can run *only* when the menu element exists. function LoadoutMenuContents({ store, onClose, menuTrigger, }: { store: DimStore; onClose: () => void; menuTrigger: React.RefObject<HTMLButtonElement | null>; }) { const menuRef = useRef<HTMLDivElement>(null); useFixOverscrollBehavior(menuRef); usePopper({ contents: menuRef, reference: menuTrigger, placement: 'bottom-start', fixed: true, padding: 0, }); return ( <ClickOutside onClickOutside={onClose} ref={menuRef} extraRef={menuTrigger} className={styles.loadoutMenu} > <LoadoutPopup dimStore={store} onClick={onClose} /> </ClickOutside> ); } ================================================ FILE: src/app/character-tile/StoreIcon.m.scss ================================================ .classIcon { width: 16px; height: 16px; margin-right: 4px; } .label { position: relative; font-weight: bold; font-size: 13px; text-shadow: 1px 1px 2px black; } .dimmedBg { opacity: 0.6; } ================================================ FILE: src/app/character-tile/StoreIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'classIcon': string; 'dimmedBg': string; 'label': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/character-tile/StoreIcon.tsx ================================================ import ClassIcon from 'app/dim-ui/ClassIcon'; import { DimStore } from 'app/inventory/store-types'; import clsx from 'clsx'; import * as styles from './StoreIcon.m.scss'; /** * Show both the store emblem and class icon for a given store. * * Providing a label overrides the class icon. * * @param useBackground uses a portion of the emblem's banner, * which is a little more neutral, instead of the square * version of the emblem */ export function StoreIcon({ store, label, useBackground, }: { store: DimStore; label?: string; useBackground?: boolean; }) { const bgColor = store.color && `rgb(${[store.color.red, store.color.green, store.color.blue].map(Math.round).join()})`; return ( <> <img src={!useBackground ? store.icon : store.background} className={clsx({ [styles.dimmedBg]: store.isVault && label })} style={{ backgroundColor: bgColor ?? 'black' }} /> {label ? ( <span className={styles.label}>{label}</span> ) : ( !store.isVault && <ClassIcon classType={store.classType} className={styles.classIcon} /> )} </> ); } ================================================ FILE: src/app/clarity/about.ts ================================================ const clarityLinkDirect = 'https://url.d2clarity.com/websiteDIM'; export const clarityLink = `<a href='${clarityLinkDirect}' target='_blank' rel='noopener noreferrer'>Clarity</a>`; const discordLinkDirect = 'https://url.d2clarity.com/discordDIM'; export const clarityDiscordLink = `<a href='${discordLinkDirect}' target='_blank' rel='noopener noreferrer'>Clarity Discord Server</a>`; ================================================ FILE: src/app/clarity/actions.ts ================================================ import { createAction } from 'typesafe-actions'; import { ClarityCharacterStats } from './descriptions/character-stats'; import { ClarityDescription } from './descriptions/descriptionInterface'; export const loadDescriptions = createAction('CLARITY/LOAD_DESCRIPTIONS')< ClarityDescription | undefined >(); export const loadCharacterStats = createAction('CLARITY/LOAD_CHAR_STATS')< ClarityCharacterStats | undefined >(); ================================================ FILE: src/app/clarity/descriptions/ClarityDescriptions.tsx ================================================ import { languageSelector } from 'app/dim-api/selectors'; import ExternalLink from 'app/dim-ui/ExternalLink'; import { t } from 'app/i18next-t'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; /* eslint-disable css-modules/no-unused-class */ import * as styles from './Description.m.scss'; import { LinesContent, Perk } from './descriptionInterface'; const customContent = (content: LinesContent) => { if (content.link) { return <ExternalLink href={content.link}>{content.text}</ExternalLink>; } }; const joinClassNames = (classNames?: (keyof typeof styles)[]) => classNames?.map((className) => styles[className]).join(' '); /* (^|\b) : start from the beginning of the string or a word boundary [+-]? : include + or - prefixes (\d*\.)?\d+ : match numbers (including decimal values) ([xs]|ms|HP)? : optionally include 'x' multiplier, 's', 'ms' and 'HP' unit suffixes ?:[%°+] : optionally include %, ° and + suffixes \b|$ : stop at a word boundary or the end of the string */ const boldTextRegEx = /(?:^|\b)[+-]?(?:\d*\.)?\d+(?:[xs]|ms|HP)?(?:[%°+]|\b|$)/g; function applyFormatting(text: string | undefined) { if (text === undefined) { return; } // I will remove this later just need to make this arrow optional in compiler if (text === '🡅') { return ''; } const segments = []; const matches = [...text.matchAll(boldTextRegEx)]; let startIndex = 0; let n = 0; for (const match of matches) { if (match.index === undefined) { continue; } const capturedText = match[0]; segments.push(text.substring(startIndex, match.index)); segments.push(<b key={n++}>{capturedText}</b>); startIndex = match.index + capturedText.length; } if (startIndex < text.length) { segments.push(text.substring(startIndex)); } return segments; } /** * Renders the Clarity description for the provided Community Insight. * This is a cut-down version of the original from the Clarity extension. */ export default function ClarityDescriptions({ perk, className, }: { perk: Perk; className?: string; }) { const selectedLanguage = useSelector(languageSelector); if (perk.descriptions === undefined) { return null; } const description = perk.descriptions[selectedLanguage] || perk.descriptions.en; const convertedDescription = description?.map((line, i) => ( <div className={joinClassNames(line.classNames)} key={i}> {line.linesContent?.map((linesContent, i) => ( <span className={joinClassNames(linesContent.classNames)} key={i}> {linesContent.link ? customContent(linesContent) : applyFormatting(linesContent.text)} </span> ))} </div> )); return ( <div className={clsx(styles.communityDescription, className)}> <h3>{t('MovePopup.CommunityData')}</h3> {convertedDescription} </div> ); } ================================================ FILE: src/app/clarity/descriptions/Description.m.scss ================================================ @use '../../variables' as *; @use 'sass:string'; /* stylelint-disable selector-class-pattern */ .communityDescription { h3 { margin: 0; font-size: inherit; } p { margin: 0; } } // --- colors .pvp { color: hsl(0, 100%, 80%); } .pve { color: hsl(220, 100%, 75%); } .green { color: hsl(116, 58%, 65%); } .yellow { color: hsl(51, 100%, 45%); } .blue { color: hsl(180, 100%, 45%); } .purple { color: hsl(300, 100%, 81%); } // copy pasted from src/app/item-popup/PlugTooltip.m.scss with slight size tweak .enhancedArrow { &::before { content: ''; display: inline-block; width: 6px; height: 11px; mask-image: url('images/enhancedArrow.svg'); background-color: $enhancedYellow; margin-right: 3px; } } // --- random styling .center { text-align: center; } .descriptionDivider { margin-top: 3px; padding-bottom: 3px; border-top: 1px solid #ccc; } .spacer { padding-top: 5px; } .bold { font-weight: bold; } .background { background-color: hsl(0, 0%, 100%, 0.17); } .title { text-decoration: underline; } .link { a:link { color: hsl(201, 100%, 65%); } a:visited { color: hsl(201, 100%, 65%); } } .breakSpaces { white-space: break-spaces; } ================================================ FILE: src/app/clarity/descriptions/Description.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'background': string; 'blue': string; 'bold': string; 'breakSpaces': string; 'center': string; 'communityDescription': string; 'descriptionDivider': string; 'enhancedArrow': string; 'green': string; 'link': string; 'purple': string; 'pve': string; 'pvp': string; 'spacer': string; 'title': string; 'yellow': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/clarity/descriptions/character-stats.ts ================================================ export interface ClarityCharacterStats { Mobility: Mobility; Resilience: Resilience; Recovery: Recovery; Discipline: StatAbilities; Intellect: Intellect; Strength: StatAbilities; } export interface ClarityStatsVersion { lastUpdate: number; schemaVersion: string; } export interface Ability { /** D2 Manifest inventoryItem hash. This is the hash of the subclass ability plug. */ Hash: number; /** Array index represents the Character Stat tier. Cooldowns are in seconds. Rounded to 2 decimal points. Note: Rounding to 2 decimal places is solely for improving math precision when combined with Override objects. When displaying these cooldown times, it is STRONGLY recommended to round them to an integer. */ Cooldowns: number[]; /** Represents the behavior of certain abilities possessing additional scaling on their cooldown depending on the number of stored ability charges. The array's length represents the number of charges an ability has intrinsically. Numbers at every array index represent the Charge Rate scalar for the ability with [index] number of stored ability charges. As this is a Charge Rate scalar, cooldown times can be calculated by dividing the times in the Cooldowns member of abilities by the scalars in this array. Do note that this is not a required member of Ability objects and will only be present if an ability has multiple charges. (Therefore, if this property is absent, it is to be assumed that the ability only has a single charge by default) */ ChargeBasedScaling?: number[]; /** Abilities receive different amounts of 'chunk energy' from things like mods and other sources depending on their base cooldown. So let's say an armor mod gives you 10% grenade energy when popping your class ability — you'd multiply that 10% by the number listed here to arrive at the final amount you'll actually receive. An user-facing explanation of this property is available at the top level of the CharacterStat object under <ChunkEnergyScalarDescription>. */ ChunkEnergyScalar?: number; /** This number represents the scalar for how much benefit you get from active super regeneration with your super ability. This ranges from 0.8 for a Tier 1 super to 1.2 for a Tier 5 super. Active Regen refers to any source of super energy that isn't just passively waiting for it to recharge. So aside from the normal passive regen and other effects that are 'additional base super regen', these influence everything. Be it collecting orbs of power, killing enemies (including assists), taking and dealing damage, armor mods, this covers everything else. */ ActiveRegenScalar?: number; } /** Contains a locale ID that you can use to grab the description for the item in your selected language. The ID is provided in a [key].[value] format where there can be an arbitrary number of keys (though it'll be 2-3 at most). Then you can use these keys and values to query the './locale/[language code].json' files for the desired description. */ type Description = string; export interface SuperAbility { /** D2 Manifest inventoryItem hash. This is the hash of the super ability subclass plug. */ Hash: number; /** Array index represents the Character Stat tier. Cooldowns are in seconds. Rounded to 2 decimal points. Note: Rounding to 2 decimal places is solely for improving math precision when combined with Override objects. When displaying these cooldown times, it is STRONGLY recommended to round them to an integer. */ Cooldowns: number[]; } export interface Override { /** D2 Manifest inventoryItem hash. This is the "reason for the override" hash, such as an equipped exotic or aspect. */ Hash: number; /** The inventoryItem hash of each ability that is required to trigger the effects of this 'Override'. Only overrides 'Abilities' under the same Character Stat as the 'Override'. Any one of these will trigger its effect defined in the other 'Override' properties. Wildcards: if the requirements array only contains 1 item and it's a 0, any ability tied to this Character Stat will have its cooldown overwritten. Negative numbers in the array indicate filters, these will be the inventoryItem hashes of subclasses multiplied by -1. Any abilities tied to the given subclass will have their cooldowns overwritten. */ Requirements: number[]; // One of CooldownOverride, Scalar, or ChunkEnergyOverride will be set. /** Array index represents the Character Stat tier. Cooldowns are in seconds. Rounded to 2 decimal points. Overrides the cooldowns of the items listed in the 'Requirements' array before the scalar is applied. Identical to the 'Cooldowns' array of the 'Ability' object. */ CooldownOverride?: number[]; /** * Length of the array is equal to the length of the 'Requirements' array. Each item represents a multiplier to the cooldown time of the abilities (of a subclass) listed in the 'Requirements' array at the same array index. Multiple scalars can stack with each other if their requirements are met (eg. Bastion Aspect and Citan's Ramparts Exotic Gauntlets). If 'CooldownOverride' property is specified: 'Scalar's are factored in after 'CooldownOverride's. */ Scalar?: number[]; /** * Length of the array is equal to the length of the <Requirements> array. Each item represents an override of the <ChunkEnergyScalar> property of the abilities (of a subclass) listed in the <Requirements> array at the same array index. If <CooldownOverride> or <Scalar> property is specified: Time is added to the cooldown times at every tier after <CooldownOverride>s and <Scalar>s have been applied. */ ChunkEnergyOverride?: number[]; } export interface StatAbilities { Abilities: Ability[]; Overrides: Override[]; Description: Description; } export interface DescriptionArray { Description: Description; Array: number[]; } export interface Mobility extends StatAbilities { /** Represents how fast you can walk (not sprint) forward in meters per second. Array index represents the Mobility tier. Rounding beyond 2 decimal places is not recommended. */ WalkSpeed: DescriptionArray; /** Represents how fast you can walk side-to-side and backwards in meters per second (85% of Walking Speed). Array index represents the Mobility tier. Rounding beyond 2 decimal places is not recommended. */ StrafeSpeed: DescriptionArray; /** Represents how fast you can move while crouching in meters per second (55% of Walking Speed). Array index represents the Mobility tier. The speeds are represented in meters per second. Rounding beyond 2 decimal places is not recommended. */ CrouchSpeed: DescriptionArray; } export interface Recovery extends StatAbilities { /** Array index represents the Recovery tier. The numbers represent how many seconds it takes to heal from 0 to full HP. Rounding is not recommended. */ TotalRegenTime: DescriptionArray; /** Array index represents the Recovery tier. The numbers representhow many seconds after taking damage Health Regeneration starts. Rounding is not recommended. Good to know: Health is a fixed 70 HP portion of your total health alongside 'Shields' which a 115-130 HP portion of total health determined by Resilience. */ HealthRegenDelay: DescriptionArray; /** Array index represents the Recovery tier. The numbers represent how fast your health regens after the delay. The numbers are provided in % of total health per second (total health is a fixed 70HP). Rounding beyond 1-2 decimal places is not recommended. For all intents and purposes, you can divide the numbers by 100, multiply by 70, and display it as HP/second. */ HealthRegenSpeed: DescriptionArray; /** Array index represents the Recovery tier. The numbers represent how many seconds after taking damage Shield Regeneration starts. Rounding is not recommended. Good to know: Shield health is a 115-130 HP portion of total health determined by Resilience. */ ShieldRegenDelay: DescriptionArray; /** Array index represents the Recovery tier. The numbers represent how fast your shields regen after the delay. The numbers are provided in % of total shield health per second (shield health is a 115-130 HP portion of total health determined by Resilience). Rounding beyond 1-2 decimal places is not recommended. For all intents and purposes, you can take the TotalHP value at a specified Resilience tier and subtract 70 to get the shield health. After that, you can divide the ShieldRegenSpeed numbers by 100, multiply it by the selected shield health, and display it as HP/second. (Though it's probably better to leave it in % to avoid potentially causing confusion for users) */ ShieldRegenSpeed: DescriptionArray; } export interface Resilience extends StatAbilities { /** Array index represents the Resilience tier. The numbers represent how much HP your <Shields> have at each tier. <Shields> are the 115 to 130 HP 'right-side portion' of your Total HP alongside <Health>. The amount of <Health> you have depends on the activity: 100 HP in most Crucible playlists (excluding Momentum Control and Mayhem) and 70 HP everywhere else. */ ShieldHP: DescriptionArray; /** Array index represents the Resilience tier. The numbers represent the percentage damage resistance granted IN PVE at each tier. */ PvEDamageResistance: DescriptionArray; /** Array index represents the Resilience tier. The numbers represent the percentage flinch resistance granted at each tier. */ FlinchResistance: DescriptionArray; } export interface Intellect { SuperAbilities: SuperAbility[]; Overrides: Override[]; Description: Description; } ================================================ FILE: src/app/clarity/descriptions/descriptionInterface.ts ================================================ import { DimLanguage } from 'app/i18n'; export type DescriptionClassNames = | 'background' | 'blue' | 'bold' | 'breakSpaces' | 'center' | 'communityDescription' | 'descriptionDivider' | 'enhancedArrow' | 'green' | 'link' | 'purple' | 'pve' | 'pvp' | 'spacer' | 'title' | 'yellow'; export interface LinesContent { text?: string; classNames?: DescriptionClassNames[]; link?: string; } export interface Line { linesContent?: LinesContent[]; classNames?: DescriptionClassNames[]; } /** * Clarity perk */ export interface Perk { /** * Perk hash from inventoryItems */ hash: number; /** * Perk name from inventoryItems */ name: string; /** * Exotic armor / weapon hash from inventoryItems */ itemHash?: number; /** * Exotic armor / weapon name from inventoryItems */ itemName?: string; descriptions: { [key in DimLanguage]?: Line[]; }; } export interface ClarityDescription { /** ** Key is always inventory item perk hash */ [key: number]: Perk; } export interface ClarityVersions { /** ** Version format x.y ** x - major version requiring update to DIM ** y - minor version just simple update to description */ descriptions: number; } ================================================ FILE: src/app/clarity/descriptions/loadDescriptions.ts ================================================ import { get, set } from 'app/storage/idb-keyval'; import { ThunkResult } from 'app/store/types'; import { isEmpty } from 'app/utils/collections'; import { errorLog } from 'app/utils/log'; import { dedupePromise } from 'app/utils/promises'; import * as actions from '../actions'; import { ClarityCharacterStats, ClarityStatsVersion } from './character-stats'; import { ClarityDescription, ClarityVersions } from './descriptionInterface'; const CLARITY_BASE = 'https://database-clarity.github.io/'; const urls = { descriptions: `${CLARITY_BASE}Live-Clarity-Database/descriptions/dim.json`, characterStats: (version: string) => `${CLARITY_BASE}Character-Stats/versions/${version}/CharacterStatInfo-NI.json`, version: `${CLARITY_BASE}Live-Clarity-Database/versions.json`, statsVersion: `${CLARITY_BASE}Character-Stats/update.json`, } as const; const CLARITY_STATS_SUPPORTED_SCHEMA = '1.9'; const fetchClarity = async <T>(type: keyof typeof urls, version?: string) => { const url = urls[type]; const data = await fetch(typeof url === 'function' ? url(version!) : url); if (!data.ok) { throw new Error(`failed to fetch ${type}`); } const json = (await data.json()) as T; if (!json || isEmpty(json)) { throw new Error(`empty response JSON for ${type}`); } return json; }; const fetchRemoteDescriptions = async (version: number) => { const descriptions = await fetchClarity<ClarityDescription>('descriptions'); set('clarity-descriptions', descriptions); localStorage.setItem('clarityDescriptionVersion', version.toString()); return descriptions; }; const loadClarityDescriptions = dedupePromise(async (loadFromIndexedDB: boolean) => { const savedVersion = Number(localStorage.getItem('clarityDescriptionVersion') ?? '0'); let liveVersion: ClarityVersions | undefined; try { liveVersion = await fetchClarity<ClarityVersions>('version'); if (savedVersion !== liveVersion.descriptions) { return await fetchRemoteDescriptions(liveVersion.descriptions); } } catch (e) { errorLog('clarity', 'failed to load remote descriptions', e); } if (loadFromIndexedDB) { const savedDescriptions = await get<ClarityDescription>('clarity-descriptions'); return ( savedDescriptions ?? // If IDB doesn't have the data (e.g. after deleting IDB but not localStorage), fetch it (liveVersion && (await fetchRemoteDescriptions(liveVersion.descriptions))) ); } return undefined; }); const fetchRemoteStats = async (version: ClarityStatsVersion) => { const descriptions = await fetchClarity<ClarityCharacterStats>( 'characterStats', version.schemaVersion, ); set('clarity-characterStats', descriptions); localStorage.setItem('clarityStatsVersion2', JSON.stringify(version)); return descriptions; }; const loadClarityStats = dedupePromise(async (loadFromIndexedDB: boolean) => { const savedStatsValue = localStorage.getItem('clarityStatsVersion2'); const savedStats = savedStatsValue !== null ? (JSON.parse(savedStatsValue) as ClarityStatsVersion) : undefined; let liveStatsVersion: ClarityStatsVersion | undefined; try { liveStatsVersion = await fetchClarity<ClarityStatsVersion>('statsVersion'); if ( liveStatsVersion.schemaVersion === CLARITY_STATS_SUPPORTED_SCHEMA && savedStats?.lastUpdate !== liveStatsVersion.lastUpdate ) { // There's been a live update and we support the update's schema -- fetch it return await fetchRemoteStats(liveStatsVersion); } } catch (e) { errorLog('clarity', 'failed to load remote character stats', e); } if (loadFromIndexedDB) { if (savedStats?.schemaVersion === CLARITY_STATS_SUPPORTED_SCHEMA) { const savedCharacterStats = await get<ClarityCharacterStats>('clarity-characterStats'); if (savedCharacterStats) { return savedCharacterStats; } } // If IDB doesn't have the data (e.g. after deleting IDB but not localStorage), // or our IDB data has an unsupported schema version, fetch whatever we need const remoteToFetch = liveStatsVersion?.schemaVersion === CLARITY_STATS_SUPPORTED_SCHEMA ? liveStatsVersion : // NB if we're an old app release and don't support the most recent schema, // we don't get a useful `lastUpdate` value anyway, and we'll never // use this `lastUpdate` to decide when to re-fetch -- outdated apps // use whatever they have in IDB, only fetching if there's nothing useful in IDB { schemaVersion: CLARITY_STATS_SUPPORTED_SCHEMA, lastUpdate: 0 }; return fetchRemoteStats(remoteToFetch); } return undefined; }); /** Reload descriptions at most every 1 hour */ const descriptionReloadAfter = 60 * 60 * 1000; let lastDescriptionUpdate = 0; let lastStatsUpdate = 0; /** * Load the Clarity database, either remotely or from the local cache. */ export function loadClarity(): ThunkResult { return async (dispatch, getState) => { const { descriptions, characterStats } = getState().clarity; // Load if it's been long enough, or if there aren't descriptions loaded. // The latter helps if there was an error loading them - it forces the next // refresh to try again. if (!descriptions || Date.now() - lastDescriptionUpdate > descriptionReloadAfter) { const newInfo = await loadClarityDescriptions(!descriptions); if (newInfo) { dispatch(actions.loadDescriptions(newInfo)); } lastDescriptionUpdate = Date.now(); } if (!characterStats || Date.now() - lastStatsUpdate > descriptionReloadAfter) { const newInfo = await loadClarityStats(!characterStats); if (newInfo) { dispatch(actions.loadCharacterStats(newInfo)); } lastStatsUpdate = Date.now(); } }; } ================================================ FILE: src/app/clarity/reducer.ts ================================================ import { Reducer } from 'redux'; import { ActionType, getType } from 'typesafe-actions'; import * as actions from './actions'; import { ClarityCharacterStats } from './descriptions/character-stats'; import { ClarityDescription } from './descriptions/descriptionInterface'; export type ClarityAction = ActionType<typeof actions>; export interface ClarityState { /** * Descriptions from community provided by Clarity API */ descriptions?: ClarityDescription; /** * Information about character stat cooldown times. */ characterStats?: ClarityCharacterStats; } const initialState: ClarityState = {}; export const clarity: Reducer<ClarityState, ClarityAction> = ( state: ClarityState = initialState, action: ClarityAction, ): ClarityState => { switch (action.type) { case getType(actions.loadDescriptions): { const descriptions = action.payload; return { ...state, descriptions: descriptions ?? state.descriptions, }; } case getType(actions.loadCharacterStats): { const characterStats = action.payload; return { ...state, characterStats: characterStats ?? state.characterStats, }; } default: return state; } }; ================================================ FILE: src/app/clarity/selectors.ts ================================================ import { RootState } from 'app/store/types'; export const clarityDescriptionsSelector = (state: RootState) => state.clarity.descriptions; export const clarityCharacterStatsSelector = (state: RootState) => state.clarity.characterStats; ================================================ FILE: src/app/compare/Compare.m.scss ================================================ @use '../variables' as *; .organizerLink { composes: dim-button from global; margin-left: auto; @include phone-portrait { margin-left: 0; } } .bucket { white-space: nowrap; display: grid; // each item is a column of cells grid-auto-flow: column; // the first column is the header grid-template-columns: 0 min-content; // the rest are item, then separator, repeated forever grid-auto-columns: min-content 1px; // This is necessary - otherwise sticky headers will stop at one screens-width of scrolling width: max-content; // Always give enough room for the item icon min-height: calc(36px + 1lh + var(--item-size) + 8px); > *[role='cell'] { padding-left: 4px; padding-right: 4px; } } // the header and items live in a horizontal scrolling container .scroller { width: 100%; overflow-x: auto; } .options { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; :global(.setting) { margin-right: 4px; } :global(.dim-button) { // Undo the general mobile "big-buttons" padding: 4px 10px; } } .comparisonModebutton { position: relative; margin-right: 7px; &::after { content: ''; position: absolute; border-right: 1px solid #8888; inset: -4px -6px -4px 100%; } } .comparisonModeHint { display: contents; } [role='listbox'] .comparisonModeHint { display: none; } :global(.dim-button) .comparisonModeInfo { display: none; } .comparisonModeDescription { display: block; min-width: 200px; opacity: 0.7; font-size: smaller; } :global(.dim-button) .comparisonModeDescription { display: none; } .modIcon { height: 12px; } ================================================ FILE: src/app/compare/Compare.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bucket': string; 'comparisonModeDescription': string; 'comparisonModeHint': string; 'comparisonModeInfo': string; 'comparisonModebutton': string; 'modIcon': string; 'options': string; 'organizerLink': string; 'scroller': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/compare/Compare.tsx ================================================ import { CustomStatDef } from '@destinyitemmanager/dim-api-types'; import { languageSelector } from 'app/dim-api/selectors'; import Select, { Option } from 'app/dim-ui/Select'; import { useTableColumnSorts } from 'app/dim-ui/table-columns'; import { t } from 'app/i18next-t'; import { locateItem } from 'app/inventory/locate-item'; import { createItemContextSelector } from 'app/inventory/selectors'; import { ItemCreationContext } from 'app/inventory/store/d2-item-factory'; import { applySocketOverrides, useSocketOverridesForItems, } from 'app/inventory/store/override-sockets'; import { useD2Definitions } from 'app/manifest/selectors'; import { showNotification } from 'app/notifications/notifications'; import { buildStatInfo } from 'app/organizer/Columns'; import { buildRows, sortRows } from 'app/organizer/ItemTable'; import { ColumnDefinition, Row, TableContext } from 'app/organizer/table-types'; import { weaponMasterworkY2SocketTypeHash } from 'app/search/d2-known-values'; import Checkbox from 'app/settings/Checkbox'; import { useSetting } from 'app/settings/hooks'; import { Settings } from 'app/settings/initial-settings'; import { AppIcon, faList, settingsIcon, statBarsIcon } from 'app/shell/icons'; import { masterworkHammer } from 'app/shell/icons/custom/MasterworkHammer'; import { acquisitionRecencyComparator } from 'app/shell/item-comparators'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { compact } from 'app/utils/collections'; import { emptyArray } from 'app/utils/empty'; import ModificationsIcon from 'destiny-icons/general/modifications.svg?react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { Link } from 'react-router'; import Sheet from '../dim-ui/Sheet'; import { DimItem, DimSocket } from '../inventory/item-types'; import { chainComparator, compareBy } from '../utils/comparators'; import * as styles from './Compare.m.scss'; import { getColumns } from './CompareColumns'; import CompareItem, { CompareHeaders } from './CompareItem'; import CompareSuggestions from './CompareSuggestions'; import { endCompareSession, removeCompareItem, updateCompareQuery } from './actions'; import { CompareSession } from './reducer'; import { compareItemsSelector, compareOrganizerLinkSelector } from './selectors'; // TODO: CSS grid-with-sticky layout // TODO: dropdowns for query buttons // TODO: freeform query // TODO: Allow minimizing the sheet (to make selection easier) export default function Compare({ session }: { session: CompareSession }) { const dispatch = useThunkDispatch(); const defs = useD2Definitions()!; const [compareBaseStats, setCompareBaseStats] = useSetting('compareBaseStats'); const [armorCompareSetting, setArmorCompareSetting] = useSetting('armorCompare'); const [assumeWeaponMasterwork, setAssumeWeaponMasterwork] = useSetting('compareWeaponMasterwork'); const itemCreationContext = useSelector(createItemContextSelector); const rawCompareItems = useSelector(compareItemsSelector(session.vendorCharacterId)); const organizerLink = useSelector(compareOrganizerLinkSelector); /** The stat row to highlight */ const [highlight, setHighlight] = useState<string | number>(); const [socketOverrides, onPlugClicked] = useSocketOverridesForItems(); const [columnSorts, toggleColumnSort] = useTableColumnSorts([]); const comparingArmor = rawCompareItems[0]?.bucket.inArmor; const comparingWeapons = rawCompareItems[0]?.bucket.inWeapons; const armorCompareMode = $DIM_FLAVOR === 'release' ? compareBaseStats && comparingArmor ? 'base' : 'current' : armorCompareSetting; const doAssumeWeaponMasterworks = Boolean(defs && assumeWeaponMasterwork && comparingWeapons); // Produce new items which have had their sockets changed const compareItems = useMemo(() => { let items = rawCompareItems; if (doAssumeWeaponMasterworks && comparingWeapons) { // Fully masterwork weapons items = items.map((i) => masterworkWeapon(i, itemCreationContext)); } // Apply any socket override selections (perk choices) return items.map((i) => applySocketOverrides(itemCreationContext, i, socketOverrides[i.id])); }, [ itemCreationContext, doAssumeWeaponMasterworks, rawCompareItems, socketOverrides, comparingWeapons, ]); const cancel = useCallback(() => { dispatch(endCompareSession()); }, [dispatch]); // Reset if there ever are no items const hasItems = compareItems.length > 0; useEffect(() => { if (!hasItems) { showNotification({ type: 'warning', title: t('Compare.Error.Invalid'), body: session.query, }); cancel(); } }, [cancel, hasItems, session.query]); // Memoize computing the list of stats const allStats = useMemo(() => buildStatInfo(compareItems), [compareItems]); const updateQuery = useCallback( (newQuery: string) => { dispatch(updateCompareQuery(newQuery)); }, [dispatch], ); const remove = useCallback( (item: DimItem) => { if (compareItems.length <= 1) { cancel(); } else { dispatch(removeCompareItem(item)); } }, [cancel, compareItems.length, dispatch], ); // If the session was started with a specific item, this is it const initialItem = session.initialItemId ? compareItems.find((i) => i.id === session.initialItemId) : undefined; /* ItemTable incursion */ const destinyVersion = compareItems[0]?.destinyVersion ?? 2; const type = comparingArmor ? 'armor' : comparingWeapons ? 'weapon' : 'general'; const hasEnergy = compareItems.some((i) => i.energy); const primaryStatDescription = (!comparingArmor && !comparingWeapons && compareItems.find((i) => i.primaryStat)?.primaryStatDisplayProperties) || undefined; const customStats = comparingArmor ? itemCreationContext.customStats : emptyArray<CustomStatDef>(); const columns: ColumnDefinition[] = useMemo( () => getColumns( type, hasEnergy, allStats, customStats, destinyVersion, armorCompareMode, primaryStatDescription, initialItem?.id, onPlugClicked, ), [ type, hasEnergy, allStats, armorCompareMode, destinyVersion, customStats, primaryStatDescription, initialItem?.id, onPlugClicked, ], ); const classIfAny = comparingArmor ? compareItems[0]?.classType : undefined; const filteredColumns = useMemo( () => // TODO: filter to enabled columns once you can select columns compact( columns.filter( (column) => column.limitToClass === undefined || column.limitToClass === classIfAny, ), ), [columns, classIfAny], ); // process items into Rows const [unsortedRows, tableCtx] = useMemo( () => buildRows(compareItems, filteredColumns), [filteredColumns, compareItems], ); const language = useSelector(languageSelector); const rows = useMemo( () => sortRows(unsortedRows, columnSorts, filteredColumns, language, (a, b) => chainComparator( compareBy((item) => item.id !== session.initialItemId), acquisitionRecencyComparator, )(a.item, b.item), ), [unsortedRows, columnSorts, filteredColumns, language, session.initialItemId], ); /* End ItemTable incursion */ const firstCompareItem = rows[0]?.item; // The example item is the one we'll use for generating suggestion buttons const exampleItem = initialItem || firstCompareItem; const items = useMemo( () => ( <CompareItems rows={rows} tableCtx={tableCtx} filteredColumns={filteredColumns} remove={remove} setHighlight={setHighlight} onPlugClicked={onPlugClicked} /> ), [rows, tableCtx, filteredColumns, remove, onPlugClicked], ); const selectOptions: Option<Settings['armorCompare']>[] = [ { content: ( <> <span className={styles.comparisonModeHint}> <AppIcon icon={settingsIcon} /> <ModificationsIcon className={styles.modIcon} /> </span> <div className={styles.comparisonModeInfo}> <ModificationsIcon className={styles.modIcon} /> {t('Compare.CurrentStats')} <span className={styles.comparisonModeDescription}> {t('Compare.CurrentStatsDescription')} </span> </div> </> ), key: 'current', value: 'current', }, { content: ( <> <span className={styles.comparisonModeHint}> <AppIcon icon={settingsIcon} /> <AppIcon icon={statBarsIcon} /> </span> <div className={styles.comparisonModeInfo}> <AppIcon icon={statBarsIcon} /> {t('Organizer.Columns.BaseStats')} <span className={styles.comparisonModeDescription}> {t('Compare.BaseStatsDescription')} </span> </div> </> ), key: 'base', value: 'base', }, { content: ( <> <span className={styles.comparisonModeHint}> <AppIcon icon={settingsIcon} /> <AppIcon icon={masterworkHammer} /> </span> <div className={styles.comparisonModeInfo}> <AppIcon icon={masterworkHammer} /> {t('Compare.AssumeMasterworked')} <span className={styles.comparisonModeDescription}> {t('Compare.AssumeMasterworkedDescription')} </span> </div> </> ), key: 'baseMasterwork', value: 'baseMasterwork', }, ]; const header = ( <div className={styles.options}> {comparingArmor && ($DIM_FLAVOR !== 'release' ? ( <Select options={selectOptions} value={armorCompareSetting} onChange={(v) => setArmorCompareSetting(v ?? 'baseMasterwork')} className={styles.comparisonModebutton} maxDropdownWidth={500} /> ) : ( <Checkbox label={t('Compare.CompareBaseStats')} name="compareBaseStats" value={compareBaseStats} onChange={setCompareBaseStats} /> ))} {comparingWeapons && defs && destinyVersion === 2 && ( <Checkbox label={t('Compare.AssumeMasterworked')} name="compareWeaponMasterwork" value={assumeWeaponMasterwork} onChange={setAssumeWeaponMasterwork} /> )} {exampleItem && <CompareSuggestions exampleItem={exampleItem} onQueryChanged={updateQuery} />} {organizerLink && ( <Link className={styles.organizerLink} to={organizerLink}> <AppIcon icon={faList} /> <span>{t('Organizer.OpenIn')}</span> </Link> )} </div> ); const gridSpec = `min-content ${filteredColumns .map((c) => c.gridWidth ?? 'min-content') .join(' ')}`; return ( <Sheet onClose={cancel} header={header} allowClickThrough> <div className={styles.scroller}> <div className={styles.bucket} style={{ gridTemplateRows: gridSpec }} onPointerLeave={() => setHighlight(undefined)} > <CompareHeaders columnSorts={columnSorts} highlight={highlight} setHighlight={setHighlight} toggleColumnSort={toggleColumnSort} filteredColumns={filteredColumns} /> {items} </div> </div> </Sheet> ); } function CompareItems({ rows, tableCtx, filteredColumns, remove, setHighlight, onPlugClicked, }: { rows: Row[]; tableCtx: TableContext; filteredColumns: ColumnDefinition[]; remove: (item: DimItem) => void; setHighlight: React.Dispatch<React.SetStateAction<string | number | undefined>>; onPlugClicked: (value: { item: DimItem; socket: DimSocket; plugHash: number }) => void; }) { return rows.map((row) => ( <CompareItem item={row.item} row={row} tableCtx={tableCtx} filteredColumns={filteredColumns} key={row.item.id} itemClick={locateItem} remove={remove} setHighlight={setHighlight} onPlugClicked={onPlugClicked} /> )); } /** * Produce a copy of the item with the masterwork socket filled in with the best * masterwork option. */ function masterworkWeapon(i: DimItem, itemCreationContext: ItemCreationContext): DimItem { if (i.destinyVersion !== 2 || !i.sockets) { return i; } const masterworkSocket = i.sockets?.allSockets.find( (socket) => socket.socketDefinition.socketTypeHash === weaponMasterworkY2SocketTypeHash, ); const plugSet = masterworkSocket?.plugSet; const plugged = masterworkSocket?.plugged; if (plugSet && plugged) { const fullMasterworkPlug = plugSet.plugs.find( (p) => // Same stat (they each have their own plug category) p.plugDef.plug.plugCategoryHash === plugged.plugDef.plug.plugCategoryHash && // And it's got a +10 stat value somewhere p.plugDef.investmentStats.some((s) => s.value === 10) && // Edge of Fate (Tiered weapon) masterworks have zeroes in their // conditional stats, while non-tiered masterworks have 3s. Both are in // the plugset so this is the only way I know to find the right one. p.plugDef.investmentStats.some( (s) => s.isConditionallyActive && s.value === (i.tier > 0 ? 0 : 3), ), ); if (fullMasterworkPlug) { return applySocketOverrides(itemCreationContext, i, { [masterworkSocket.socketIndex]: fullMasterworkPlug.plugDef.hash, }); } } return i; } ================================================ FILE: src/app/compare/CompareButtons.m.scss ================================================ .svgIcon { img, svg { height: 1.3em; } } .inlineImageIcon { display: inline-block; vertical-align: middle; height: 1.3em; width: 1.3em; } .intrinsicIcon { transform: scale(1.2); } .statIconAdjust { width: 1.1em; object-fit: cover; } ================================================ FILE: src/app/compare/CompareButtons.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'inlineImageIcon': string; 'intrinsicIcon': string; 'statIconAdjust': string; 'svgIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/compare/CompareColumns.m.scss ================================================ @use '../variables.scss' as *; .name { padding-top: 2px; padding-bottom: 2px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; // Add the magnifying glass icons to items that can be clicked to find them &:global(.compare-findable) { cursor: pointer; &::after { content: '\f002'; // Copied from .fas classes: /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */ font-family: 'Font Awesome 5 Free'; font-weight: 900; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; display: inline-block; font-style: normal; font-variant: normal; text-rendering: auto; line-height: 1; font-size: 8px; margin-left: 4px; margin-right: 0; vertical-align: initial; } } } .initialItem { color: var(--theme-accent-primary); font-weight: bold; } .energy { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; } .stat { margin-right: calc(4px + var(--item-size)); max-width: 68px; } .noWrap { white-space: nowrap; } .archetype { --item-size-mod: calc(#{dim-item-px(24)}); composes: noWrap; padding-top: 4px; padding-bottom: 4px; } // Perks in the perk/mod columns .perks { --mod-size: calc(#{dim-item-px(24)}); padding-top: 4px; padding-bottom: 4px; @include phone-portrait { --mod-size: 26px; } } .weaponPerksHeader { align-items: flex-start !important; } // Make room for the item image .imageRoom { padding-right: calc(var(--item-size) + 16px) !important; } .archetypeRow { --archetype-size: 22px !important; composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; gap: 2px; } ================================================ FILE: src/app/compare/CompareColumns.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'archetype': string; 'archetypeRow': string; 'energy': string; 'imageRoom': string; 'initialItem': string; 'name': string; 'noWrap': string; 'perks': string; 'stat': string; 'weaponPerksHeader': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/compare/CompareColumns.tsx ================================================ import { CustomStatDef, DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { EnergyCostIcon } from 'app/dim-ui/ElementIcon'; import { t } from 'app/i18next-t'; import { DimStat } from 'app/inventory/item-types'; import { PlugClickedHandler } from 'app/inventory/store/override-sockets'; import ArchetypeSocket from 'app/item-popup/ArchetypeSocket'; import { d1QualityColumn, getIntrinsicSockets, getStatColumns, modsColumn, perksGridColumn, perkString, perkStringSort, } from 'app/organizer/Columns'; import { createCustomStatColumns } from 'app/organizer/CustomStatColumns'; import { ColumnDefinition, SortDirection, Value } from 'app/organizer/table-types'; import { quoteFilterString } from 'app/search/query-parser'; import { Settings } from 'app/settings/initial-settings'; import { compact } from 'app/utils/collections'; import { getArmorArchetype, getArmorArchetypeSocket, getWeaponArchetype, getWeaponArchetypeSocket, } from 'app/utils/socket-utils'; import { DestinyDisplayPropertiesDefinition } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import * as styles from './CompareColumns.m.scss'; import CompareStat from './CompareStat'; /** * This helper allows TypeScript to perform type inference to determine the * type of V based on its arguments. This allows us to automatically type the * various column methods like `cell` and `filter` automatically based on the * return type of `value`. */ /*@__INLINE__*/ function c<V extends Value>(columnDef: ColumnDefinition<V>): ColumnDefinition<V> { return columnDef; } /** * This function generates the columns. */ // TODO: converge this with Columns.tsx export function getColumns( itemsType: 'weapon' | 'armor' | 'general', hasEnergy: boolean, stats: DimStat[], customStatDefs: CustomStatDef[], destinyVersion: DestinyVersion, armorCompare: Settings['armorCompare'], primaryStatDescription: DestinyDisplayPropertiesDefinition | undefined, initialItemId: string | undefined, onPlugClicked: PlugClickedHandler, ): ColumnDefinition[] { const isArmor = itemsType === 'armor'; const isWeapon = itemsType === 'weapon'; const isGeneral = itemsType === 'general'; const { statColumns, baseStatColumns, baseMasterworkStatColumns, d1ArmorQualityByStat } = getStatColumns(stats, customStatDefs, destinyVersion, { isArmor, showStatLabel: true, extraStatInfo: true, className: styles.stat, }); const preferredStatColumns = !isArmor || armorCompare === 'current' ? statColumns : armorCompare === 'base' ? baseStatColumns : baseMasterworkStatColumns; const customStatColumns = createCustomStatColumns(customStatDefs, { className: styles.stat, hideFormula: true, withMasterwork: armorCompare === 'baseMasterwork', extraStatInfo: true, }); // TODO: maybe add destinyVersion / usecase to the ColumnDefinition type?? const columns: ColumnDefinition[] = compact([ c({ id: 'name', header: t('Organizer.Columns.Name'), csv: 'Name', className: styles.name, value: (i) => i.name, cell: (val, i) => ( <span className={clsx({ [styles.initialItem]: initialItemId === i.id, })} title={initialItemId === i.id ? t('Compare.InitialItem') : undefined} > {val} </span> ), filter: (name) => `name:${quoteFilterString(name)}`, }), (isArmor || isWeapon) && c({ id: 'power', csv: destinyVersion === 2 ? 'Power' : 'Light', header: t('Organizer.Columns.Power'), // We don't want to show a value for power if it's 0 value: (item) => (item.power === 0 ? undefined : item.power), cell: (val, item, ctx) => val !== undefined ? ( <CompareStat min={ctx?.min ?? 0} max={ctx?.max ?? 0} item={item} value={val} /> ) : ( t('Stats.NotApplicable') ), defaultSort: SortDirection.DESC, filter: (value) => `power:>=${value}`, }), primaryStatDescription && c({ id: 'primaryStat', header: primaryStatDescription.name, // We don't want to show a value for power if it's 0 value: (item) => item.primaryStat?.value, cell: (val, item, ctx) => val !== undefined ? ( <CompareStat min={ctx?.min ?? 0} max={ctx?.max ?? 0} item={item} value={val} /> ) : ( t('Stats.NotApplicable') ), defaultSort: SortDirection.DESC, }), hasEnergy && c({ id: 'energy', header: t('Organizer.Columns.Energy'), csv: 'Energy Capacity', className: styles.energy, value: (item) => item.energy?.energyCapacity, cell: (val, item, ctx) => val !== undefined && ( <> <EnergyCostIcon /> <CompareStat min={ctx?.min ?? 0} max={ctx?.max ?? 0} item={item} value={val} /> </> ), defaultSort: SortDirection.DESC, filter: (value) => `energycapacity:>=${value}`, }), ...preferredStatColumns, ...d1ArmorQualityByStat, destinyVersion === 1 && isArmor && d1QualityColumn, ...(destinyVersion === 2 && isArmor ? customStatColumns : []), destinyVersion === 2 && c({ id: 'archetype', header: t('Organizer.Columns.Archetype'), className: styles.archetype, headerClassName: styles.archetype, value: (item) => item.bucket.inWeapons ? getWeaponArchetype(item)?.displayProperties.name : getArmorArchetype(item)?.displayProperties.name, cell: (_val, item) => { const s = item.bucket.inWeapons ? getWeaponArchetypeSocket(item) : getArmorArchetypeSocket(item); return ( s && ( <div className={styles.archetypeRow}> <ArchetypeSocket archetypeSocket={s} item={item} /> </div> ) ); }, filter: (value) => (value ? `exactperk:${quoteFilterString(value)}` : undefined), }), (isWeapon || ((isArmor || isGeneral) && destinyVersion === 1)) && perksGridColumn( styles.perks, clsx(styles.perks, { [styles.weaponPerksHeader]: isWeapon }), onPlugClicked, initialItemId, ), destinyVersion === 2 && modsColumn( clsx(styles.perks, { [styles.imageRoom]: isGeneral }), styles.perks, isWeapon, onPlugClicked, ), // Armor intrinsic perks destinyVersion === 2 && isArmor && c({ id: 'intrinsics', className: styles.perks, header: t('Organizer.Columns.Perks'), value: (item) => { const intrinsics = getIntrinsicSockets(item); return ( // Sort by PCI first so that similar intrinsics land near each other before sub-alphabetizing `${intrinsics[0]?.plugged?.plugDef.plug.plugCategoryIdentifier ?? ''},${perkString( intrinsics, )}` ); }, cell: (_val, item) => { const sockets = getIntrinsicSockets(item); return ( <> {sockets.map((s) => ( <div className={styles.archetypeRow} key={s.socketIndex}> <ArchetypeSocket archetypeSocket={s} item={item} /> </div> ))} </> ); }, sort: perkStringSort, }), ]); return columns; } ================================================ FILE: src/app/compare/CompareContainer.tsx ================================================ import { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { gaPageView } from 'app/google'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { Suspense, lazy, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import { endCompareSession } from './actions'; import { compareSessionSelector } from './selectors'; const Compare = lazy(() => import(/* webpackChunkName: "compare" */ './Compare')); export default function CompareContainer({ destinyVersion }: { destinyVersion: DestinyVersion }) { const session = useSelector(compareSessionSelector); const show = Boolean(session); const dispatch = useThunkDispatch(); // Reset on path changes and unmount const { pathname } = useLocation(); useEffect( () => () => { dispatch(endCompareSession()); }, [dispatch, pathname], ); useEffect(() => { if (show && destinyVersion !== undefined) { gaPageView(`/profileMembershipId/d${destinyVersion}/compare`); } }, [destinyVersion, show]); if (!session) { return null; } return ( <Suspense fallback={null}> <Compare session={session} /> </Suspense> ); } ================================================ FILE: src/app/compare/CompareItem.m.scss ================================================ @use '../variables.scss' as *; /* Row Headers */ .header { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; padding-right: 16px; padding-left: 4px; cursor: pointer; position: sticky; left: 0; align-self: stretch; background-color: var(--theme-item-sheet-bg); z-index: 2; border-right: 1px solid #f5f5f540; img { height: 16px; width: 16px; vertical-align: bottom; } } .sortDesc { padding-right: 8px; color: var(--theme-accent-primary); } .sortAsc { padding-right: 8px; color: var(--theme-accent-secondary); } .highlighted { background-color: #202020; } .spacer { composes: header; cursor: default !important; } .headerContent { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; gap: 2px; } // The highlight behind each item .highlightBar { position: relative; &::after { content: ''; display: block; position: absolute; top: 0; left: 0; height: 100%; // Reserve space for always-on scrollbars :-( width: calc( 100vw - var(--scrollbar-width) - env(safe-area-inset-left) - env(safe-area-inset-right) ); z-index: -1; pointer-events: none; } &.highlighted::after { background-color: #202020; } } /* Items */ // The toolbar across the top of each item .itemActions { display: flex; flex-direction: row; align-items: center; justify-content: space-between; height: 32px; // pull & lock buttons > div:nth-child(1), > div:nth-child(2) { padding: 4px; } // Tag selector > div > div > button { padding: 3px !important; } } .headerContainer { position: relative; } .separator { width: 0; height: 100%; border-right: 1px solid #f5f5f540; grid-row: 1 / -1; // Span all columns in the grid } // The "dismiss" button in the item header .close { composes: resetButton from '../dim-ui/common.m.scss'; width: 32px; height: 32px; background-size: 16px; background-image: url('images/close.png'); background-position: center; background-repeat: no-repeat; @include interactive($hover: true) { background-color: var(--theme-accent-primary); } } // The item icon that's floated behind the stats .itemAside { position: absolute; padding: 0; right: 4px; top: calc(32px + 4px + 1lh); cursor: pointer; } ================================================ FILE: src/app/compare/CompareItem.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'close': string; 'header': string; 'headerContainer': string; 'headerContent': string; 'highlightBar': string; 'highlighted': string; 'itemActions': string; 'itemAside': string; 'separator': string; 'sortAsc': string; 'sortDesc': string; 'spacer': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/compare/CompareItem.tsx ================================================ import { PressTip } from 'app/dim-ui/PressTip'; import { useDynamicStringReplacer } from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { ColumnSort, SortDirection } from 'app/dim-ui/table-columns'; import { t } from 'app/i18next-t'; import ItemPopupTrigger from 'app/inventory/ItemPopupTrigger'; import { moveItemTo } from 'app/inventory/move-item'; import { currentStoreSelector } from 'app/inventory/selectors'; import ActionButton from 'app/item-actions/ActionButton'; import { LockActionButton, TagActionButton } from 'app/item-actions/ActionButtons'; import { useD2Definitions } from 'app/manifest/selectors'; import { ColumnDefinition, Row, TableContext } from 'app/organizer/table-types'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { noop } from 'app/utils/functions'; import { useSetCSSVarToHeight, useShiftHeld } from 'app/utils/hooks'; import { nonPullablePostmasterItem } from 'app/utils/item-utils'; import clsx from 'clsx'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import ConnectedInventoryItem from '../inventory/ConnectedInventoryItem'; import { DimItem, DimSocket } from '../inventory/item-types'; import { AppIcon, faAngleLeft, faAngleRight, faArrowCircleDown, faExclamationTriangle, shoppingCart, } from '../shell/icons'; import * as styles from './CompareItem.m.scss'; export default memo(function CompareItem({ item, row, tableCtx, filteredColumns, itemClick, remove, setHighlight, }: { item: DimItem; row: Row; tableCtx: TableContext; filteredColumns: ColumnDefinition[]; itemClick: (item: DimItem) => void; remove: (item: DimItem) => void; setHighlight: (value?: string | number) => void; onPlugClicked: (value: { item: DimItem; socket: DimSocket; plugHash: number }) => void; }) { const headerRef = useRef<HTMLDivElement>(null); useSetCSSVarToHeight(headerRef, '--compare-item-height'); const dispatch = useThunkDispatch(); const currentStore = useSelector(currentStoreSelector)!; const pullItem = useCallback(() => { dispatch(moveItemTo(item, currentStore, false)); }, [currentStore, dispatch, item]); const { pathname } = useLocation(); const isFindable = !item.vendor && pathname.endsWith('/inventory'); const itemHeader = useMemo( () => ( <div ref={headerRef} className={styles.headerContainer}> <div className={styles.itemActions}> {item.vendor ? ( <VendorItemWarning item={item} /> ) : nonPullablePostmasterItem(item) ? ( <PressTip elementType="span" tooltip={t('MovePopup.CantPullFromPostmaster')}> <ActionButton onClick={noop} disabled> <AppIcon icon={faExclamationTriangle} /> </ActionButton> </PressTip> ) : ( <ActionButton title={t('Hotkey.Pull')} onClick={pullItem}> <AppIcon icon={faArrowCircleDown} /> </ActionButton> )} {item.lockable ? <LockActionButton item={item} noHotkey /> : <div />} {item.taggable ? <TagActionButton item={item} label={false} hideKeys={true} /> : <div />} <button type="button" className={styles.close} onClick={() => remove(item)} /> </div> <ItemPopupTrigger item={item} noCompare={true}> {(ref, onClick) => ( <div className={styles.itemAside} ref={ref} onClick={onClick}> <ConnectedInventoryItem item={item} /> </div> )} </ItemPopupTrigger> </div> ), [item, pullItem, remove], ); const handleRowClick = (row: Row, column: ColumnDefinition) => { if (column.id === 'name' && isFindable) { return () => itemClick(row.item); } return undefined; }; return ( <> {itemHeader} {filteredColumns.map((column) => ( // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions <div key={column.id} onClick={handleRowClick(row, column)} className={clsx( column.className, column.id === 'name' && { 'compare-findable': isFindable, }, )} role="cell" onPointerEnter={() => setHighlight(column.id)} > {column.cell ? column.cell(row.values[column.id], row.item, tableCtx.minMaxValues[column.id]) : row.values[column.id]} </div> ))} <div className={styles.separator} /> </> ); }); function VendorItemWarning({ item }: { item: DimItem }) { const defs = useD2Definitions()!; const replacer = useDynamicStringReplacer(item.owner); return item.vendor ? ( <PressTip elementType="span" tooltip={() => { const vendorName = replacer(defs.Vendor.get(item.vendor!.vendorHash)?.displayProperties?.name) || '--'; return <>{t('Compare.IsVendorItem', { vendorName })}</>; }} > <ActionButton onClick={noop} disabled> <AppIcon icon={shoppingCart} /> </ActionButton> </PressTip> ) : null; } /** The row headers that appear on the left of the compare window */ export function CompareHeaders({ columnSorts, highlight, setHighlight, toggleColumnSort, filteredColumns, }: { columnSorts: ColumnSort[]; highlight: string | number | undefined; setHighlight: React.Dispatch<React.SetStateAction<string | number | undefined>>; toggleColumnSort: (columnId: string, shiftHeld: boolean, sort?: SortDirection) => () => void; filteredColumns: ColumnDefinition[]; }) { const isShiftHeld = useShiftHeld(); return ( <> <div key="spacer-1" className={styles.spacer} /> {filteredColumns.map((column) => ( <div key={`hl-${column.id}`} className={clsx(styles.highlightBar, { [styles.highlighted]: highlight === column.id, })} /> ))} <div key="spacer-2" className={styles.spacer} /> {filteredColumns.map((column) => { const columnSort = !column.noSort && columnSorts.find((c) => c.columnId === column.id); return ( <div key={column.id} className={clsx( styles.header, column.headerClassName, columnSort ? columnSort.sort === SortDirection.ASC ? styles.sortDesc : styles.sortAsc : undefined, { [styles.highlighted]: highlight === column.id, }, )} onPointerEnter={() => setHighlight(column.id)} onPointerLeave={() => setHighlight(undefined)} onClick={ column.noSort ? undefined : toggleColumnSort(column.id, isShiftHeld, column.defaultSort) } role="rowheader" aria-sort={ columnSort ? columnSort.sort === SortDirection.ASC ? 'ascending' : 'descending' : 'none' } > <div className={styles.headerContent}> {column.header} {columnSort && ( <AppIcon icon={columnSort.sort === SortDirection.ASC ? faAngleRight : faAngleLeft} /> )} </div> </div> ); })} </> ); } ================================================ FILE: src/app/compare/CompareStat.m.scss ================================================ @use '../variables.scss' as *; .range { font-size: 0.9em; } .stat { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; height: 16px; position: relative; font-variant-numeric: tabular-nums; gap: 4px; } .statValue { min-width: 2ch; // Try to right-align 2-digit numbers text-align: right; position: relative; white-space: nowrap; &.noMinWidth { min-width: unset; } } .statBarArea { height: 80%; position: relative; } .statBarContainer { width: 100%; } .statBarFill { display: block; background-color: rgb(255, 255, 255, 0.25); height: 100%; } .masterwork::after { content: ''; display: block; border-radius: 50%; background-color: $stat-masterworked; height: 4px; width: 4px; position: absolute; top: calc(50% - 2px); left: calc(100% + 2px); } .statExceptionIndicator { color: #fff; position: absolute; left: 0; background-color: transparent !important; line-height: 1; white-space: pre; sup { position: absolute; left: 100%; } } .customStat { opacity: 0.8; } /* stylelint-disable-next-line selector-class-pattern */ .smaller :global(.svg-inline--fa) { height: 0.8em; } ================================================ FILE: src/app/compare/CompareStat.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'customStat': string; 'masterwork': string; 'noMinWidth': string; 'range': string; 'smaller': string; 'stat': string; 'statBarArea': string; 'statBarContainer': string; 'statBarFill': string; 'statExceptionIndicator': string; 'statValue': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/compare/CompareStat.tsx ================================================ import AnimatedNumber from 'app/dim-ui/AnimatedNumber'; import RecoilStat, { recoilValue } from 'app/item-popup/RecoilStat'; import { getCompareColor, percent } from 'app/shell/formatters'; import { AppIcon, tunedStatIcon } from 'app/shell/icons'; import { artificeIcon } from 'app/shell/icons/custom/Artifice'; import { getArmor3TuningStat, isArtifice } from 'app/utils/item-utils'; import clsx from 'clsx'; import { StatHashes } from 'data/d2/generated-enums'; import { D1Stat, DimItem, DimStat } from '../inventory/item-types'; import * as styles from './CompareStat.m.scss'; export default function CompareStat({ min, max, stat, item, value, relevantStatHashes, extraStatInfo = false, }: { stat?: DimStat | D1Stat; item: DimItem; value: number; min: number; max: number; /** If this represents a custom stat, these are the real stats that custom stat includes. */ relevantStatHashes?: number[]; /** Whether to show extra stat info icons (e.g. that the total includes tuners, or that the stat is tuned) and stat bars. */ extraStatInfo?: boolean; }) { const isMasterworkStat = Boolean( item?.bucket.inWeapons && stat && item.masterworkInfo?.stats?.some((s) => s.isPrimary && s.hash === stat.statHash), ); const color = getCompareColor(statRange(stat, min, max, value)); const tunedStatHash = getArmor3TuningStat(item); // If this tuner benefits the custom stat, or this is a single real stat that's tunable const showTunerIcon = relevantStatHashes?.includes(tunedStatHash!) || Boolean(tunedStatHash && tunedStatHash === stat?.statHash); const syntheticStat = Boolean(stat?.statHash && stat.statHash < 0); // If this is Artifice armor and a custom or Total stat const showArtificeIcon = isArtifice(item) && syntheticStat; const extraIcon = showTunerIcon ? tunedStatIcon : showArtificeIcon ? artificeIcon : undefined; const showBar = stat?.bar && item.bucket.inArmor; return ( <div className={styles.stat} style={{ color }}> <AnimatedNumber value={value} className={clsx(styles.statValue, { [styles.masterwork]: isMasterworkStat, [styles.noMinWidth]: !stat || stat.statHash === StatHashes.AnyEnergyTypeCost, })} /> {item.bucket.inArmor && extraStatInfo && ( <span className={clsx(styles.statBarArea, showBar && styles.statBarContainer)}> {extraIcon && ( <span className={clsx(styles.statExceptionIndicator, { [styles.customStat]: syntheticStat, [styles.smaller]: showArtificeIcon, })} > <AppIcon icon={extraIcon} /> {showArtificeIcon && <sup>+</sup>} </span> )} {showBar && ( <span className={styles.statBarFill} style={{ width: percent(Math.max(0, value) / stat.maximumValue) }} /> )} </span> )} {stat?.statHash === StatHashes.RecoilDirection && <RecoilStat value={value} />} {Boolean(value) && stat && 'qualityPercentage' in stat && stat.qualityPercentage && Boolean(stat.qualityPercentage.range) && ( <span className={styles.range}>({stat.qualityPercentage.range})</span> )} </div> ); } // Turns a stat and a list of ranges into a 0-100 scale function statRange(stat: DimStat | D1Stat | undefined, min: number, max: number, value: number) { if (stat && 'qualityPercentage' in stat && stat.qualityPercentage) { return stat.qualityPercentage.min; } if (min === max) { return -1; } if (stat?.statHash === StatHashes.RecoilDirection) { value = recoilValue(value); } if (stat?.smallerIsBetter) { return (100 * (max - value)) / (max - min); } return (100 * (value - min)) / (max - min); } ================================================ FILE: src/app/compare/CompareSuggestions.tsx ================================================ import { DimItem } from 'app/inventory/item-types'; import { filterFactorySelector } from 'app/search/items/item-search-filter'; import { canonicalizeQuery, parseQuery } from 'app/search/query-parser'; import clsx from 'clsx'; import { memo } from 'react'; import { useSelector } from 'react-redux'; import { defaultComparisons, findSimilarArmors, findSimilarWeapons } from './compare-buttons'; import { compareCategoryItemsSelector, compareQuerySelector } from './selectors'; /** * Display a row of buttons that suggest alternate queries based on an example item. */ export default memo(function CompareSuggestions({ exampleItem, onQueryChanged, }: { exampleItem: DimItem; onQueryChanged: (query: string) => void; }) { const currentQuery = useSelector(compareQuerySelector); const categoryItems = useSelector(compareCategoryItemsSelector); const filterFactory = useSelector(filterFactorySelector); // Find all possible buttons const compareButtons = exampleItem.bucket.inArmor ? findSimilarArmors(exampleItem) : exampleItem.bucket.inWeapons ? findSimilarWeapons(exampleItem) : defaultComparisons(exampleItem); // Fill in the items that match each query const compareButtonsWithItems = compareButtons.map((button) => ({ ...button, items: categoryItems.filter(filterFactory(button.query)), })); let keptPenultimateButton = false; // Filter out useless buttons const filteredCompareButtons = compareButtonsWithItems.filter((compareButton, index) => { const nextCompareButton = compareButtonsWithItems[index + 1]; // always print the final button, unless it matched the penultimate button if (!nextCompareButton) { return !keptPenultimateButton; } // skip empty buttons or buttons that only contain the example item (except the first item-specific button and the example item specific button) if ( compareButton.items.length < 2 && !compareButton.query.includes('name:') && !compareButton.query.includes('id:') ) { return false; } // if the next button has [all of, & only] the exact same items in it if ( compareButton.items.length === nextCompareButton?.items.length && compareButton.items.every((setItem) => nextCompareButton?.items.some((nextSetItem) => nextSetItem === setItem), ) ) { // do include this button, if the next button is the "includes armor 2.0 items" button. // that's a confusing label to users with no armor 2.0 items. if (exampleItem.bucket.inArmor && !nextCompareButton?.query.includes('is:armor2.0')) { keptPenultimateButton = true; return true; } // otherwise skip it. it's a redundant button. return false; } return true; }); const parsedQuery = currentQuery && canonicalizeQuery(parseQuery(currentQuery)); return ( <> {filteredCompareButtons.map(({ query, items, buttonLabel }) => ( <button key={query} type="button" className={clsx('dim-button', { selected: parsedQuery !== undefined && canonicalizeQuery(parseQuery(query)) === parsedQuery, })} title={query} onClick={() => onQueryChanged(query)} > {buttonLabel.map((l) => (typeof l === 'string' ? <span key={l}>{l}</span> : l))} {!query.includes('id:') && <span>({items.length})</span>} </button> ))} </> ); }); ================================================ FILE: src/app/compare/actions.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import { createAction } from 'typesafe-actions'; /** Add an item to the set of compared items. If there are none already, this compares duplicates. */ export const addCompareItem = createAction('compare/ADD_ITEM')<DimItem>(); export const removeCompareItem = createAction('compare/REMOVE_ITEM')<DimItem>(); /** End a compare session (close the compare tool) */ export const endCompareSession = createAction('compare/END_SESSION')(); /** Update the query of an active compare session */ export const updateCompareQuery = createAction('compare/UPDATE_QUERY')<string>(); /** Compare items that match a search filter. */ export const compareFilteredItems = createAction( 'compare/FILTERED_ITEMS', // Do we really need the items? ( query: string, filteredItems: DimItem[], /** The first item added to compare, so we can highlight it. */ initialItem: DimItem | undefined, ) => ({ query, filteredItems, initialItem }), )(); /** Compare a specific set of items. */ export const compareSelectedItems = createAction('compare/SELECTED_ITEMS')<DimItem[]>(); ================================================ FILE: src/app/compare/compare-buttons.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import ElementIcon from 'app/dim-ui/ElementIcon'; import { ArmorSlotIcon, WeaponSlotIcon, WeaponTypeIcon } from 'app/dim-ui/ItemCategoryIcon'; import { PressTip } from 'app/dim-ui/PressTip'; import { SpecialtyModSlotIcon } from 'app/dim-ui/SpecialtyModSlotIcon'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { realD2ArmorStatSearchByHash } from 'app/search/d2-known-values'; import { quoteFilterString } from 'app/search/query-parser'; import { AppIcon, clearIcon } from 'app/shell/icons'; import { compact, filterMap } from 'app/utils/collections'; import { getArmor3StatFocus, getItemDamageShortName, getSpecialtySocketMetadata, isArmor3, } from 'app/utils/item-utils'; import { getArmorArchetype, getExtraIntrinsicPerkSockets, getIntrinsicArmorPerkSocket, getWeaponArchetype, } from 'app/utils/socket-utils'; import clsx from 'clsx'; import rarityIcons from 'data/d2/engram-rarity-icons.json'; import { BucketHashes, StatHashes } from 'data/d2/generated-enums'; import archetypeIcon from 'images/armorArchetype.png'; import React from 'react'; import * as styles from './CompareButtons.m.scss'; import { compareNameQuery, stripAdept } from './compare-utils'; /** A definition for a button on the top of the compare too, which can be clicked to show the given items. */ interface CompareButton { buttonLabel: React.ReactNode[]; /** The query that results in this list of items */ query: string; } const modernArmor = 'is:armor2.0 or is:armor3.0'; /** * Generate possible comparisons for armor, given a reference item. */ export function findSimilarArmors(exampleItem: DimItem): CompareButton[] { const exampleItemModSlotMetadata = getSpecialtySocketMetadata(exampleItem); const exampleItemIntrinsic = !exampleItem.isExotic && getIntrinsicArmorPerkSocket(exampleItem)?.plugged?.plugDef.displayProperties; const focusedStats = isArmor3(exampleItem) && getArmor3StatFocus(exampleItem); const tertiaryStatHash = focusedStats && focusedStats[2]; focusedStats && focusedStats.sort(); const focusedStatsDisplayProperties = focusedStats && focusedStats.map((h) => exampleItem.stats!.find((s) => s.statHash === h)!.displayProperties); const archetype = getArmorArchetype(exampleItem); const tertiaryStat = tertiaryStatHash && realD2ArmorStatSearchByHash[tertiaryStatHash]; const tertiaryStatDisplayProperties = tertiaryStatHash && exampleItem.stats!.find((s) => s.statHash === tertiaryStatHash)!.displayProperties; // exotic class item perks const extraIntrinsicButtons = (exampleItem.destinyVersion === 2 && filterMap( getExtraIntrinsicPerkSockets(exampleItem), (s) => s.plugged?.plugDef.displayProperties, ) ?.map((intrinsic) => ({ buttonLabel: [ <BungieImage key="1" src={intrinsic.icon} />, intrinsic.name, exampleItem.rarity === 'Legendary' ? ( <BungieImage key="rarity" src={rarityIcons.Legendary} className="dontInvert" /> ) : null, <ArmorSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, ], query: `${archetype ? 'is:armor3.0' : modernArmor} perk:${quoteFilterString(intrinsic.name)} is:${exampleItem.rarity}`, })) .reverse()) || []; return compact([ // same slot on the same class { buttonLabel: [ <ArmorSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, `+ ${t('Compare.NoModArmor')}`, ], query: '', // since we already filter by itemCategoryHash, an empty query gives you all items matching that category }, // above but also has to be modern armor (2.0 or 3.0) exampleItem.destinyVersion === 2 && { buttonLabel: [<ArmorSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />], query: modernArmor, }, // above but also has to be legendary exampleItem.destinyVersion === 2 && exampleItem.rarity === 'Legendary' && { buttonLabel: [ <BungieImage key="rarity" src={rarityIcons.Legendary} className="dontInvert" />, <ArmorSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, ], query: `${modernArmor} is:legendary`, }, // above but also the same seasonal mod slot, if it has one exampleItem.destinyVersion === 2 && exampleItemModSlotMetadata && { buttonLabel: [ <SpecialtyModSlotIcon className={styles.inlineImageIcon} key="1" item={exampleItem} />, <BungieImage key="rarity" src={rarityIcons.Legendary} className="dontInvert" />, <ArmorSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, ], query: `${modernArmor} modslot:${exampleItemModSlotMetadata.slotTag || 'none'}`, }, // above but also the same special intrinsic, if it has one exampleItem.destinyVersion === 2 && exampleItemIntrinsic && { buttonLabel: [ <PressTip minimal tooltip={exampleItemIntrinsic.name} key="1"> <BungieImage className={styles.intrinsicIcon} src={exampleItemIntrinsic.icon} /> </PressTip>, <BungieImage key="rarity" src={rarityIcons.Legendary} className="dontInvert" />, <ArmorSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, ], query: `${modernArmor} perk:${quoteFilterString(exampleItemIntrinsic.name)} is:${exampleItem.rarity}`, }, // above but only Armor 3.0 if the example item is Armor 3.0 exampleItem.destinyVersion === 2 && exampleItem.rarity === 'Legendary' && archetype && { buttonLabel: [ <img key="1" src={archetypeIcon} />, <span key="2">{t('Compare.Archetype')}</span>, <BungieImage key="rarity" src={rarityIcons.Legendary} className="dontInvert" />, <ArmorSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, ], query: `is:armor3.0 is:legendary`, }, // Try to make a group of armors 3.0 with the same archetype. exampleItem.destinyVersion === 2 && archetype && { buttonLabel: [ <BungieImage key="1" src={archetype.displayProperties.icon} />, <span key="2">{archetype.displayProperties.name}</span>, <ArmorSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, ], query: `${modernArmor} perk:${quoteFilterString(archetype.displayProperties.name)} is:${exampleItem.rarity}`, }, // Try to make a group of armors 3.0 with the exact same 3 stats focused. This is an easy win for identifying better/worse armor. exampleItem.destinyVersion === 2 && focusedStatsDisplayProperties && { buttonLabel: focusedStatsDisplayProperties.map((s, index) => ( <React.Fragment key={s.name}> {index > 0 && '+'} <BungieImage className={styles.statIconAdjust} src={s.icon} /> </React.Fragment> )), query: `is:armor3.0 is:${exampleItem.rarity} ${focusedStats.map((h) => `basestat:${realD2ArmorStatSearchByHash[h]}:>0`).join(' ')}`, }, // Try to make a group of armors 3.0 with the exact same 3 stats focused and the same archetype. This is an easy win for identifying better/worse armor. exampleItem.destinyVersion === 2 && archetype && tertiaryStat && tertiaryStatDisplayProperties && { buttonLabel: [ <BungieImage key="1" src={archetype.displayProperties.icon} />, <span key="2">{archetype.displayProperties.name}</span>, '+', <BungieImage key="tertiary" className={styles.statIconAdjust} src={tertiaryStatDisplayProperties.icon} />, <ArmorSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, ], query: `${modernArmor} perk:${quoteFilterString(archetype.displayProperties.name)} tertiarystat:${tertiaryStat} is:${exampleItem.rarity}`, }, // exotic class items ...extraIntrinsicButtons, // basically stuff with the same name & categories { buttonLabel: [exampleItem.name], // TODO: I'm gonna get in trouble for this but I think it should just match on name which includes reissues. The old logic used dupeID which is more discriminating. query: compareNameQuery(exampleItem), }, // Exact armor based on ID { buttonLabel: [<AppIcon key="icon" icon={clearIcon} />], query: `id:${exampleItem.id}`, }, ]).reverse(); } const bucketToSearch = { [BucketHashes.KineticWeapons]: `is:kineticslot`, [BucketHashes.EnergyWeapons]: `is:energy`, [BucketHashes.PowerWeapons]: `is:heavy`, }; // stuff for looking up weapon archetypes const getRpm = (i: DimItem) => { const itemRpmStat = i.stats?.find( (s) => s.statHash === (i.destinyVersion === 1 ? i.stats![0].statHash : StatHashes.RoundsPerMinute), ); return itemRpmStat?.value || -99999999; }; /** * Generate possible comparisons for weapons, given a reference item. */ export function findSimilarWeapons(exampleItem: DimItem): CompareButton[] { const intrinsic = getWeaponArchetype(exampleItem); const intrinsicName = intrinsic?.displayProperties.name || t('Compare.Archetype'); const adeptStripped = stripAdept(exampleItem.name); let comparisonSets: CompareButton[] = compact([ // same weapon type { // TODO: replace typeName with a lookup of itemCategoryHash buttonLabel: [<WeaponTypeIcon key="type" item={exampleItem} className={styles.svgIcon} />], query: '', // since we already filter by itemCategoryHash, an empty query gives you all items matching that category }, // above but also has to be legendary exampleItem.destinyVersion === 2 && exampleItem.rarity === 'Legendary' && { buttonLabel: [ <BungieImage key="rarity" src={rarityIcons.Legendary} className="dontInvert" />, <WeaponTypeIcon key="type" item={exampleItem} className={styles.svgIcon} />, ], query: 'is:legendary', }, // above, but also matching intrinsic (rpm+impact..... ish) { buttonLabel: [ intrinsicName, <WeaponSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, <WeaponTypeIcon key="type" item={exampleItem} className={styles.svgIcon} />, ], query: `(${bucketToSearch[exampleItem.bucket.hash as keyof typeof bucketToSearch]} ${ exampleItem.destinyVersion === 2 && intrinsic ? `exactperk:${quoteFilterString(intrinsic.displayProperties.name)}` : `stat:rpm:${getRpm(exampleItem)}` })`, }, // above, but also same (kinetic/energy/heavy) slot { buttonLabel: [ <WeaponSlotIcon key="slot" item={exampleItem} className={styles.svgIcon} />, <WeaponTypeIcon key="type" item={exampleItem} className={styles.svgIcon} />, ], query: bucketToSearch[exampleItem.bucket.hash as keyof typeof bucketToSearch], }, // same weapon type and also matching element (& usually same-slot because same element) exampleItem.element && { buttonLabel: [ <ElementIcon key={exampleItem.id} element={exampleItem.element} className={clsx(styles.inlineImageIcon, 'dontInvert')} />, <WeaponTypeIcon key="type" item={exampleItem} className={styles.svgIcon} />, ], query: `is:${getItemDamageShortName(exampleItem)}`, }, // exact same weapon, judging by name. might span multiple expansions. { buttonLabel: [adeptStripped], query: compareNameQuery(exampleItem), }, // Exact weapon based on ID { buttonLabel: [<AppIcon key="icon" icon={clearIcon} />], query: `id:${exampleItem.id}`, }, ]); comparisonSets = comparisonSets.reverse(); return comparisonSets; } /** * Generate possible comparisons for non-armor/weapon, given a reference item */ export function defaultComparisons(exampleItem: DimItem): CompareButton[] { let comparisonSets: CompareButton[] = [ // same item type { // TODO: replace typeName with a lookup of itemCategoryHash buttonLabel: [exampleItem.typeName], query: '', // since we already filter by itemCategoryHash, an empty query gives you all items matching that category }, // exact same item, judging by name. might span multiple expansions. { buttonLabel: [exampleItem.name], query: compareNameQuery(exampleItem), }, // Exact item based on ID { buttonLabel: [<AppIcon key="icon" icon={clearIcon} />], query: `id:${exampleItem.id}`, }, ]; comparisonSets = comparisonSets.reverse(); return comparisonSets; } ================================================ FILE: src/app/compare/compare-utils.ts ================================================ import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { quoteFilterString } from 'app/search/query-parser'; /** * Strips the (Adept) (or (Timelost) or (Harrowed)) suffixes for the user's language * in order to include adept items in non-adept comparisons and vice versa. */ export const stripAdept = (name: string) => name .replace(new RegExp(t('Filter.Adept'), 'gi'), '') .replace(new RegExp(t('Filter.Timelost'), 'gi'), '') .replace(new RegExp(t('Filter.Harrowed'), 'gi'), '') .trim(); /** * Builds search query that should be used to compare this item to its dupes, * correctly quoting/escaping the name. */ export function compareNameQuery(item: DimItem) { return item.bucket.inWeapons ? `name:${quoteFilterString(stripAdept(item.name))}` : `name:${quoteFilterString(item.name)}`; } ================================================ FILE: src/app/compare/reducer.ts ================================================ import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { showNotification } from 'app/notifications/notifications'; import { getSelectionTree } from 'app/organizer/ItemTypeSelector'; import { quoteFilterString } from 'app/search/query-parser'; import { getSpecialtySocketMetadata, isD1Item } from 'app/utils/item-utils'; import { getArmorArchetype } from 'app/utils/socket-utils'; import { ItemCategoryHashes, PlugCategoryHashes } from 'data/d2/generated-enums'; import { ActionType, Reducer, getType } from 'typesafe-actions'; import * as actions from './actions'; import { compareNameQuery } from './compare-utils'; export interface CompareSession { /** * A list of itemCategoryHashes must be provided in order to limit the type of items which can be compared. * This list should match the item category drill-down from Organizer's ItemTypeSelector. */ readonly itemCategoryHashes: ItemCategoryHashes[]; /** * The query further filters the items to be shown. Since this query is modified * when adding or removing items, external queries must be parenthesized first * to avoid modifications binding to single filters within the original query. */ readonly query: string; /** * The instance ID of the first item added to compare, so we can highlight it. */ readonly initialItemId?: string; /** * The ID of the character (if any) whose vendor response we should intermingle with owned items */ readonly vendorCharacterId?: string; // TODO: Query history to offer back/forward navigation within compare sessions? } export interface CompareState { /** * This is set if the compare screen is shown. It contains the minimal state required to show the compare screen. * This is in Redux because it can be manipulated from all over the app. */ readonly session?: CompareSession; } export type CompareAction = ActionType<typeof actions>; const initialState: CompareState = {}; // TODO: how to determine the itemCategory? reverse index of organizer leaves? export const compare: Reducer<CompareState, CompareAction> = ( state: CompareState = initialState, action: CompareAction, ): CompareState => { switch (action.type) { case getType(actions.addCompareItem): return addCompareItem(state, action.payload); case getType(actions.removeCompareItem): return removeCompareItem(state, action.payload); case getType(actions.endCompareSession): return { ...state, session: undefined, }; case getType(actions.updateCompareQuery): { if (!state.session) { throw new Error("Programmer error: Can't update query with no session"); } return { ...state, session: { ...state.session, query: action.payload ? `(${action.payload})` : '', }, }; } case getType(actions.compareFilteredItems): return compareFilteredItems( state, action.payload.query, action.payload.filteredItems, action.payload.initialItem, ); case getType(actions.compareSelectedItems): return compareSelectedItems(state, action.payload); default: return state; } }; // TODO: better query editing tools // TODO: extract some of this into shared functions w/ the compare screen? // TODO: some way to just compare that one item? shift-click? highlight the original item? function addCompareItem(state: CompareState, item: DimItem): CompareState { if (state.session) { // Add to an existing session // Validate that item category matches what we have open if (!state.session.itemCategoryHashes.every((h) => item.itemCategoryHashes.includes(h))) { // TODO: throw error instead? showNotification({ type: 'warning', title: item.name, body: t('Compare.Error.Unmatched'), }); return state; } const itemQuery = `id:${item.id} or`; const query = state.session?.query || ''; // Don't just keep adding them if (query.includes(itemQuery)) { return state; } const removeQuery = `-id:${item.id}`; // Add `or` item filter to the left to avoid mixing it with // `implicit_and` filters from item removal (see `removeCompareItem`). const newQuery = ( query.includes(removeQuery) ? query.replace(removeQuery, '') : `${itemQuery} ${query}`.replace(/\s+/, ' ') ).trim(); return { ...state, session: { ...state.session, query: newQuery, }, }; } else { // Start a new session const itemCategoryHashes = getItemCategoryHashesFromExampleItem(item); const itemNameQuery = initialCompareQuery(item); const vendorCharacterId = item.vendor?.characterId; return { ...state, session: { query: `(${itemNameQuery})`, itemCategoryHashes, initialItemId: item.id, vendorCharacterId, }, }; } } function initialCompareQuery(item: DimItem) { if (isD1Item(item) || !item.bucket.inArmor || item.isExotic) { // D1 items, weapons, and exotic armor match by name return compareNameQuery(item); } else { // For D2 armor, we match by rarity, intrinsic and interesting mod sockets const factors = [`is:${item.rarity.toLowerCase()}`]; const intrinsicSocket = item.sockets?.allSockets.find( (socket) => socket.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Intrinsics && socket.plugged.plugDef.displayProperties.name, ); if (intrinsicSocket) { const intrinsicName = intrinsicSocket.plugged!.plugDef.displayProperties.name; factors.push(`exactperk:${quoteFilterString(intrinsicName)}`); } const modSlotMetadata = getSpecialtySocketMetadata(item); if (modSlotMetadata) { factors.push(`modslot:${modSlotMetadata.slotTag}`); } const archetype = getArmorArchetype(item); if (archetype) { factors.push(`perk:${quoteFilterString(archetype.displayProperties.name)}`); } return factors.join(' '); } } function removeCompareItem(state: CompareState, item: DimItem): CompareState { if (!state.session) { throw new Error("Programmer error: Can't remove item with no session"); } // Add `-id` filter to the right to avoid mixing it with // `or` filters from item addition (see `addCompareItem`). const addedQuery = `id:${item.id} or`; const newQuery = ( state.session.query.includes(addedQuery) ? state.session.query.replace(addedQuery, '') : // Quote this string because vendor item IDs can contain complex characters `${state.session.query} -id:"${item.id}"` ) .replace(/\s+/, ' ') .trim(); return { ...state, session: { ...state.session, query: newQuery, }, }; } function compareFilteredItems( state: CompareState, query: string, filteredItems: DimItem[], /** The first item added to compare, so we can highlight it. */ initialItem: DimItem | undefined, ): CompareState { if (state.session) { return state; } const itemCategoryHashes = getItemCategoryHashesFromExampleItem(filteredItems[0]); return { ...state, session: { query: query, itemCategoryHashes, initialItemId: initialItem?.id, vendorCharacterId: initialItem?.vendor?.characterId, }, }; } function compareSelectedItems(state: CompareState, items: DimItem[]) { if (state.session || !items.length) { return state; } const itemCategoryHashes = getItemCategoryHashesFromExampleItem(items[0]); const itemIds = items.map((i) => `id:${i.id}`); return { ...state, session: { query: `(${itemIds.join(' or ')})`, itemCategoryHashes, }, }; } function getItemCategoryHashesFromExampleItem(item: DimItem) { // This isn't right for armor // TODO: OK we might need a thunk so we can make decisions based on manifest state // For now just assume the last category is the most specific? const itemSelectionTree = getSelectionTree(item.destinyVersion); const hashes: number[] = []; // Dive two layers down (weapons/armor => type) for (const node of itemSelectionTree.subCategories!) { if (node.itemCategoryHash && item.itemCategoryHashes.includes(node.itemCategoryHash)) { hashes.push(node.itemCategoryHash); if (node.subCategories) { for (const subNode of node.subCategories) { if ( subNode.itemCategoryHash && item.itemCategoryHashes.includes(subNode.itemCategoryHash) ) { hashes.push(subNode.itemCategoryHash); break; } } } break; } } if (hashes.length === 0) { hashes.push(item.itemCategoryHashes[0]); } return hashes; } // TODO: observe state and reflect in URL params? ================================================ FILE: src/app/compare/selectors.ts ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { DimItem } from 'app/inventory/item-types'; import { allItemsSelector } from 'app/inventory/selectors'; import { accountRoute } from 'app/routes'; import { filterFactorySelector } from 'app/search/items/item-search-filter'; import { RootState } from 'app/store/types'; import { emptyArray } from 'app/utils/empty'; import { currySelector } from 'app/utils/selectors'; import { characterVendorItemsSelector } from 'app/vendors/selectors'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import { createSelector } from 'reselect'; /** * The current compare session settings. */ export const compareSessionSelector = (state: RootState) => state.compare.session; export const compareQuerySelector = (state: RootState) => compareSessionSelector(state)?.query; export const compareOpenSelector = (state: RootState) => Boolean(compareSessionSelector(state)); /** * Returns all the items matching the item category of the current compare session. */ export const compareCategoryItemsSelector = createSelector( (state: RootState) => state.compare.session?.itemCategoryHashes, allItemsSelector, characterVendorItemsSelector, (itemCategoryHashes, allItems, vendorItems) => { if (!itemCategoryHashes) { return emptyArray<DimItem>(); } return [...allItems, ...vendorItems].filter( (i) => (!i.vendor || i.vendor.vendorHash) && itemCategoryHashes.every((h) => i.itemCategoryHashes.includes(h)), ); }, ); /** * Returns all the items being compared. */ export const compareItemsSelector = currySelector( createSelector( compareSessionSelector, compareCategoryItemsSelector, filterFactorySelector, (session, categoryItems, filterFactory) => { if (!session) { return emptyArray<DimItem>(); } const filterFunction = filterFactory(session.query); return categoryItems.filter(filterFunction); }, ), ); const organizerTypes = [ ItemCategoryHashes.Hunter, ItemCategoryHashes.Titan, ItemCategoryHashes.Warlock, ItemCategoryHashes.Armor, ItemCategoryHashes.Weapon, ItemCategoryHashes.Ghost, ]; /** * Returns a link to the organizer for the current compare search. */ export const compareOrganizerLinkSelector = createSelector( currentAccountSelector, compareSessionSelector, (account, session) => { if (!session || !account || !organizerTypes.includes(session.itemCategoryHashes[0])) { return undefined; } return `${accountRoute(account)}/organizer?category=${session.itemCategoryHashes.join( '~', )}&search=${encodeURIComponent(session.query)}`; }, ); ================================================ FILE: src/app/compare/types.ts ================================================ import { DimPlug } from 'app/inventory/item-types'; export interface DimAdjustedItemPlug { /** A plug selected for stat comparison */ [socketIndex: number]: DimPlug; } export interface DimAdjustedPlugs { /** A collection of selected plugs for comparison, keyed by item instance ID */ [itemId: string]: DimAdjustedItemPlug; } export interface DimAdjustedItemStat { /** An updated stat value when plug effects are being compared */ [statHash: number]: number; [statId: string]: number; } export interface DimAdjustedStats { /** A collection of adjusted stat values, keyed by item instance ID */ [itemId: string]: DimAdjustedItemStat; } ================================================ FILE: src/app/css-variables.ts ================================================ import { OrnamentDisplay } from '@destinyitemmanager/dim-api-types'; import { settingsSelector } from 'app/dim-api/selectors'; import { deepEqual } from 'fast-equals'; import { isPhonePortraitSelector } from './shell/selectors'; import { StoreObserver } from './store/observerMiddleware'; /** * Visual viewport diffs greater than this value will cause --viewport-bottom-offset * to be set. A threshold of 50px accounts for full size keyboards as well as the * iPad layout that only shows the predictive text bar. */ const KEYBOARD_THRESHOLD = 50; function setCSSVariable(property: string, value: { toString: () => string }) { if (value !== undefined && value !== null) { document.querySelector('html')!.style.setProperty(property, value.toString()); } } export function createItemSizeObserver(): StoreObserver<number> { return { id: 'item-size-observer', getObserved: (rs) => settingsSelector(rs).itemSize, sideEffect: ({ current }) => { setCSSVariable('--item-size', `${Math.max(48, current)}px`); }, }; } export function createOrnamentDisplayObserver(): StoreObserver<OrnamentDisplay> { return { id: 'ornament-display-observer', getObserved: (rs) => settingsSelector(rs).ornamentDisplay, sideEffect: ({ current }) => { setCSSVariable('--ornament-display-opacity', current === OrnamentDisplay.All ? 1 : 0); setCSSVariable( '--ornament-display-visibility', current === OrnamentDisplay.All ? 'auto' : 'hidden', ); setCSSVariable( '--ornament-display-visibility-inverse', current === OrnamentDisplay.All ? 'hidden' : 'auto', ); }, }; } export function createThemeObserver(): StoreObserver<{ theme: string; isPhonePortrait: boolean }> { return { id: 'theme-observer', equals: deepEqual, getObserved: (rs) => ({ theme: settingsSelector(rs).theme, isPhonePortrait: isPhonePortraitSelector(rs), }), sideEffect: ({ current }) => { // Set a class on the body to control the theme. This must be applied on the body for syncThemeColor to work. const themeClass = `theme-${current.theme}`; document.body.className = themeClass; syncThemeColor(current.isPhonePortrait); }, }; } export function createTilesPerCharColumnObserver(): StoreObserver<number> { return { id: 'tiles-per-char-column-observer', runInitially: true, getObserved: (rs) => isPhonePortraitSelector(rs) ? settingsSelector(rs).charColMobile : settingsSelector(rs).charCol, sideEffect: ({ current }) => { setCSSVariable('--tiles-per-char-column', current); }, }; } /** * Update a set of CSS variables depending on the settings of the app and whether we're in portrait mode. */ export function setCssVariableEventListeners() { // Set a CSS var for the true viewport height. This changes when the keyboard appears/disappears. // https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ if (window.visualViewport) { const defineVH = () => { const viewport = window.visualViewport!; const viewportHeight = Math.round(viewport.height); setCSSVariable('--viewport-height', `${viewportHeight}px`); /** * The amount the bottom of the visual viewport is offset from the layout viewport * This is calculated so elements such as sheets are not hidden by the keyboard. * However, other viewport changes such as a scrollbar appearing can cause the visual * viewport to change. As a result, we only apply the following CSS Variable if the * viewport size change is large enough (such as when the keyboard opens). */ const bottomOffset = Math.max( 0, window.innerHeight - (viewportHeight + Math.round(viewport.offsetTop)), ); // bottomOffset === 0 means the visual viewport has been reset to its initial size if (bottomOffset === 0 || bottomOffset >= KEYBOARD_THRESHOLD) { setCSSVariable('--viewport-bottom-offset', `${bottomOffset}px`); } }; defineVH(); window.visualViewport.addEventListener('resize', () => defineVH()); window.visualViewport.addEventListener('scroll', () => defineVH()); } else { const defineVH = () => { setCSSVariable('--viewport-height', `${window.innerHeight}px`); }; defineVH(); window.addEventListener('resize', defineVH); } const defineScrollbarWidth = () => { // Set a css var for the width of a scrollbar const scrollDiv = document.createElement('div'); scrollDiv.className = 'scrollbar-measure'; scrollDiv.style.width = '100px'; scrollDiv.style.height = '100px'; scrollDiv.style.overflow = 'scroll'; scrollDiv.style.position = 'absolute'; scrollDiv.style.top = '-9999px'; document.body.appendChild(scrollDiv); const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; // Delete the div document.body.removeChild(scrollDiv); setCSSVariable('--scrollbar-width', `${scrollbarWidth}px`); }; defineScrollbarWidth(); window.addEventListener('resize', defineScrollbarWidth); } /** * Read the --theme-pwa-background CSS variable and use it to set the meta theme-color element. */ function syncThemeColor(isPhonePortrait: boolean) { let background = getComputedStyle(document.body).getPropertyValue('--theme-pwa-background'); // Extract tint from mobile header on mobile devices to match notch/dynamic island fill if (isPhonePortrait) { background = getComputedStyle(document.body).getPropertyValue('--theme-mobile-background'); } if (background) { const metaElem = document.querySelector("meta[name='theme-color']"); if (metaElem) { metaElem.setAttribute('content', background); } } } ================================================ FILE: src/app/debug/Debug.m.scss ================================================ .debugPage { margin: 2em 10px; } .cells { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1em; section { background: rgb(0, 0, 0, 0.1); padding: 1em; } h3 { margin: 0; font-weight: 700; } p { margin: 0; } } .error { background-color: rgb(100, 0, 0, 0.2); padding: 0.2em; } .token { background: rgb(0, 0, 0, 0.1); margin-top: 0.5em; padding: 0.5em; } ================================================ FILE: src/app/debug/Debug.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'cells': string; 'debugPage': string; 'error': string; 'token': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/debug/Debug.tsx ================================================ import Account from 'app/accounts/Account'; import { accountsSelector, currentAccountSelector } from 'app/accounts/selectors'; import { BungieError, HttpStatusError } from 'app/bungie-api/http-client'; import { Token, getToken } from 'app/bungie-api/oauth-tokens'; import { clarityCharacterStatsSelector, clarityDescriptionsSelector } from 'app/clarity/selectors'; import { DimAuthToken, getToken as getDimApiToken } from 'app/dim-api/dim-api-helper'; import { apiPermissionGrantedSelector, currentProfileSelector, dimSyncErrorSelector, settingSelector, updateQueueLengthSelector, } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { profileErrorSelector, profileResponseSelector } from 'app/inventory/selectors'; import { useLoadStores } from 'app/inventory/store/hooks'; import { TroubleshootingSettings } from 'app/settings/Troubleshooting'; import LocalStorageInfo from 'app/storage/LocalStorageInfo'; import { set } from 'app/storage/idb-keyval'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { streamDeckSelector } from 'app/stream-deck/selectors'; import { DimError } from 'app/utils/dim-error'; import { convertToError } from 'app/utils/errors'; import { usePageTitle } from 'app/utils/hooks'; import { systemInfo } from 'app/utils/system-info'; import { wishListsLastFetchedSelector, wishListsSelector } from 'app/wishlists/selectors'; import { fetchWishList } from 'app/wishlists/wishlist-fetch'; import { useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import * as styles from './Debug.m.scss'; /** * A user-facing debug page that displays information about the DIM environment, * browser, and various states and capabilities to help us figure things out. * * The text on this page is for DIM developers and does not need to be translated. */ export default function Debug() { usePageTitle('Debug'); const dispatch = useThunkDispatch(); const bungieToken = getToken(); const dimApiToken = getDimApiToken(); const accounts = useSelector(accountsSelector); const currentAccount = useSelector(currentAccountSelector); const profileResponse = useSelector(profileResponseSelector); const profileError = useSelector(profileErrorSelector); const dimSyncProfile = useSelector(currentProfileSelector); const dimSyncError = useSelector(dimSyncErrorSelector); const dimSyncUpdateQueueSize = useSelector(updateQueueLengthSelector); const apiPermissionGranted = useSelector(apiPermissionGrantedSelector); const wishListsLastFetched = useSelector(wishListsLastFetchedSelector); const wishlistSource = useSelector(settingSelector('wishListSource')); const wishList = useSelector(wishListsSelector); const streamDeck = useSelector(streamDeckSelector); const clarityDescriptions = useSelector(clarityDescriptionsSelector); const clarityCharacterStats = useSelector(clarityCharacterStatsSelector); useLoadStores(currentAccount); const [idbError, setIdbError] = useState<Error>(); useEffect(() => { (async () => { try { await set('idb-test', true); } catch (e) { setIdbError(convertToError(e)); } })(); }, []); const localStorageError = useMemo(() => { try { localStorage.setItem('test', 'true'); } catch (e) { return convertToError(e); } }, []); const [serviceWorkers, setServiceWorkers] = useState<readonly ServiceWorkerRegistration[]>([]); useEffect(() => { (async () => { setServiceWorkers(await navigator.serviceWorker.getRegistrations()); })(); }, []); const isD2 = currentAccount?.destinyVersion === 2; useEffect(() => { if ($featureFlags.wishLists && isD2) { dispatch(fetchWishList()); } }, [dispatch, isD2]); const now = ( <p> <b>Now:</b> {new Date().toISOString()} </p> ); const weirdWishlistRoll = wishList?.wishListAndInfo?.wishListRolls.find( (r) => r.recommendedPerks && !(r.recommendedPerks instanceof Set), ); // TODO: If these tiles get too complicated, they could be broken out into components return ( <div className={styles.debugPage}> <p> Open the debug console (F12 or Ctrl + ⇧ Shift + J on Windows, ⌘ + ⌥ Option + J on macOS, ⌘ + ⌥ Option + C in Safari (after enabling it)), select the console tab, then send a screenshot of this entire page and the console. </p> <div className={styles.cells}> <section> <h3>Browser</h3> <p> {t('Views.About.Version', { version: $DIM_VERSION, flavor: $DIM_FLAVOR, date: new Date($DIM_BUILD_DATE).toLocaleString(), })} </p> <p>{systemInfo}</p> <p> <b>User Agent:</b> {navigator.userAgent} </p> <p> <b>navigator.platform:</b> {navigator.platform} </p> </section> <section> <h3>Auth Tokens</h3> {now} {bungieToken ? ( <> <p> <b>Membership ID:</b> {bungieToken.bungieMembershipId} </p> {bungieToken.accessToken && <BungieTokenState token={bungieToken.accessToken} />} {bungieToken.refreshToken && <BungieTokenState token={bungieToken.refreshToken} />} </> ) : ( <p>No auth token</p> )} </section> <section> <h3>DIM Sync Auth Tokens</h3> {now} {dimApiToken ? <DIMTokenState token={dimApiToken} /> : <p>No auth token</p>} </section> <section> <h3>Accounts</h3> {accounts.map((account) => ( <div key={`${account.membershipId}-${account.destinyVersion}`}> <Account account={account} selected={account === currentAccount} /> {account.membershipId} </div> ))} </section> <section> <h3>Storage</h3> <p> {localStorageError ? ( <> <b>Local Storage Broken:</b> <ErrorInfo error={localStorageError} /> </> ) : ( <b>Local Storage Works</b> )} </p> <p> {idbError ? ( <> <b>IDB Broken:</b> <ErrorInfo error={idbError} /> </> ) : ( <b>IDB Works</b> )} </p> <LocalStorageInfo showDetails /> </section> <section> <h3>Profile Response</h3> {now} {profileResponse && ( <p> <b>Minted:</b> {new Date(profileResponse?.responseMintedTimestamp).toISOString()} </p> )} <p> <b>Error:</b> {profileError ? <ErrorInfo error={profileError} /> : 'None'} </p> </section> <section> <h3>DIM Sync</h3> <p> <b>API Permission Granted:</b> {JSON.stringify(apiPermissionGranted)} </p> {now} {dimSyncProfile ? ( <> <p> <b>Last Fetched:</b> {new Date(dimSyncProfile.profileLastLoaded).toISOString()} </p> <p> <b>Tags:</b> {Object.keys(dimSyncProfile.tags).length} </p> <p> <b>Loadouts:</b> {Object.keys(dimSyncProfile.loadouts).length} </p> </> ) : ( <p>No Profile Loaded</p> )} <p> <b>Update Queue Size:</b> {dimSyncUpdateQueueSize} </p> <p> <b>Error:</b> {dimSyncError ? <ErrorInfo error={dimSyncError} /> : 'None'} </p> </section> <section> <h3>Service Worker</h3> {serviceWorkers.length > 0 ? ( serviceWorkers.map((w, i) => ( <div key={i}> {i}: Active: {w.active?.state || 'null'}, Waiting: {w.waiting?.state || 'null'}, Installing: {w.installing?.state || 'null'} </div> )) ) : ( <div>Service worker not installed</div> )} </section> <section> <h3>Wish Lists</h3> <p> <b>Source:</b> {wishlistSource} </p> <p> <b>Last Fetched:</b> {wishListsLastFetched?.toISOString() ?? 'Never'} </p> <p> <b>Wish list rolls:</b>{' '} {wishList?.wishListAndInfo?.wishListRolls.length.toLocaleString() ?? 'No wishlist'} </p> <p> <b>Weird recommendedPerks?:</b>{' '} {weirdWishlistRoll ? typeof weirdWishlistRoll.recommendedPerks : 'All good'} </p> </section> <section> <h3>Clarity</h3> <p> <b>Descriptions loaded?:</b> {JSON.stringify(Boolean(clarityDescriptions))} </p> <p> <b>Character stats loaded?:</b> {JSON.stringify(Boolean(clarityCharacterStats))} </p> </section> {$featureFlags.elgatoStreamDeck && ( <section> <h3>Stream Deck</h3> <p> <b>Enabled:</b> {JSON.stringify(Boolean(streamDeck.enabled))} </p> <p> <b>Connected:</b> {JSON.stringify(streamDeck.connected)} </p> <p> <b>Instance:</b> {JSON.stringify(streamDeck.auth?.instance) ?? '-'} </p> <p> <b>Token:</b> {JSON.stringify(streamDeck.auth?.token) ?? '-'} </p> </section> )} {$DIM_FLAVOR !== 'release' && currentAccount?.destinyVersion === 2 && ( <TroubleshootingSettings /> )} </div> </div> ); } function BungieTokenState({ token }: { token: Token }) { const tokenInception = new Date(token.inception); const expires = new Date(token.inception + token.expires * 1000); const expired = expires.getTime() < Date.now(); return ( <div className={styles.token}> <p> <b>Type:</b> {token.name} </p> <p> <b>Retrieved:</b> {tokenInception.toISOString()} </p> <p> <b>Expires:</b> {expires.toISOString()} </p> <p> <b>Expired?:</b> {JSON.stringify(expired)} </p> </div> ); } function DIMTokenState({ token }: { token: DimAuthToken }) { const tokenInception = new Date(token.inception); const expires = new Date(token.inception + token.expiresInSeconds * 1000); const expired = expires.getTime() < Date.now(); return ( <div className={styles.token}> <p> <b>Retrieved:</b> {tokenInception.toISOString()} </p> <p> <b>Expires:</b> {expires.toISOString()} </p> <p> <b>Expired?:</b> {JSON.stringify(expired)} </p> </div> ); } function ErrorInfo({ error }: { error: Error | DimError }) { const cause = error instanceof DimError ? error.cause : undefined; const code = error instanceof DimError || error instanceof BungieError ? error.code : error instanceof HttpStatusError ? `HTTP ${error.status}` : undefined; const name = error.name; const message = error.message || 'No message'; return ( <> <div className={styles.error}> {name} {code !== undefined && ' '} {code}: {message} </div> {cause && ( <div> Cause: <ErrorInfo error={cause} /> </div> )} </> ); } ================================================ FILE: src/app/destiny1/activities/Activities.m.scss ================================================ @use '../../variables.scss' as *; .activities { composes: dim-page from global; width: calc( (40px + var(--item-size) + (3 * (var(--item-size) + 8px))) * var(--num-characters) + ((var(--num-characters) - 1) * 12px) ); margin: 0 auto; @include phone-portrait { width: 100%; } } .characters { composes: flexRow from '../../dim-ui/common.m.scss'; justify-content: space-between; margin-top: 12px; margin-bottom: 12px; > * { flex-shrink: 0; width: $emblem-width; } } .title { background-blend-mode: multiply; background: #666; text-shadow: 2px 2px 5px black; @include interactive($hover: true) { background: #aaa; } } .featured { color: gold; } .smallIcon { height: 26px; } .activityType { font-size: 0.85em; } .activityInfo { padding: 8px 0 16px 0; @include phone-portrait { padding-left: var(--inventory-column-padding); padding-right: var(--inventory-column-padding); } } .tierTitle { text-transform: uppercase; position: absolute; font-size: 12px; color: #ccc; } .tierCharacters { composes: flexRow from '../../dim-ui/common.m.scss'; justify-content: space-around; } .steps { composes: flexRow from '../../dim-ui/common.m.scss'; } .stepIcon { border-radius: 50%; background-color: rgb(245, 245, 245, 0.1); width: 8px; height: 8px; display: inline-block; border: 2px solid #fff; margin: 2px; &.complete { border-color: #ffce1f; background-color: rgb(255, 206, 31, 0.4); } } .skulls { composes: flexColumn from '../../dim-ui/common.m.scss'; gap: 8px; max-width: 350px; margin: 0 auto; &:not(:first-child) { margin-top: 12px; } } .skullIcon { height: 16px; padding: 2px 7px; float: left; } .weak { color: var(--theme-text-secondary); } ================================================ FILE: src/app/destiny1/activities/Activities.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'activities': string; 'activityInfo': string; 'activityType': string; 'characters': string; 'complete': string; 'featured': string; 'skullIcon': string; 'skulls': string; 'smallIcon': string; 'stepIcon': string; 'steps': string; 'tierCharacters': string; 'tierTitle': string; 'title': string; 'weak': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/activities/Activities.tsx ================================================ import CharacterTile from 'app/character-tile/CharacterTile'; import CharacterSelect from 'app/dim-ui/CharacterSelect'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { useLoadStores } from 'app/inventory/store/hooks'; import { useD1Definitions } from 'app/manifest/selectors'; import Objective from 'app/progress/Objective'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { compareBy, compareByIndex } from 'app/utils/comparators'; import { emptyArray } from 'app/utils/empty'; import { usePageTitle } from 'app/utils/hooks'; import { StringLookup } from 'app/utils/util-types'; import clsx from 'clsx'; import { useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { DestinyAccount } from '../../accounts/destiny-account'; import BungieImage, { bungieBackgroundStyle } from '../../dim-ui/BungieImage'; import CollapsibleTitle from '../../dim-ui/CollapsibleTitle'; import { sortedStoresSelector } from '../../inventory/selectors'; import { D1Store } from '../../inventory/store-types'; import { AppIcon, starIcon } from '../../shell/icons'; import { D1ManifestDefinitions } from '../d1-definitions'; import { D1ActivityComponent, D1ActivityTier, D1ObjectiveProgress } from '../d1-manifest-types'; import * as styles from './Activities.m.scss'; interface Skull { displayName: string; description: string; icon: string; } interface Activity { hash: number; name: string; icon: string; image: string; type: string; skulls: Skull[] | null; tiers: ActivityTier[]; featured?: boolean; } interface ActivityTier { hash: number; icon: string; name: string; complete: boolean; characters: { name: string; lastPlayed: Date; id: string; icon: string; steps: { complete: boolean }[]; objectives: D1ObjectiveProgress[]; }[]; } export default function Activities({ account }: { account: DestinyAccount }) { usePageTitle(t('Activities.Activities')); const storesLoaded = useLoadStores(account); const stores = useSelector(sortedStoresSelector); const isPhonePortrait = useIsPhonePortrait(); const defs = useD1Definitions(); const characters = stores.filter((s) => !s.isVault) as D1Store[]; const [selectedStoreId, setSelectedStoreId] = useState<string>(); const selectedStore = (stores.find((store) => store.id === selectedStoreId) as D1Store) || characters[0]; const activities = useActivities( defs, isPhonePortrait && storesLoaded ? [selectedStore] : characters, ); if (!defs || !storesLoaded) { return <ShowPageLoading message={t('Loading.Profile')} />; } return ( <div className={styles.activities}> {isPhonePortrait ? ( <CharacterSelect selectedStore={selectedStore} stores={characters} onCharacterChanged={setSelectedStoreId} /> ) : ( <div className={styles.characters}> {characters.map((store) => ( <CharacterTile key={store.id} store={store} /> ))} </div> )} {activities.map((activity) => ( <div key={activity.hash}> <CollapsibleTitle style={bungieBackgroundStyle(activity.image)} className={clsx(styles.title, { [styles.featured]: activity.featured, })} sectionId={`activities-${activity.hash}`} title={ <> <BungieImage src={activity.icon} className={styles.smallIcon} /> <span>{activity.name}</span> {activity.featured && <AppIcon icon={starIcon} />} </> } extra={<span className={styles.activityType}>{activity.type}</span>} > <div className={styles.activityInfo}> {activity.tiers.map( (tier) => tier.characters.some((c) => c.objectives.length || c.steps.length) && ( <div key={tier.name}> {activity.tiers.length > 1 && ( <div className={styles.tierTitle}>{tier.name}</div> )} <div className={styles.tierCharacters}> {tier.characters .toSorted(compareBy((c) => characters.findIndex((s) => s.id === c.id))) .map( (character) => (!isPhonePortrait || character.id === selectedStore.id) && ( <div key={character.id}> {character.objectives.length === 0 && character.steps.length > 0 && ( <div className={styles.steps}> {character.steps.map((step, index) => ( <span key={index} className={clsx(styles.stepIcon, { [styles.complete]: step.complete, })} /> ))} </div> )} {character.objectives.map((objective) => ( <Objective objective={objective} key={objective.objectiveHash} /> ))} </div> ), )} </div> </div> ), )} {activity.skulls && ( <div className={styles.skulls}> {activity.skulls?.map((skull) => ( <div key={skull.displayName}> <BungieImage src={skull.icon} className={styles.skullIcon} /> {skull.displayName} <span className={styles.weak}> - {skull.description}</span> </div> ))} </div> )} </div> </CollapsibleTitle> </div> ))} </div> ); } function useActivities(defs: D1ManifestDefinitions | undefined, characters: D1Store[]): Activity[] { return useMemo(() => { const processActivity = ( defs: D1ManifestDefinitions, activityId: string, stores: D1Store[], tier: D1ActivityTier, index: number, ): ActivityTier => { const tierDef = defs.Activity.get(tier.activityHash); const name = tier.activityData.recommendedLight === 390 ? '390' : tier.tierDisplayName ? t(`Activities.${tier.tierDisplayName}`, { metadata: { keys: 'difficulty' }, }) : tierDef.activityName; const characters = activityId === 'heroicstrike' ? [] : stores.map((store) => { const activity = store.advisors.activities[activityId]; let steps = activity.activityTiers[index].steps; if (!steps) { steps = [activity.activityTiers[index].completion]; } const objectives = activity.extended?.objectives || []; return { name: store.name, lastPlayed: store.lastPlayed, id: store.id, icon: store.icon, steps, objectives, }; }); return { hash: tierDef.activityHash, icon: tierDef.icon, name, complete: tier.activityData.isCompleted, characters, }; }; const processActivities = ( defs: D1ManifestDefinitions, stores: D1Store[], rawActivity: D1ActivityComponent, ): Activity => { const def = defs.Activity.get(rawActivity.display.activityHash); const activity = { hash: rawActivity.display.activityHash, name: def.activityName, icon: rawActivity.display.icon, image: rawActivity.display.image, type: rawActivity.identifier === 'nightfall' ? t('Activities.Nightfall') : rawActivity.identifier === 'heroicstrike' ? t('Activities.WeeklyHeroic') : defs.ActivityType.get(def.activityTypeHash).activityTypeName, skulls: null as Skull[] | null, tiers: [] as ActivityTier[], }; if (rawActivity.extended) { activity.skulls = rawActivity.extended.skullCategories.flatMap((s) => s.skulls); } const rawSkullCategories = rawActivity.activityTiers[0].skullCategories; if (rawSkullCategories?.length) { activity.skulls = rawSkullCategories[0].skulls.flat(); } if (activity.skulls) { activity.skulls = i18nActivitySkulls(activity.skulls, defs); } // flatten modifiers and bonuses for now. if (activity.skulls) { activity.skulls = activity.skulls.flat(); } activity.tiers = rawActivity.activityTiers.map((r, i) => processActivity(defs, rawActivity.identifier, stores, r, i), ); return activity; }; const init = (stores: D1Store[], defs: D1ManifestDefinitions) => { const allowList = [ 'vaultofglass', 'crota', 'kingsfall', 'wrathofthemachine', 'nightfall', 'heroicstrike', 'elderchallenge', ]; const activities = Object.values(stores[0].advisors.activities) .filter((a) => a.activityTiers && allowList.includes(a.identifier)) .sort(compareByIndex(allowList, (a) => a.identifier)) .map((a) => processActivities(defs, stores, a)); for (const a of activities) { for (const t of a.tiers) { if (t.hash === stores[0].advisors.activities.weeklyfeaturedraid.display.activityHash) { a.featured = true; t.name = t.hash === 1387993552 ? '390' : t.name; } } } return activities; }; if (!defs || !characters.length) { return emptyArray(); } return init(characters, defs); }, [characters, defs]); } const skullHashesByName: StringLookup<number> = { Heroic: 0, 'Arc Burn': 1, 'Solar Burn': 2, 'Void Burn': 3, Berserk: 4, Brawler: 5, Lightswitch: 6, 'Small Arms': 7, Specialist: 8, Juggler: 9, Grounded: 10, Bloodthirsty: 11, Chaff: 12, 'Fresh Troops': 13, Ironclad: 14, 'Match Game': 15, Exposure: 16, Airborne: 17, Catapult: 18, Epic: 20, }; function i18nActivitySkulls(skulls: Skull[], defs: D1ManifestDefinitions): Skull[] { const activity = { heroic: defs.Activity.get(870614351), epic: defs.Activity.get(2234107290), }; for (const skull of skulls) { const hash = skullHashesByName[skull.displayName]; if (hash) { if (hash === 20) { skull.displayName = activity.epic.skulls[0].displayName; skull.description = activity.epic.skulls[0].description; } else if (activity.heroic.skulls[hash]) { skull.displayName = activity.heroic.skulls[hash].displayName; skull.description = activity.heroic.skulls[hash].description; } } } return skulls; } ================================================ FILE: src/app/destiny1/d1-bucket-categories.ts ================================================ import type { D1BucketCategory } from 'app/inventory/inventory-buckets'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { BucketHashes } from 'data/d2/generated-enums'; export const D1Categories: { [key in D1BucketCategory]: (BucketHashes | D1BucketHashes)[]; } = { Postmaster: [BucketHashes.LostItems, BucketHashes.SpecialOrders, BucketHashes.Messages], Weapons: [BucketHashes.KineticWeapons, BucketHashes.EnergyWeapons, BucketHashes.PowerWeapons], Armor: [ BucketHashes.Helmet, BucketHashes.Gauntlets, BucketHashes.ChestArmor, BucketHashes.LegArmor, BucketHashes.ClassArmor, ], General: [ BucketHashes.Subclass, D1BucketHashes.Artifact, BucketHashes.Ghost, BucketHashes.Consumables, BucketHashes.Materials, BucketHashes.Modifications, BucketHashes.Emblems, D1BucketHashes.Shader, D1BucketHashes.D1Emotes, BucketHashes.Ships, BucketHashes.Vehicle, D1BucketHashes.Horn, ], Progress: [D1BucketHashes.Bounties, D1BucketHashes.Quests, D1BucketHashes.Missions], }; ================================================ FILE: src/app/destiny1/d1-buckets.ts ================================================ import { filterMap } from 'app/utils/collections'; import { HashLookup, StringLookup } from 'app/utils/util-types'; import { BucketCategory } from 'bungie-api-ts/destiny2'; import type { D1BucketCategory, InventoryBucket, InventoryBuckets, } from '../inventory/inventory-buckets'; import { D1Categories } from './d1-bucket-categories'; import type { D1ManifestDefinitions } from './d1-definitions'; export const vaultTypes: HashLookup<string> = { 3003523923: 'Armor', 4046403665: 'Weapons', 138197802: 'General', }; const sortToVault: StringLookup<number> = { Armor: 3003523923, Weapons: 4046403665, General: 138197802, }; const bucketHashToSort: { [bucketHash: number]: D1BucketCategory } = {}; for (const [category, bucketHashes] of Object.entries(D1Categories)) { for (const bucketHash of bucketHashes) { bucketHashToSort[bucketHash] = category as D1BucketCategory; } } export function getBuckets(defs: D1ManifestDefinitions) { const buckets: InventoryBuckets = { byHash: {}, byCategory: {}, unknown: { description: 'Unknown items. DIM needs a manifest update.', name: 'Unknown', hash: -1, // default to false. an equipped item existing, will override this in inv display equippable: false, hasTransferDestination: false, category: BucketCategory.Item, capacity: Number.MAX_SAFE_INTEGER, sort: 'Unknown', accountWide: false, }, setHasUnknown() { this.byCategory[this.unknown.sort!] = [this.unknown]; }, }; for (const def of Object.values(defs.InventoryBucket.getAll())) { if (def.enabled) { const sort = bucketHashToSort[def.hash] ?? vaultTypes[def.hash]; const bucket: InventoryBucket = { description: def.bucketDescription, name: def.bucketName, hash: def.hash, equippable: def.category === BucketCategory.Equippable, hasTransferDestination: def.hasTransferDestination, capacity: def.itemCount, accountWide: false, category: BucketCategory.Item, sort, }; if (sort) { // Add an easy helper property like "inPostmaster" bucket[`in${sort}`] = true; } buckets.byHash[bucket.hash] = bucket; } } for (const bucket of Object.values(buckets.byHash)) { if (bucket.sort && sortToVault[bucket.sort] && sortToVault[bucket.sort] !== bucket.hash) { bucket.vaultBucket = buckets.byHash[sortToVault[bucket.sort]!]; } } for (const [category, bucketHashes] of Object.entries(D1Categories)) { buckets.byCategory[category] = filterMap( bucketHashes, (bucketHash) => buckets.byHash[bucketHash], ); } return buckets; } ================================================ FILE: src/app/destiny1/d1-definitions.ts ================================================ import { ThunkResult } from 'app/store/types'; import { reportException } from 'app/utils/sentry'; import { HashLookupFailure } from '../destiny2/definitions'; import { setD1Manifest } from '../manifest/actions'; import { getManifest } from '../manifest/d1-manifest-service'; import { D1ActivityDefinition, D1ActivityTypeDefinition, D1ClassDefinition, D1DamageTypeDefinition, D1FactionDefinition, D1InventoryBucketDefinition, D1InventoryItemDefinition, D1ItemCategoryDefinition, D1ObjectiveDefinition, D1ProgressionDefinition, D1RaceDefinition, D1RecordBookDefinition, D1RecordDefinition, D1StatDefinition, D1TalentGridDefinition, D1VendorCategoryDefinition, D1VendorDefinition, } from './d1-manifest-types'; const allTables = [ 'InventoryBucket', 'Class', 'Race', 'Faction', 'Vendor', 'InventoryItem', 'Objective', 'Stat', 'TalentGrid', 'Progression', 'Record', 'ItemCategory', 'VendorCategory', 'RecordBook', 'Activity', 'ActivityType', 'DamageType', ] as const; export interface DefinitionTable<T> { readonly get: (hash: number) => T; readonly getAll: () => { [hash: number]: T }; } export interface D1ManifestDefinitions { InventoryBucket: DefinitionTable<D1InventoryBucketDefinition>; Class: DefinitionTable<D1ClassDefinition>; Race: DefinitionTable<D1RaceDefinition>; Faction: DefinitionTable<D1FactionDefinition>; Vendor: DefinitionTable<D1VendorDefinition>; InventoryItem: DefinitionTable<D1InventoryItemDefinition>; Objective: DefinitionTable<D1ObjectiveDefinition>; Stat: DefinitionTable<D1StatDefinition>; TalentGrid: DefinitionTable<D1TalentGridDefinition>; Progression: DefinitionTable<D1ProgressionDefinition>; Record: DefinitionTable<D1RecordDefinition>; ItemCategory: DefinitionTable<D1ItemCategoryDefinition>; VendorCategory: DefinitionTable<D1VendorCategoryDefinition>; RecordBook: DefinitionTable<D1RecordBookDefinition>; Activity: DefinitionTable<D1ActivityDefinition>; ActivityType: DefinitionTable<D1ActivityTypeDefinition>; DamageType: DefinitionTable<D1DamageTypeDefinition>; /** Check if these defs are from D2. Inside an if statement, these defs will be narrowed to type D2ManifestDefinitions. */ readonly isDestiny2: false; } /** * Manifest database definitions. This returns a promise for an * objet that has a property named after each of the tables listed * above (defs.TalentGrid, etc.). */ export function getDefinitions(force = false): ThunkResult<D1ManifestDefinitions> { return async (dispatch, getState) => { let existingManifest = getState().manifest.d1Manifest; if (existingManifest && !force) { return existingManifest; } const db = await dispatch(getManifest()); existingManifest = getState().manifest.d1Manifest; if (existingManifest && !force) { return existingManifest; } const defs: { [table: string]: any; isDestiny2: false } = { isDestiny2: false, }; for (const tableShort of allTables) { const table = `Destiny${tableShort}Definition` as const; const dbTable = db[table]; defs[tableShort] = { get(id: number, requestor?: { hash: number } | string | number) { if (!dbTable) { throw new Error(`Table ${table} does not exist in the manifest`); } const dbEntry = dbTable[id]; if (!dbEntry) { const requestingEntryInfo = typeof requestor === 'object' ? requestor.hash : String(requestor); reportException(`hashLookupFailureD1`, new HashLookupFailure(table, id), { requestingEntryInfo, failedHash: id, failedComponent: table, }); } return dbEntry; }, getAll() { return dbTable; }, }; } dispatch(setD1Manifest(defs as D1ManifestDefinitions)); return defs as D1ManifestDefinitions; }; } ================================================ FILE: src/app/destiny1/d1-factions.ts ================================================ import { D1Item } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { findItemsByBucket } from 'app/inventory/stores-helpers'; import { HashLookup } from 'app/utils/util-types'; // In D1 there were exotic ghosts that you could only equip if you also had a "Faction Badge" equipped // that matched that faction. These functions help identify what faction badge is equipped on the character // and what badge a particular item requires. // Maps inventory item hash to faction const factionBadges: HashLookup<string> = { 969832704: 'Future War Cult', 27411484: 'Dead Orbit', 2954371221: 'New Monarchy', }; // Maps talent grid node to faction const factionNodes: HashLookup<string> = { 2669659850: 'Future War Cult', 2794386410: 'Dead Orbit', 652505621: 'New Monarchy', }; // Maps faction definition hash to faction name const factionsByHash: HashLookup<string> = { 489342053: 'Future War Cult', 2397602219: 'Dead Orbit', 3197190122: 'New Monarchy', }; /** What faction is this character aligned with (by equipping that faction's badge)? */ function factionAlignment(store: DimStore): string | null { const badge = findItemsByBucket(store, 375726501).find((i) => factionBadges[i.hash]); if (!badge) { return null; } return factionBadges[badge.hash] ?? null; } /** * Check to see if this item has a node that restricts it to a * certain faction, and if the character is aligned with that * faction. */ export function factionItemAligns(store: DimStore, item: D1Item) { if (!item.talentGrid) { return true; } const factionNode = item.talentGrid.nodes.find((n) => factionNodes[n.hash]); if (!factionNode) { return true; } return factionNodes[factionNode.hash] === factionAlignment(store); } /** * Whether or not this character is aligned with the given faction definition hash. */ export function factionAligned(store: DimStore, factionHash: number) { const alignment = factionAlignment(store); return alignment === factionsByHash[factionHash]; } ================================================ FILE: src/app/destiny1/d1-manifest-types.ts ================================================ import { D1Progression } from 'app/inventory/store-types'; import { BungieMembershipType, DamageType, DestinyClass, DestinyGender, DestinyInventoryItemStatDefinition, DestinyItemQuantity, DestinyItemSubType, DestinyItemType, DestinyProgressionScope, DestinyProgressionStepDefinition, DestinyRace, DestinyStat, DestinyStatAggregationType, DestinyTalentNodeState, DestinyTalentNodeStepGroups, DestinyUnlockValueUIStyle, ItemBindStatus, ItemState, SpecialItemType, TierType, TransferStatuses, } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes, StatHashes } from 'data/d2/generated-enums'; export const enum D1StatHashes { Intellect = StatHashes.Super, Discipline = StatHashes.Grenade, Strength = StatHashes.Melee, Recovery = StatHashes.Class, Resilience = StatHashes.Health, Mobility = StatHashes.Weapons, } export interface AllD1DestinyManifestComponents { DestinyRecordDefinition: { [hash: number]: D1RecordDefinition }; DestinyItemCategoryDefinition: { [hash: number]: D1ItemCategoryDefinition }; DestinyVendorCategoryDefinition: { [hash: number]: D1VendorCategoryDefinition }; DestinyRecordBookDefinition: { [hash: number]: D1RecordBookDefinition }; DestinyActivityDefinition: { [hash: number]: D1ActivityDefinition }; DestinyActivityTypeDefinition: { [hash: number]: D1ActivityTypeDefinition }; DestinyDamageTypeDefinition: { [hash: number]: D1DamageTypeDefinition }; DestinyInventoryBucketDefinition: { [hash: number]: D1InventoryBucketDefinition }; DestinyClassDefinition: { [hash: number]: D1ClassDefinition }; DestinyRaceDefinition: { [hash: number]: D1RaceDefinition }; DestinyFactionDefinition: { [hash: number]: D1FactionDefinition }; DestinyVendorDefinition: { [hash: number]: D1VendorDefinition }; DestinyInventoryItemDefinition: { [key: number]: D1InventoryItemDefinition }; DestinyObjectiveDefinition: { [key: number]: D1ObjectiveDefinition }; DestinyStatDefinition: { [key: number]: D1StatDefinition }; DestinyTalentGridDefinition: { [key: number]: D1TalentGridDefinition }; DestinyProgressionDefinition: { [key: number]: D1ProgressionDefinition }; } export interface D1TalentNode { isActivated: boolean; stepIndex: number; state: DestinyTalentNodeState; hidden: boolean; nodeHash: number; } export interface D1Perk { iconPath: string; perkHash: number; isActive: boolean; } export interface D1ItemSourceDefinition { expansionIndex: number; level: number; minQuality: number; maxQuality: number; minLevelRequired: number; maxLevelRequired: number; exclusivity: number; computedStats: { [statHash: number]: D1Stat }; sourceHashes: number[]; } export interface D1Stat extends DestinyStat { maximumValue: number; } export interface D1ItemComponent { itemHash: number; bindStatus: ItemBindStatus; isEquipped: boolean; itemInstanceId: string; itemLevel: number; stackSize: number; qualityLevel: number; stats: D1Stat[]; primaryStat?: D1Stat; canEquip: boolean; equipRequiredLevel: number; unlockFlagHashRequiredToEquip: number; cannotEquipReason: number; damageType: DamageType; damageTypeHash: number; damageTypeNodeIndex: number; damageTypeStepIndex: number; progression?: D1LevelProgression; talentGridHash: number; nodes: D1TalentNode[]; useCustomDyes: boolean; isEquipment: boolean; isGridComplete: boolean; perks: D1Perk[]; location: number; transferStatus: TransferStatuses; locked: boolean; lockable: boolean; objectives: D1ObjectiveProgress[]; state: ItemState; bucket: number; } export interface D1InventoryItemDefinition { itemHash: number; itemName: string; itemDescription: string; icon: string; hasIcon: boolean; secondaryIcon: string; actionName: string; hasAction: boolean; deleteOnAction: boolean; tierTypeName: string; tierType: TierType; itemTypeName: string; bucketTypeHash: number; primaryBaseStatHash: number; stats: { [key: number]: DestinyInventoryItemStatDefinition; }; perkHashes: number[]; specialItemType: SpecialItemType; talentGridHash: number; hasGeometry: boolean; statGroupHash: number; itemLevels: number[]; qualityLevel: number; equippable: boolean; instanced: boolean; rewardItemHash: number; values: object; itemType: DestinyItemType; itemSubType: DestinyItemSubType; classType: DestinyClass; sources: D1ItemSourceDefinition[]; itemCategoryHashes: ItemCategoryHashes[]; sourceHashes: number[]; nonTransferrable: boolean; exclusive: BungieMembershipType; maxStackSize: number; itemIndex: number; setItemHashes: number[]; tooltipStyle: string; questlineItemHash: number; needsFullCompletion: boolean; objectiveHashes: number[]; allowActions: boolean; questTrackingUnlockValueHash: number; bountyResetUnlockHash: number; uniquenessHash: number; showActiveNodesInTooltip: boolean; hash: number; index: number; redacted: boolean; } export interface D1DamageTypeDefinition { damageTypeHash: number; identifier: string; damageTypeName: string; description: string; iconPath: string; transparentIconPath: string; showIcon: boolean; enumValue: number; hash: number; index: number; redacted: boolean; } export interface D1TalentGridNodeStepDefinition { stepIndex: number; nodeStepHash: number; nodeStepName?: string; nodeStepDescription?: string; interactionDescription?: string; icon: string; damageType: number; damageTypeHash: number; activationRequirement: { gridLevel: number; materialRequirementHashes: []; exclusiveSetRequiredHash: number; }; canActivateNextStep: boolean; nextStepIndex: number; isNextStepRandom: boolean; perkHashes: []; startProgressionBarAtProgress: number; statHashes: number[]; affectsQuality: boolean; stepGroups: DestinyTalentNodeStepGroups; trueStepIndex: number; truePropertyIndex: number; affectsLevel: boolean; } export interface D1TalentGridNodeDefinition { nodeIndex: number; nodeHash: number; row: number; column: number; prerequisiteNodeIndexes: number[]; binaryPairNodeIndex: number; autoUnlocks: boolean; lastStepRepeats: boolean; isRandom: boolean; randomActivationRequirement: { gridLevel: number; materialRequirementHashes: number[]; exclusiveSetRequiredHash: number; }; isRandomRepurchasable: boolean; steps: D1TalentGridNodeStepDefinition[]; exlusiveWithNodes: number[]; randomStartProgressionBarAtProgression: number; originalNodeHash: number; talentNodeTypes: number; exclusiveSetHash: number; isRealStepSelectionRandom: boolean; } export interface D1TalentGridDefinition { gridHash: number; maxGridLevel: number; gridLevelPerColumn: number; progressionHash: number; nodes: D1TalentGridNodeDefinition[]; calcMaxGridLevel: number; calcProgressToMaxLevel: number; exclusiveSets: { nodeIndexes: number[]; }[]; independentNodeIndexes: number[]; maximumRandomMaterialRequirements: number; hash: number; index: number; redacted: boolean; } export interface D1ClassDefinition { classHash: number; classType: DestinyClass; className: string; classNameMale: string; classNameFemale: string; classIdentifier: string; mentorVendorIdentifier: string; hash: number; index: number; redacted: boolean; } export interface D1StatDefinition { statHash: number; statName: string; statDescription: string; icon: string; statIdentifier: D1StatLabel; aggregationType: DestinyStatAggregationType; hasComputedBlock: boolean; interpolate: boolean; hash: number; index: number; redacted: boolean; } export interface D1ProgressionDefinition { progressionHash: number; name: string; scope: DestinyProgressionScope; repeatLastStep: boolean; icon: string; steps: DestinyProgressionStepDefinition[]; visible: boolean; hash: number; index: number; redacted: boolean; } export interface D1ObjectiveDefinition { objectiveHash: number; unlockValueHash: number; completionValue: number; vendorHash: number; vendorCategoryHash: number; displayDescription: string; locationHash: number; allowNegativeValue: boolean; allowValueChangeWhenCompleted: boolean; isCountingDownward: boolean; valueStyle: DestinyUnlockValueUIStyle; hash: number; index: number; contentIdentifier: string; redacted: boolean; } export interface D1ObjectiveProgress { objectiveHash: number; destinationHash: number; activityHash: number; progress: number; hasProgress: boolean; isComplete: boolean; displayValue: number; } export interface D1RecordComponent { recordHash: number; objectives: D1ObjectiveProgress[]; status: number; scramble: boolean; } export interface D1RecordDefinition { displayName: string; description: string; recordValueUIStyle: string; icon: string; style: number; rewards: never[]; actualRewards: never[]; objectives: { objectiveHash: number }[]; hash: number; index: number; contentIdentifier: string; redacted: boolean; } export interface D1ProgressionStep { progressTotal: number; rewardItems: DestinyItemQuantity[]; } export interface D1RecordBook { bookHash: number; records: { [recordHash: number]: D1RecordComponent }; progression: D1Progression; completedCount: number; redeemedCount: number; spotlights: never[]; startDate: string; expirationDate: string; progress: D1Progression; percentComplete: number; } export interface D1RecordBookPageDefinition { displayName: string; displayDescription: string; displayStyle: number; records: { recordHash: number; spotlight: boolean; scrambled: boolean; }[]; rewards: { visible: boolean; itemHash: number; requirementUnlockExpressions: string[]; requirementProgressionLevel: number; claimedUnlockHash: number; canReclaim: boolean; quantity: number; }[]; } export interface D1RecordBookDefinition { bookAvailableUnlockExpression: { steps: { stepOperator: number; flagHash: number; valueHash: number; value: number; }[]; }; activeRanges: { start: string; end: string; }[]; pages: D1RecordBookPageDefinition[]; displayName: string; displayDescription: string; icon: string; unavailableReason: string; progressionHash: number; recordCount: number; bannerImage: string; itemHash: number; hash: number; index: number; contentIdentifier: string; redacted: boolean; } export interface D1ItemCategoryDefinition { itemCategoryHash: number; identifier: string; visible: boolean; title: string; shortTitle: string; description: string; grantDestinyItemType: DestinyItemType; grantDestinySubType: DestinyItemSubType; grantDestinyClass: DestinyClass; hash: number; index: number; redacted: boolean; } export interface D1VendorCategoryDefinition { categoryHash: number; order: number; categoryName: string; mobileBannerPath: string; identifier: string; hash: number; index: number; redacted: boolean; } export interface D1ActivityTier { activityHash: number; tierDisplayName: 'Normal' | 'Hard'; completion: { complete: boolean; success: boolean; }; steps: { complete: boolean; }[]; skullCategories: D1SkullCategory[]; rewards: { rewardItems: DestinyItemQuantity[]; }[]; activityData: { activityHash: number; isNew: boolean; canLead: boolean; canJoin: boolean; isCompleted: boolean; isVisible: boolean; displayLevel: number; recommendedLight: number; difficultyTier: number; }; } export interface D1SkullCategory { title: string; skulls: { displayName: string; description: string; icon: string; }[]; } export interface D1ActivityComponent { identifier: string; status: { expirationDate: string; startDate: string; expirationKnown: boolean; active: boolean; }; display: { categoryHash: number; icon: string; image: string; advisorTypeCategory: string; activityHash: number; destinationHash: number; placeHash: number; about: string; status: string; tips: string[]; recruitmentIds: string[]; }; activityTiers: D1ActivityTier[]; extended?: { highestWinRank: number; objectives: D1ObjectiveProgress[]; skullCategories: D1SkullCategory[]; }; } export interface D1ActivityDefinition { activityHash: number; activityName: string; activityDescription: string; icon: string; releaseIcon: string; releaseTime: number; activityLevel: number; completionFlagHash: number; activityPower: number; minParty: number; maxParty: number; maxPlayers: number; destinationHash: number; placeHash: number; activityTypeHash: number; tier: number; pgcrImage: string; rewards: DestinyItemQuantity[]; skulls: { displayName: string; description: string }[]; isPlaylist: boolean; isMatchmade: boolean; hash: number; index: number; redacted: boolean; } export interface D1ActivityTypeDefinition { activityTypeHash: number; identifier: string; activityTypeName: string; icon: string; activeBackgroundVirtualPath: string; completedBackgroundVirtualPath: string; hiddenOverrideVirtualPath: string; tooltipBackgroundVirtualPath: string; enlargedActiveBackgroundVirtualPath: string; enlargedCompletedBackgroundVirtualPath: string; enlargedHiddenOverrideVirtualPath: string; enlargedTooltipBackgroundVirtualPath: string; order: number; hash: number; index: number; redacted: boolean; } export interface D1InventoryBucketDefinition { bucketHash: number; bucketName: string; bucketDescription: string; scope: number; category: number; bucketOrder: number; bucketIdentifier: string; itemCount: number; location: number; hasTransferDestination: boolean; enabled: boolean; fifo: boolean; hash: number; index: number; redacted: boolean; } export interface D1RaceDefinition { raceHash: number; raceType: DestinyRace; raceName: string; raceNameMale: string; raceNameFemale: string; raceDescription: string; hash: number; index: number; redacted: boolean; } export interface D1FactionDefinition { factionHash: number; factionName: string; factionDescription: string; factionIcon: string; progressionHash: number; hash: number; index: number; redacted: boolean; } export interface D1VendorDefinition { summary: { vendorHash: number; vendorName: string; vendorDescription: string; vendorIcon: string; vendorOrder: number; factionName: string; factionIcon: string; factionHash: number; factionDescription: string; resetIntervalMinutes: number; resetOffsetMinutes: number; vendorIdentifier: string; positionX: number; positionY: number; transitionNodeIdentifier: string; visible: boolean; progressionHash: number; sellString: string; buyString: string; vendorPortrait: string; vendorBanner: string; unlockFlagHashes: number[]; enabledUnlockFlagHashes: number[]; mapSectionIdentifier: string; mapSectionName: string; mapSectionOrder: number; showOnMap: boolean; eventHash: number; vendorCategoryHash: number; vendorCategoryHashes: number[]; vendorSubcategoryHash: number; inhibitBuying: boolean; }; acceptedItems: never[]; categories: { categoryHash: number; categoryIndex: number; displayTitle: string; overlayCurrencyItemHash: number; quantityAvailable: number; showUnavailableItems: boolean; hideIfNoCurrency: boolean; buyStringOverride: string; overlayTitle: string; overlayDescription: string; overlayChoice: string; overlayIcon: string; hasOverlay: boolean; hideFromRegularPurchase: boolean; }[]; failureStrings: string[]; sales: { priceOverride: boolean; itemHash: number; bucketHash: number; categoryHash: number; categoryIndex: number; quantityPurchased: number; licenseUnlockHash: number; currencies: [ { itemHash: number; quantity: number; }, ]; price: number; currencyHash: number; hasCurrency: boolean; failureIndexes: number[]; refundPolicy: number; refundLimit: number; seedOverride: number; weight: number; requiredLevel: number; creationLevel: number; saleItemIndex: number; originalCategoryIndex: number; minimumLevel: number; maximumLevel: number; }[]; unlockValueHash: number; hash: number; index: number; redacted: boolean; } export interface D1StoresData { characters: D1CharacterData[]; profileInventory: D1Inventory; vaultInventory: D1VaultInventory; } export interface D1CharacterData { id: string; character: D1Character; inventory: D1Inventory; progression?: D1GetProgressionResponse['data']; advisors?: D1GetAdvisorsResponse['data']; } export interface D1GetAccountResponse { data: { membershipId: string; membershipType: BungieMembershipType; characters: D1Character[]; inventory: D1Inventory; grimoireScore: number; dateLastPlayed: string; versions: number; }; } export interface D1GetInventoryResponse { data: D1Inventory; } export interface D1GetVaultInventoryResponse { data: D1VaultInventory; } export interface D1Character { characterBase: D1CharacterBase; levelProgression: D1LevelProgression; emblemPath: string; backgroundPath: string; emblemHash: number; characterLevel: number; baseCharacterLevel: number; isPrestigeLevel: boolean; percentToNextLevel: number; } export interface D1CharacterBase { membershipId: string; membershipType: BungieMembershipType; characterId: string; dateLastPlayed: string; minutesPlayedThisSession: string; minutesPlayedTotal: string; powerLevel: number; raceHash: number; genderHash: number; classHash: number; currentActivityHash: number; lastCompletedStoryHash: number; stats: D1Stats; grimoireScore: number; genderType: DestinyGender; classType: DestinyClass; buildStatGroupHash: number; peerView?: { equipment: { itemHash: number }[] }; } export interface D1LevelProgression { dailyProgress: number; weeklyProgress: number; currentProgress: number; level: number; step: number; progressToNextLevel: number; nextLevelAt: number; progressionHash: number; } export interface D1Inventory { buckets: { [bucketLabel in D1BucketLabel]: D1Bucket[] }; currencies: { itemHash: number; value: number }[]; } export interface D1VaultInventory { buckets: { [bucketLabel in D1BucketLabel]: D1Bucket }; } export type D1BucketLabel = 'Invisible' | 'Item' | 'Currency'; export interface D1Bucket { items: D1ItemComponent[]; bucketHash: number; } export interface D1GetProgressionResponse { data: { progressions: D1Progression[]; levelProgression: D1LevelProgression; baseCharacterLevel: number; isPrestigeLevel: boolean; factionProgressionHash: number; percentToNextLevel: number; }; } export interface D1GetAdvisorsResponse { data: { activities: { [activityLabel: string]: D1ActivityComponent }; activityCategories: { [activityHash: number]: { categoryHash: number } }; bounties: { [pursuitHash: number]: D1Pursuit }; quests: { [pursuitHash: number]: D1Pursuit }; progressions: { [progressionHash: number]: D1Progression }; recordBooks: { [recordBookHash: number]: D1RecordBook }; }; } export interface D1Pursuit { questHash: number; stepHash: number; stepObjectives: any[]; tracked: boolean; itemInstanceId: string; completed: boolean; started: boolean; vendorHash: number; } export type D1StatLabel = | 'STAT_DEFENSE' | 'STAT_INTELLECT' | 'STAT_DISCIPLINE' | 'STAT_STRENGTH' | 'STAT_LIGHT' | 'STAT_ARMOR' | 'STAT_AGILITY' | 'STAT_RECOVERY' | 'STAT_OPTICS' | 'STAT_MAGAZINE_SIZE' | 'STAT_ATTACK_ENERGY'; export type D1Stats = { [stat in D1StatLabel]: D1Stat | undefined }; ================================================ FILE: src/app/destiny1/loadout-builder/D1LoadoutBuilder.m.scss ================================================ @use '../../variables' as *; @use './LoadoutBuilderLocksDialog.m.scss' as locks; .lockedButton { composes: dim-button from global; } // TODO: Combine with the controls-strip in GeneratedSet .controls { composes: flexWrap from '../../dim-ui/common.m.scss'; align-items: center; gap: 8px; margin: 12px 0; } .excludedItems { border: 2px solid #ddd; display: flex; flex-flow: row wrap; gap: 10px; width: calc((var(--item-size) + 14px) * 7); min-height: var(--item-size); max-width: 100%; padding: 5px; } .itemRow { composes: flexWrap from '../../dim-ui/common.m.scss'; width: 100%; align-items: flex-start; gap: 16px; } .example { width: 10px; height: 10px; display: inline-block; .or { background-color: locks.$or-color; } .and { background-color: locks.$and-color; } } .section { margin-bottom: 16px; @include phone-portrait { margin-left: var(--inventory-column-padding); margin-right: var(--inventory-column-padding); } } ================================================ FILE: src/app/destiny1/loadout-builder/D1LoadoutBuilder.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'and': string; 'controls': string; 'example': string; 'excludedItems': string; 'itemRow': string; 'lockedButton': string; 'or': string; 'perk': string; 'perkSelectBox': string; 'perkSelectPopup': string; 'section': string; 'shiftHeld': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-builder/D1LoadoutBuilder.tsx ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import ClosableContainer from 'app/dim-ui/ClosableContainer'; import PageWithMenu from 'app/dim-ui/PageWithMenu'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import Switch from 'app/dim-ui/Switch'; import { t } from 'app/i18next-t'; import { useLoadStores } from 'app/inventory/store/hooks'; import { getCurrentStore } from 'app/inventory/stores-helpers'; import { useD1Definitions } from 'app/manifest/selectors'; import { D1_StatHashes, D1BucketHashes } from 'app/search/d1-known-values'; import { getD1QualityColor } from 'app/shell/formatters'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { uniqBy } from 'app/utils/collections'; import { compareBy, reverseComparator } from 'app/utils/comparators'; import { itemCanBeInLoadout } from 'app/utils/item-utils'; import { errorLog } from 'app/utils/log'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { BucketHashes, ItemCategoryHashes } from 'data/d2/generated-enums'; import { produce } from 'immer'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import CharacterSelect from '../../dim-ui/CharacterSelect'; import CollapsibleTitle from '../../dim-ui/CollapsibleTitle'; import ErrorBoundary from '../../dim-ui/ErrorBoundary'; import { D1GridNode, D1Item, DimItem } from '../../inventory/item-types'; import { bucketsSelector, sortedStoresSelector } from '../../inventory/selectors'; import { D1Store } from '../../inventory/store-types'; import { AppIcon, refreshIcon } from '../../shell/icons'; import { loadVendors, Vendor } from '../vendors/vendor.service'; import * as styles from './D1LoadoutBuilder.m.scss'; import ExcludeItemsDropTarget from './ExcludeItemsDropTarget'; import GeneratedSet from './GeneratedSet'; import LoadoutBuilderItem from './LoadoutBuilderItem'; import LoadoutBuilderLockPerk from './LoadoutBuilderLockPerk'; import { getSetBucketsStep } from './calculate'; import { ArmorTypes, ClassTypes, D1ItemWithNormalStats, LockedPerkHash, PerkCombination, SetType, } from './types'; import { getActiveHighestSets, loadBucket, loadVendorsBucket, mergeBuckets } from './utils'; interface State { selectedCharacter?: D1Store; excludeditems: D1Item[]; lockedperks: { [armorType in ArmorTypes]: LockedPerkHash }; activesets: string; scaleType: 'base' | 'scaled'; progress: number; fullMode: boolean; includeVendors: boolean; allSetTiers: string[]; highestsets: { [setHash: number]: SetType }; lockeditems: { [armorType in ArmorTypes]: D1ItemWithNormalStats | null }; vendors?: { [vendorHash: number]: Vendor; }; loadingVendors: boolean; } export const d1ArmorTypes = [ BucketHashes.Helmet, BucketHashes.Gauntlets, BucketHashes.ChestArmor, BucketHashes.LegArmor, BucketHashes.ClassArmor, D1BucketHashes.Artifact, BucketHashes.Ghost, ] as ArmorTypes[]; const allClassTypes: ClassTypes[] = [DestinyClass.Titan, DestinyClass.Warlock, DestinyClass.Hunter]; const initialState: State = { activesets: '5/5/2', scaleType: 'scaled', progress: 0, fullMode: false, includeVendors: false, loadingVendors: false, allSetTiers: [], highestsets: {}, excludeditems: [], lockeditems: { [BucketHashes.Helmet]: null, [BucketHashes.Gauntlets]: null, [BucketHashes.ChestArmor]: null, [BucketHashes.LegArmor]: null, [BucketHashes.ClassArmor]: null, [D1BucketHashes.Artifact]: null, [BucketHashes.Ghost]: null, }, lockedperks: { [BucketHashes.Helmet]: {}, [BucketHashes.Gauntlets]: {}, [BucketHashes.ChestArmor]: {}, [BucketHashes.LegArmor]: {}, [BucketHashes.ClassArmor]: {}, [D1BucketHashes.Artifact]: {}, [BucketHashes.Ghost]: {}, }, }; const unwantedPerkHashes = [ 1270552711, // Infuse 217480046, // Twist Fate 191086989, // Reforge Artifact 913963685, // Reforge Shell 1034209669, // Increase Intellect 1263323987, // Increase Discipline 193091484, // Increase Strength 2133116599, // Deactivate Chroma ]; export default function D1LoadoutBuilder({ account }: { account: DestinyAccount }) { const buckets = useSelector(bucketsSelector); const stores = useSelector(sortedStoresSelector) as D1Store[]; const defs = useD1Definitions(); const [state, setStateFull] = useState(initialState); const setState = (partialState: Partial<State>) => setStateFull((state: State) => ({ ...state, ...partialState })); const cancelToken = useRef({ cancelled: false }); const dispatch = useThunkDispatch(); const storesLoaded = useLoadStores(account); const { includeVendors, loadingVendors, excludeditems, progress, allSetTiers, fullMode, scaleType, activesets, lockeditems, lockedperks, highestsets, vendors, } = state; const selectedCharacter = state.selectedCharacter || getCurrentStore(stores); useEffect(() => { if (storesLoaded) { // Exclude felwinters if we have them, but only the first time stores load const felwinters = stores.flatMap((store) => store.items.filter((i) => i.hash === 2672107540), ); if (felwinters.length) { setStateFull((state) => ({ ...state, excludeditems: uniqBy([...state.excludeditems, ...felwinters], (i) => i.id), })); } } // Only depend on storesLoaded because we only want this to run once // eslint-disable-next-line react-hooks/exhaustive-deps }, [storesLoaded]); useEffect(() => { // TODO: replace progress with state field (calculating/done) if (defs && selectedCharacter && stores.length && !progress) { (async () => { cancelToken.current.cancelled = true; cancelToken.current = { cancelled: false, }; const result = await getSetBucketsStep( defs, loadBucket(selectedCharacter, stores), loadVendorsBucket(selectedCharacter, vendors), lockeditems, lockedperks, excludeditems, scaleType, includeVendors, fullMode, cancelToken.current, ); setState({ ...result, progress: 1 }); })(); } }, [ defs, selectedCharacter, excludeditems, fullMode, includeVendors, lockeditems, lockedperks, progress, scaleType, vendors, stores, ]); useEffect(() => { if (includeVendors && !vendors && !loadingVendors) { setState({ loadingVendors: true }); dispatch(loadVendors()).then((vendors) => { setState({ vendors, loadingVendors: false }); }); } }, [dispatch, includeVendors, loadingVendors, vendors]); const vendorsLoaded = Boolean(vendors); useEffect(() => { if (vendorsLoaded) { const felwinters = Object.values(vendors!).flatMap((vendor) => vendor.allItems.filter((i) => i.item.hash === 2672107540), ); if (felwinters.length) { setState({ excludeditems: uniqBy( [...excludeditems, ...felwinters.map((si) => si.item)], (i) => i.id, ), }); } } // Only depend on vendorsLoaded because we only want this to run once // eslint-disable-next-line react-hooks/exhaustive-deps }, [vendorsLoaded]); const activePerks = useActivePerks({ classType: selectedCharacter?.classType, vendors, includeVendors, stores, }); const onFullModeChanged: React.ChangeEventHandler<HTMLSelectElement> = (e) => { const fullMode = e.target.value === 'true'; setState({ fullMode, progress: 0 }); }; const onChange: React.ChangeEventHandler<HTMLSelectElement> = (e) => { if (e.target.name.length === 0) { errorLog('loadout optimizer', new Error('You need to have a name on the form input')); } setState({ [e.target.name]: e.target.value, progress: 0, }); }; const onActiveSetsChange: React.ChangeEventHandler<HTMLSelectElement> = (e) => { const activesets = e.target.value; setState({ activesets, }); }; const onSelectedChange = (storeId: string) => { // TODO: reset more state?? setState({ selectedCharacter: stores.find((s) => s.id === storeId), progress: 0, }); }; const onIncludeVendorsChange = (includeVendors: boolean) => { setState({ includeVendors, progress: 0 }); }; const onPerkLocked = (perk: D1GridNode, type: ArmorTypes, $event: React.MouseEvent) => { const { lockedperks } = state; const lockedPerk = lockedperks[type][perk.hash]; const activeType = $event.shiftKey ? lockedPerk?.lockType === 'and' ? 'none' : 'and' : lockedPerk?.lockType === 'or' ? 'none' : 'or'; const newLockedPerks = produce(lockedperks, (lockedperks) => { if (activeType === 'none') { delete lockedperks[type][perk.hash]; } else { lockedperks[type][perk.hash] = { icon: perk.icon, description: perk.description, lockType: activeType, }; } }); setState({ lockedperks: newLockedPerks, progress: 0 }); }; const onItemLocked = (item: DimItem) => { setStateFull((state) => ({ ...state, lockeditems: { ...state.lockeditems, [item.bucket.hash]: item }, progress: 0, })); }; const onRemove = ({ type }: { type: ArmorTypes }) => { setStateFull((state) => ({ ...state, lockeditems: { ...state.lockeditems, [type]: null }, progress: 0, })); }; const excludeItem = (item: D1Item) => { setStateFull((state) => ({ ...state, excludeditems: [...state.excludeditems, item], progress: 0, })); }; const onExcludedRemove = (item: DimItem) => { setStateFull((state) => ({ ...state, excludeditems: state.excludeditems.filter( (excludeditem) => excludeditem.index !== item.index, ), progress: 0, })); }; const lockEquipped = () => { const items = Map.groupBy( selectedCharacter!.items.filter( (item) => itemCanBeInLoadout(item) && item.equipped && d1ArmorTypes.includes(item.bucket.hash), ), (i) => i.bucket.hash as ArmorTypes, ); function nullWithoutStats(items: DimItem[] | undefined) { return items?.[0].stats ? (items[0] as D1Item) : null; } // Do not lock items with no stats setState({ lockeditems: { [BucketHashes.Helmet]: nullWithoutStats(items.get(BucketHashes.Helmet)), [BucketHashes.Gauntlets]: nullWithoutStats(items.get(BucketHashes.Gauntlets)), [BucketHashes.ChestArmor]: nullWithoutStats(items.get(BucketHashes.ChestArmor)), [BucketHashes.LegArmor]: nullWithoutStats(items.get(BucketHashes.LegArmor)), [BucketHashes.ClassArmor]: nullWithoutStats(items.get(BucketHashes.ClassArmor)), [D1BucketHashes.Artifact]: nullWithoutStats(items.get(D1BucketHashes.Artifact)), [BucketHashes.Ghost]: nullWithoutStats(items.get(BucketHashes.Ghost)), }, progress: 0, }); }; const clearLocked = () => { setState({ lockeditems: { [BucketHashes.Helmet]: null, [BucketHashes.Gauntlets]: null, [BucketHashes.ChestArmor]: null, [BucketHashes.LegArmor]: null, [BucketHashes.ClassArmor]: null, [D1BucketHashes.Artifact]: null, [BucketHashes.Ghost]: null, }, activesets: '', progress: 0, }); }; if (!selectedCharacter || !stores.length || !buckets || !defs || !activePerks) { return <ShowPageLoading message={t('Loading.Profile')} />; } const hasSets = allSetTiers.length > 0; const activeHighestSets = getActiveHighestSets(highestsets, activesets); const i18nItemNames = Object.fromEntries( d1ArmorTypes.map((type, i) => [ type, defs.ItemCategory.get( [ ItemCategoryHashes.Helmets, ItemCategoryHashes.Arms, ItemCategoryHashes.Chest, ItemCategoryHashes.Legs, ItemCategoryHashes.ClassItems, 38, // D1 Artifact ItemCategoryHashes.Ghost, ][i], ).title, ]), ) as { [key in ArmorTypes]: string }; return ( <PageWithMenu className="itemQuality"> <PageWithMenu.Menu> <CharacterSelect selectedStore={selectedCharacter} stores={stores} onCharacterChanged={onSelectedChange} /> </PageWithMenu.Menu> <PageWithMenu.Contents> <CollapsibleTitle defaultCollapsed={true} sectionId="lb1-classitems" title={t('LB.ShowGear', { class: selectedCharacter.className })} > <div className={styles.section}> <ArmorForClass i18nItemNames={i18nItemNames} selectedCharacter={selectedCharacter} stores={stores} includeVendors={includeVendors} loadingVendors={loadingVendors} onIncludeVendorsChange={onIncludeVendorsChange} excludeItem={excludeItem} /> </div> </CollapsibleTitle> <div className={styles.section}> <div className={styles.controls}> <button type="button" className="dim-button" onClick={lockEquipped}> {t('LB.LockEquipped')} </button> <button type="button" className="dim-button" onClick={clearLocked}> {t('LB.ClearLocked')} </button> <span> {t('LB.Locked')} - <small>{t('LB.LockedHelp')}</small> </span> </div> <div className={styles.itemRow}> {Object.entries(lockeditems).map(([type, lockeditem]) => ( <LoadoutBuilderLockPerk key={type} lockeditem={lockeditem} activePerks={activePerks} lockedPerks={lockedperks} type={parseInt(type, 10) as ArmorTypes} i18nItemNames={i18nItemNames} onRemove={onRemove} onPerkLocked={onPerkLocked} onItemLocked={onItemLocked} /> ))} </div> </div> {excludeditems.length > 0 && ( <div className={styles.section}> <p> <span>{t('LB.Exclude')}</span> - <small>{t('LB.ExcludeHelp')}</small> </p> <div className={styles.itemRow}> <ExcludeItemsDropTarget onExcluded={excludeItem} className={styles.excludedItems}> {excludeditems.map((excludeditem) => ( <ClosableContainer key={excludeditem.index} onClose={() => onExcludedRemove(excludeditem)} > <LoadoutBuilderItem item={excludeditem} /> </ClosableContainer> ))} </ExcludeItemsDropTarget> </div> </div> )} {progress >= 1 && hasSets && ( <SetControls allSetTiers={allSetTiers} activesets={activesets} fullMode={fullMode} scaleType={scaleType} onActiveSetsChange={onActiveSetsChange} onFullModeChanged={onFullModeChanged} onChangeScaleType={onChange} /> )} {progress >= 1 && !hasSets && ( <div> <p>{t('LB.Missing2')}</p> </div> )} {progress < 1 && hasSets && ( <div> <p> {t('LB.Loading')} <AppIcon spinning={true} icon={refreshIcon} /> </p> </div> )} {progress >= 1 && ( <ErrorBoundary name="Generated Sets"> <div className={styles.section}> {activeHighestSets.map((setType) => ( <GeneratedSet key={setType.set.setHash} store={selectedCharacter} setType={setType} activesets={activesets} excludeItem={excludeItem} /> ))} </div> </ErrorBoundary> )} </PageWithMenu.Contents> </PageWithMenu> ); } function SetControls({ allSetTiers, activesets, fullMode, scaleType, onActiveSetsChange, onFullModeChanged, onChangeScaleType, }: { allSetTiers: string[]; activesets: string; fullMode: boolean; scaleType: 'base' | 'scaled'; onActiveSetsChange: React.ChangeEventHandler<HTMLSelectElement>; onFullModeChanged: React.ChangeEventHandler<HTMLSelectElement>; onChangeScaleType: React.ChangeEventHandler<HTMLSelectElement>; }) { const [showAdvanced, setShowAdvanced] = useState(false); const [showHelp, setShowHelp] = useState(false); const toggleShowHelp = () => setShowHelp((show) => !show); const toggleShowAdvanced = () => setShowAdvanced((show) => !show); return ( <div className={styles.section}> <div className={styles.controls}> <div> <span> {t('LB.FilterSets')} ({t('Stats.Intellect')}/{t('Stats.Discipline')}/ {t('Stats.Strength')}):{' '} </span> <select name="activesets" onChange={onActiveSetsChange} value={activesets}> {allSetTiers.map((val) => ( <option key={val} disabled={val.startsWith('-')} value={val}> {val} </option> ))} </select> </div> <button type="button" className="dim-button" onClick={toggleShowAdvanced}> {t('LB.AdvancedOptions')} </button> <button type="button" className="dim-button" onClick={toggleShowHelp}> {t('LB.Help.Help')} </button> </div> <span> {showAdvanced && ( <div> <p> <label> <select name="fullMode" onChange={onFullModeChanged} value={fullMode ? 'true' : 'false'} > <option value="false">{t('LB.ProcessingMode.Fast')}</option> <option value="true">{t('LB.ProcessingMode.Full')}</option> </select>{' '} <span>{t('LB.ProcessingMode.ProcessingMode')}</span> </label> <small> {' '} - {fullMode ? t('LB.ProcessingMode.HelpFull') : t('LB.ProcessingMode.HelpFast')} </small> </p> <p> <label> <select name="scaleType" value={scaleType} onChange={onChangeScaleType}> <option value="scaled">{t('LB.Scaled')}</option> <option value="base">{t('LB.Current')}</option> </select>{' '} <span>{t('LB.LightMode.LightMode')}</span> </label> <small> {' '} - {scaleType === 'scaled' && t('LB.LightMode.HelpScaled')} {scaleType === 'base' && t('LB.LightMode.HelpCurrent')} </small> </p> </div> )} </span> <span> {showHelp && ( <div> <ul> <li>{t('LB.Help.Lock')}</li> <ul> <li>{t('LB.Help.NoPerk')}</li> <li>{t('LB.Help.MultiPerk')}</li> <li> <div className={clsx(styles.example, styles.or)}>- {t('LB.Help.Or')}</div> </li> <li> <div className={clsx(styles.example, styles.and)}>- {t('LB.Help.And')}</div> </li> </ul> <li>{t('LB.Help.DragAndDrop')}</li> <li>{t('LB.Help.ShiftClick')}</li> <li>{t('LB.Help.HigherTiers')}</li> <ul> <li>{t('LB.Help.Tier11Example')}</li> <li>{t('LB.Help.Intellect')}</li> <li>{t('LB.Help.Discipline')}</li> <li>{t('LB.Help.Strength')}</li> </ul> <li>{t('LB.Help.Synergy')}</li> <li>{t('LB.Help.ChangeNodes')}</li> <li>{t('LB.Help.StatsIncrease')}</li> </ul> </div> )} </span> </div> ); } function ArmorForClass({ i18nItemNames, selectedCharacter, stores, vendors, includeVendors, loadingVendors, onIncludeVendorsChange, excludeItem, }: { i18nItemNames: { [key in ArmorTypes]: string }; selectedCharacter: D1Store; stores: D1Store[]; vendors?: { [vendorHash: number]: Vendor; }; includeVendors: boolean; loadingVendors: boolean; onIncludeVendorsChange: (includeVendors: boolean) => void; excludeItem: (item: D1Item) => void; }) { const [type, setType] = useState<ArmorTypes>(BucketHashes.Helmet); // Armor of each type on a particular character // TODO: don't even need to load this much! let bucket = loadBucket(selectedCharacter, stores); if (includeVendors) { bucket = mergeBuckets(bucket, loadVendorsBucket(selectedCharacter, vendors)); } const items = bucket[type] .filter((i) => i.power >= 280) .sort(reverseComparator(compareBy((i) => i.quality?.min ?? 0))); return ( <> <div className={styles.controls}> <div> {/* TODO: break into its own component */} <span>{t('Bucket.Armor')}:</span>{' '} <select name="type" value={type} onChange={(e) => setType(parseInt(e.target.value, 10))}> {d1ArmorTypes.map((type) => ( <option key={type} value={type}> {i18nItemNames[type]} </option> ))} </select> </div> <div> <Switch name="includeVendors" checked={includeVendors} onChange={onIncludeVendorsChange} /> <label htmlFor="includeVendors">{t('LB.Vendor')}</label> </div> {loadingVendors && <AppIcon spinning={true} icon={refreshIcon} />} </div> <div className={styles.itemRow}> {items.map((item) => ( <div key={item.index}> {item.stats?.map((stat) => ( <div key={stat.statHash} style={getD1QualityColor( item.normalStats![stat.statHash].qualityPercentage, 'color', )} > {item.normalStats![stat.statHash].scaled === 0 && <small>-</small>} {item.normalStats![stat.statHash].scaled > 0 && ( <span> <small>{item.normalStats![stat.statHash].scaled}</small>/ <small>{stat.split}</small> </span> )} </div> ))} <LoadoutBuilderItem shiftClickCallback={excludeItem} item={item} /> </div> ))} </div> </> ); } function filterPerks(perks: D1GridNode[], item: D1Item) { if (!item.talentGrid) { return []; } return uniqBy(perks.concat(item.talentGrid.nodes), (node) => node.hash).filter( (node) => !unwantedPerkHashes.includes(node.hash), ); } function useActivePerks({ classType, vendors, includeVendors, stores, }: { classType: DestinyClass | undefined; vendors: | { [vendorHash: number]: Vendor; } | undefined; includeVendors: boolean; stores: D1Store[]; }) { return useMemo(() => { if (classType === undefined) { return undefined; } const emptyPerks = { [BucketHashes.Helmet]: [], [BucketHashes.Gauntlets]: [], [BucketHashes.ChestArmor]: [], [BucketHashes.LegArmor]: [], [BucketHashes.ClassArmor]: [], [D1BucketHashes.Artifact]: [], [BucketHashes.Ghost]: [], }; const perks: { [classType in ClassTypes]: PerkCombination } = { [DestinyClass.Warlock]: structuredClone(emptyPerks), [DestinyClass.Titan]: structuredClone(emptyPerks), [DestinyClass.Hunter]: structuredClone(emptyPerks), }; const vendorPerks: { [classType in ClassTypes]: PerkCombination } = structuredClone(perks); function filterItems(items: readonly D1Item[]) { return items.filter( (item) => item.primaryStat?.statHash === D1_StatHashes.Defense && item.talentGrid?.nodes && item.stats, ); } let allItems: D1Item[] = []; let vendorItems: D1Item[] = []; for (const store of stores) { const items = filterItems(store.items); allItems = allItems.concat(items); // Build a map of perks for (const item of items) { const itemType = item.bucket.hash as ArmorTypes; if (item.classType === DestinyClass.Unknown) { for (const classType of allClassTypes) { perks[classType][itemType] = filterPerks(perks[classType][itemType], item); } } else if (item.classType !== DestinyClass.Classified) { perks[item.classType][itemType] = filterPerks(perks[item.classType][itemType], item); } } } if (vendors && includeVendors) { // Process vendors here for (const vendor of Object.values(vendors)) { const vendItems = filterItems( vendor.allItems .map((i) => i.item) .filter( (item) => item.bucket.inArmor || item.bucket.hash === D1BucketHashes.Artifact || item.bucket.hash === BucketHashes.Ghost, ), ); vendorItems = vendorItems.concat(vendItems); // Build a map of perks for (const item of vendItems) { const itemType = item.bucket.hash as ArmorTypes; if (item.classType === DestinyClass.Unknown) { for (const classType of allClassTypes) { vendorPerks[classType][itemType] = filterPerks( vendorPerks[classType][itemType], item, ); } } else if (item.classType !== DestinyClass.Classified) { vendorPerks[item.classType][itemType] = filterPerks( vendorPerks[item.classType][itemType], item, ); } } } // Remove overlapping perks in allPerks from vendorPerks for (const [classType, perksWithType] of Object.entries(vendorPerks) as unknown as [ ClassTypes, PerkCombination, ][]) { for (const [type, perkArr] of Object.entries(perksWithType) as unknown as [ ArmorTypes, D1GridNode[], ][]) { vendorPerks[classType][type] = perkArr.filter( (perk) => !perks[classType][type].map((i) => i.hash).includes(perk.hash), ); } } } return mergeBuckets<D1GridNode[]>( perks[classType as ClassTypes], vendorPerks[classType as ClassTypes], ); }, [classType, vendors, includeVendors, stores]); } ================================================ FILE: src/app/destiny1/loadout-builder/ExcludeItemsDropTarget.tsx ================================================ import React from 'react'; import { useDrop } from 'react-dnd'; import { D1Item } from '../../inventory/item-types'; import { d1ArmorTypes } from './D1LoadoutBuilder'; import { dropClasses } from './LoadoutBuilderDropTarget'; interface Props { className?: string; children?: React.ReactNode; onExcluded: (lockedItem: D1Item) => void; } export default function ExcludeItemsDropTarget({ className, children, onExcluded }: Props) { const [{ isOver, canDrop }, dropRef] = useDrop< D1Item, unknown, { isOver: boolean; canDrop: boolean } >( () => ({ accept: d1ArmorTypes.map((h) => h.toString()), collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop() }), drop: onExcluded, }), [onExcluded], ); return ( <div ref={(el) => { dropRef(el); }} className={dropClasses(isOver, canDrop, className)} > {children} </div> ); } ================================================ FILE: src/app/destiny1/loadout-builder/GeneratedSet.m.scss ================================================ @use '../../variables' as *; .controls { composes: flexWrap from '../../dim-ui/common.m.scss'; align-items: center; margin-bottom: 7px; gap: 8px; } .talentGrid { width: 71px; max-height: 48px; } .label { text-align: center; font-size: smaller; } .expandConfigs { @include interactive($hover: true) { color: var(--theme-accent-primary); cursor: pointer; } :global(.app-icon) { margin-right: 0.25em; } } .set { composes: flexWrap from '../../dim-ui/common.m.scss'; align-items: flex-start; gap: 16px; } .setItem { composes: flexColumn from '../../dim-ui/common.m.scss'; gap: 4px; position: relative; } .dimStats { flex: 1; min-width: 100px; } .loadout { margin-bottom: 20px; } ================================================ FILE: src/app/destiny1/loadout-builder/GeneratedSet.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'controls': string; 'dimStats': string; 'expandConfigs': string; 'label': string; 'loadout': string; 'set': string; 'setItem': string; 'talentGrid': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-builder/GeneratedSet.tsx ================================================ import { t } from 'app/i18next-t'; import { findItemsByBucket } from 'app/inventory/stores-helpers'; import { applyLoadout } from 'app/loadout-drawer/loadout-apply'; import { editLoadout } from 'app/loadout-drawer/loadout-events'; import { Loadout } from 'app/loadout/loadout-types'; import { D1CharacterStats } from 'app/store-stats/D1CharacterStats'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { filterMap } from 'app/utils/collections'; import { BucketHashes } from 'data/d2/generated-enums'; import { useState } from 'react'; import { D1Item } from '../../inventory/item-types'; import { DimStore } from '../../inventory/store-types'; import ItemTalentGrid from '../../item-popup/ItemTalentGrid'; import { convertToLoadoutItem, newLoadout } from '../../loadout-drawer/loadout-utils'; import { AppIcon, faMinusSquare, faPlusSquare } from '../../shell/icons'; import { d1ArmorTypes } from './D1LoadoutBuilder'; import * as styles from './GeneratedSet.m.scss'; import LoadoutBuilderItem from './LoadoutBuilderItem'; import { ArmorSet, ArmorTypes, SetType } from './types'; interface Props { store: DimStore; setType: SetType; activesets: string; excludeItem: (item: D1Item) => void; } export default function GeneratedSet({ setType, store, activesets, excludeItem }: Props) { const [collapsed, setCollapsed] = useState(true); const dispatch = useThunkDispatch(); const subclass = findItemsByBucket(store, BucketHashes.Subclass).find((i) => i.equipped); const toggle = () => setCollapsed((collapsed) => !collapsed); const makeLoadoutFromSet = (set: ArmorSet): Loadout => { const items = filterMap(d1ArmorTypes, (bucketHash) => set.armor[bucketHash]?.item); const loadout = newLoadout( '', items.map((i) => convertToLoadoutItem(i, true)), store.classType, ); return loadout; }; const makeNewLoadout = (set: ArmorSet) => { editLoadout(makeLoadoutFromSet(set), store.id, { showClass: false, }); }; const equipItems = (set: ArmorSet) => dispatch(applyLoadout(store, makeLoadoutFromSet(set), { allowUndo: true })); return ( <div key={setType.set.setHash} className={styles.loadout}> <div className={styles.controls}> {setType.set.includesVendorItems ? ( <span>{t('LB.ContainsVendorItems')}</span> ) : ( <> <button type="button" className="dim-button" onClick={() => makeNewLoadout(setType.set)} > {t('Loadouts.Create')} </button> <button type="button" className="dim-button" onClick={() => equipItems(setType.set)}> {t('LB.Equip', { character: store.name })} </button> </> )} <div className={styles.dimStats}> <D1CharacterStats stats={setType.tiers[activesets].stats} subclassHash={subclass?.hash} /> </div> </div> <div className={styles.set}> {Object.entries(setType.set.armor).map(([type, armorpiece]) => ( <div key={type} className={styles.setItem}> <LoadoutBuilderItem shiftClickCallback={excludeItem} item={armorpiece.item} /> <ItemTalentGrid item={armorpiece.item} className={styles.talentGrid} perksOnly={true} /> <div className={styles.label}> {setType.tiers[activesets].configs[0][armorpiece.item.bucket.hash as ArmorTypes] === 'int' ? t('Stats.Intellect') : setType.tiers[activesets].configs[0][ armorpiece.item.bucket.hash as ArmorTypes ] === 'dis' ? t('Stats.Discipline') : setType.tiers[activesets].configs[0][ armorpiece.item.bucket.hash as ArmorTypes ] === 'str' ? t('Stats.Strength') : t('Stats.NoBonus')} </div> {setType.tiers[activesets].configs.map( (config, i) => i > 0 && !collapsed && ( <div key={i} className={styles.label}> {config[armorpiece.item.bucket.hash as ArmorTypes] === 'int' ? t('Stats.Intellect') : config[armorpiece.item.bucket.hash as ArmorTypes] === 'dis' ? t('Stats.Discipline') : config[armorpiece.item.bucket.hash as ArmorTypes] === 'str' ? t('Stats.Strength') : t('Stats.NoBonus')} </div> ), )} </div> ))} </div> {setType.tiers[activesets].configs.length > 1 && ( <div className={styles.expandConfigs} onClick={toggle}> {!collapsed ? ( <> <AppIcon icon={faMinusSquare} title={t('LB.HideConfigs')} /> {t('LB.HideAllConfigs')} </> ) : ( <> <AppIcon icon={faPlusSquare} title={t('LB.ShowConfigs')} /> {t('LB.ShowAllConfigs')} </> )} </div> )} </div> ); } ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderDropTarget.m.scss ================================================ .onDragEnter { background-color: rgb(200, 200, 200, 0.2); } .onDragHover { box-shadow: inset 0 0 6px 0 rgb(200, 200, 200, 0.7); } ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderDropTarget.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'onDragEnter': string; 'onDragHover': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderDropTarget.tsx ================================================ import clsx from 'clsx'; import React from 'react'; import { useDrop } from 'react-dnd'; import { DimItem } from '../../inventory/item-types'; import * as styles from './LoadoutBuilderDropTarget.m.scss'; export default function LoadoutBucketDropTarget({ bucketHash, children, onItemLocked, className, }: { bucketHash: number; className?: string; children?: React.ReactNode; onItemLocked: (lockedItem: DimItem) => void; }) { const [{ isOver, canDrop }, dropRef] = useDrop< DimItem, unknown, { isOver: boolean; canDrop: boolean } >( () => ({ accept: bucketHash.toString(), collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop() }), drop: onItemLocked, canDrop: (item) => item.bucket.hash === bucketHash, }), [bucketHash, onItemLocked], ); return ( <div ref={(el) => { dropRef(el); }} className={dropClasses(isOver, canDrop, className)} > {children} </div> ); } export function dropClasses(isOver: boolean, canDrop: boolean, className?: string) { return clsx(className, { [styles.onDragHover]: canDrop && isOver, [styles.onDragEnter]: canDrop, }); } ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderItem.m.scss ================================================ .overlayContainer { position: relative; display: inline-block; } .vendorIconBackground { background-color: rgb(100, 100, 100, 0.6); position: absolute; width: 20px; height: 20px; top: 0; left: 0; z-index: 2; > img { width: 20px; height: 20px; } } ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderItem.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'overlayContainer': string; 'vendorIconBackground': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderItem.tsx ================================================ import React from 'react'; import BungieImage from '../../dim-ui/BungieImage'; import ConnectedInventoryItem from '../../inventory/ConnectedInventoryItem'; import DraggableInventoryItem from '../../inventory/DraggableInventoryItem'; import ItemPopupTrigger from '../../inventory/ItemPopupTrigger'; import { D1Item } from '../../inventory/item-types'; import * as styles from './LoadoutBuilderItem.m.scss'; interface Props { item: D1Item & { vendorIcon?: string }; shiftClickCallback?: (item: D1Item) => void; } export default function LoadoutBuilderItem({ item, shiftClickCallback }: Props) { const onShiftClick = shiftClickCallback && ((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); shiftClickCallback(item); }); // no owner means this is a vendor item if (!item.owner) { return ( <DraggableInventoryItem item={item}> <div className={styles.overlayContainer}> <div className={styles.vendorIconBackground}> <BungieImage src={item.vendorIcon!} /> </div> <ConnectedInventoryItem item={item} onShiftClick={onShiftClick} /> </div> </DraggableInventoryItem> ); } return ( <DraggableInventoryItem item={item}> <ItemPopupTrigger item={item}> {(ref, onClick) => ( <ConnectedInventoryItem item={item} ref={ref} onClick={onClick} onShiftClick={onShiftClick} /> )} </ItemPopupTrigger> </DraggableInventoryItem> ); } ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderLockPerk.m.scss ================================================ @use '../../variables.scss' as *; .lockedItem { position: relative; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 4px; } .itemSized { width: var(--item-size); height: var(--item-size); } .emptyItem { composes: itemSized; background-color: #656565; border: 2px solid #ddd; cursor: pointer; > img { width: 100%; height: 100%; } } .lockPerkIcon { composes: flexColumn from '../../dim-ui/common.m.scss'; align-items: center; justify-content: center; height: 100%; width: 100%; opacity: 0; @include interactive($hover: true) { opacity: 1; } > small { font-size: 8px; } } ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderLockPerk.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'emptyItem': string; 'itemSized': string; 'lockPerkIcon': string; 'lockedItem': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderLockPerk.tsx ================================================ import ClosableContainer from 'app/dim-ui/ClosableContainer'; import { t } from 'app/i18next-t'; import React, { useState } from 'react'; import BungieImage from '../../dim-ui/BungieImage'; import { D1GridNode, DimItem } from '../../inventory/item-types'; import { AppIcon, plusIcon } from '../../shell/icons'; import LoadoutBucketDropTarget from './LoadoutBuilderDropTarget'; import LoadoutBuilderItem from './LoadoutBuilderItem'; import * as styles from './LoadoutBuilderLockPerk.m.scss'; import LoadoutBuilderLocksDialog from './LoadoutBuilderLocksDialog'; import { ArmorTypes, D1ItemWithNormalStats, LockedPerkHash, PerkCombination } from './types'; export default function LoadoutBuilderLockPerk({ type, lockeditem, i18nItemNames, activePerks, lockedPerks, onRemove, onPerkLocked, onItemLocked, }: { type: ArmorTypes; lockeditem: D1ItemWithNormalStats | null; lockedPerks: { [armorType in ArmorTypes]: LockedPerkHash }; activePerks: PerkCombination; i18nItemNames: { [key in ArmorTypes]: string }; onRemove: ({ type }: { type: ArmorTypes }) => void; onPerkLocked: (perk: D1GridNode, type: ArmorTypes, $event: React.MouseEvent) => void; onItemLocked: (item: DimItem) => void; }) { const [dialogOpen, setDialogOpen] = useState(false); const closeDialog = () => setDialogOpen(false); const addPerkClicked = () => setDialogOpen(true); const doOnPerkLocked = (perk: D1GridNode, type: ArmorTypes, $event: React.MouseEvent) => { closeDialog(); onPerkLocked(perk, type, $event); }; const firstPerk = lockedPerks[type][parseInt(Object.keys(lockedPerks[type])[0], 10)]; const hasLockedPerks = Object.keys(lockedPerks[type]).length > 0; return ( <LoadoutBucketDropTarget className={styles.lockedItem} bucketHash={type} onItemLocked={onItemLocked} > {lockeditem ? ( <ClosableContainer onClose={() => onRemove({ type })}> <LoadoutBuilderItem item={lockeditem} /> </ClosableContainer> ) : ( <div className={styles.emptyItem} onClick={addPerkClicked}> {hasLockedPerks ? ( <BungieImage src={firstPerk.icon} title={firstPerk.description} /> ) : ( <div className={styles.lockPerkIcon}> <AppIcon icon={plusIcon} /> <small>{t('LB.LockPerk')}</small> </div> )} </div> )} <div>{i18nItemNames[type]}</div> {dialogOpen && ( <LoadoutBuilderLocksDialog activePerks={activePerks} lockedPerks={lockedPerks} type={type} onPerkLocked={doOnPerkLocked} onClose={closeDialog} /> )} </LoadoutBucketDropTarget> ); } ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderLocksDialog.m.scss ================================================ @use '../../variables' as *; $or-color: var(--theme-accent-primary); $and-color: #814bcf; .perkSelectPopup { left: 6px; top: 0; } .perk { width: calc(var(--item-size) + 20px); padding-left: 3px; padding-right: 3px; cursor: pointer; @include interactive($hover: true) { background-color: $or-color; } img { width: var(--item-size); height: var(--item-size); display: block; margin: auto; } small { text-align: center; display: block; } } .perkSelectBox { user-select: none; display: grid; grid-template-columns: repeat(5, auto); grid-template-rows: auto; position: absolute; left: 0; top: 0; overflow-y: auto; background-color: #656565; border: 2px solid #ddd; z-index: 3; & .or { background-color: $or-color; } &.shiftHeld .perk { @include interactive($hover: true) { background-color: $and-color; } } & .and { background-color: $and-color; } } ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderLocksDialog.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'and': string; 'or': string; 'perk': string; 'perkSelectBox': string; 'perkSelectPopup': string; 'shiftHeld': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-builder/LoadoutBuilderLocksDialog.tsx ================================================ import { useShiftHeld } from 'app/utils/hooks'; import clsx from 'clsx'; import React from 'react'; import BungieImage from '../../dim-ui/BungieImage'; import ClickOutside from '../../dim-ui/ClickOutside'; import { D1GridNode } from '../../inventory/item-types'; import * as styles from './LoadoutBuilderLocksDialog.m.scss'; import { ArmorTypes, LockedPerkHash, PerkCombination } from './types'; interface Props { activePerks: PerkCombination; lockedPerks: { [armorType in ArmorTypes]: LockedPerkHash }; type: ArmorTypes; onPerkLocked: (perk: D1GridNode, type: ArmorTypes, $event: React.MouseEvent) => void; onClose: () => void; } export default function LoadoutBuilderLocksDialog({ onClose, lockedPerks, type, activePerks, onPerkLocked, }: Props) { const shiftHeld = useShiftHeld(); return ( <ClickOutside className={styles.perkSelectPopup} onClickOutside={onClose}> <div className={clsx(styles.perkSelectBox, { [styles.shiftHeld]: shiftHeld })}> {activePerks[type].map((perk) => ( <div key={perk.hash} className={clsx( styles.perk, lockedPerks[type][perk.hash] ? lockedPerks[type][perk.hash].lockType === 'and' ? styles.and : styles.or : undefined, )} onClick={(e) => onPerkLocked(perk, type, e)} > <BungieImage src={perk.icon} title={perk.description} /> <small>{perk.name}</small> </div> ))} </div> </ClickOutside> ); } ================================================ FILE: src/app/destiny1/loadout-builder/calculate.ts ================================================ import { characterStatFromStatDef } from 'app/inventory/store/character-utils'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { sumBy } from 'app/utils/collections'; import { infoLog } from 'app/utils/log'; import { delay } from 'app/utils/promises'; import { BucketHashes } from 'data/d2/generated-enums'; import { D1Item } from '../../inventory/item-types'; import { D1ManifestDefinitions } from '../d1-definitions'; import { D1StatHashes } from '../d1-manifest-types'; import { ArmorSet, ArmorTypes, D1ItemWithNormalStats, ItemBucket, LockedPerkHash, SetType, } from './types'; import { calcArmorStats, genSetHash, getBestArmor, getBonusConfig } from './utils'; const TAG = 'loadout optimizer'; export async function getSetBucketsStep( defs: D1ManifestDefinitions, activeGuardianBucket: ItemBucket, vendorBucket: ItemBucket, lockeditems: { [armorType in ArmorTypes]: D1ItemWithNormalStats | null }, lockedperks: { [armorType in ArmorTypes]: LockedPerkHash }, excludeditems: D1Item[], scaleType: 'base' | 'scaled', includeVendors: boolean, fullMode: boolean, cancelToken: { cancelled: boolean }, ): Promise< | { allSetTiers: string[]; activesets: string; highestsets: { [setHash: number]: SetType }; } | undefined > { const bestArmor = getBestArmor( activeGuardianBucket, vendorBucket, lockeditems, excludeditems, lockedperks, scaleType, includeVendors, fullMode, ); const helms: { item: D1Item; bonusType: string; }[] = bestArmor[BucketHashes.Helmet] || []; const gauntlets: { item: D1Item; bonusType: string; }[] = bestArmor[BucketHashes.Gauntlets] || []; const chests: { item: D1Item; bonusType: string; }[] = bestArmor[BucketHashes.ChestArmor] || []; const legs: { item: D1Item; bonusType: string; }[] = bestArmor[BucketHashes.LegArmor] || []; const classItems: { item: D1Item; bonusType: string; }[] = bestArmor[BucketHashes.ClassArmor] || []; const ghosts: { item: D1Item; bonusType: string; }[] = bestArmor[BucketHashes.Ghost] || []; const artifacts: { item: D1Item; bonusType: string; }[] = bestArmor[D1BucketHashes.Artifact] || []; const setMap: { [setHash: string]: SetType } = {}; const tiersSet = new Set<string>(); const combos = helms.length * gauntlets.length * chests.length * legs.length * classItems.length * ghosts.length * artifacts.length; if (combos === 0) { return Promise.resolve({ allSetTiers: [], activesets: '', highestsets: {}, }); } let processedCount = 0; const intellect = characterStatFromStatDef(defs.Stat.get(D1StatHashes.Intellect), 0); const strength = characterStatFromStatDef(defs.Stat.get(D1StatHashes.Strength), 0); const discipline = characterStatFromStatDef(defs.Stat.get(D1StatHashes.Discipline), 0); for (const helm of helms) { for (const gauntlet of gauntlets) { for (const chest of chests) { for (const leg of legs) { for (const classItem of classItems) { for (const ghost of ghosts) { for (const artifact of artifacts) { const validSet = Number(helm.item.isExotic) + Number(gauntlet.item.isExotic) + Number(chest.item.isExotic) + Number(leg.item.isExotic) < 2; if (validSet) { const set: ArmorSet = { armor: { [BucketHashes.Helmet]: helm, [BucketHashes.Gauntlets]: gauntlet, [BucketHashes.ChestArmor]: chest, [BucketHashes.LegArmor]: leg, [BucketHashes.ClassArmor]: classItem, [D1BucketHashes.Artifact]: artifact, [BucketHashes.Ghost]: ghost, }, stats: { [D1StatHashes.Intellect]: { ...intellect }, [D1StatHashes.Discipline]: { ...discipline }, [D1StatHashes.Strength]: { ...strength }, }, setHash: '', includesVendorItems: false, }; const pieces = Object.values(set.armor); set.setHash = genSetHash(pieces); calcArmorStats(pieces, set.stats, scaleType); const tiersString = `${tierValue(set.stats[D1StatHashes.Intellect].value)}/${tierValue( set.stats[D1StatHashes.Discipline].value, )}/${tierValue(set.stats[D1StatHashes.Strength].value)}`; tiersSet.add(tiersString); // Build a map of all sets but only keep one copy of armor // so we reduce memory usage if (setMap[set.setHash]) { if (setMap[set.setHash].tiers[tiersString]) { setMap[set.setHash].tiers[tiersString].configs.push( getBonusConfig(set.armor), ); } else { setMap[set.setHash].tiers[tiersString] = { stats: set.stats, configs: [getBonusConfig(set.armor)], }; } } else { setMap[set.setHash] = { set, tiers: {} }; setMap[set.setHash].tiers[tiersString] = { stats: set.stats, configs: [getBonusConfig(set.armor)], }; } // no owner means this is a vendor item set.includesVendorItems = pieces.some((armor) => !armor.item.owner); } processedCount++; if (cancelToken.cancelled) { infoLog(TAG, 'cancelled processing'); return; } if (processedCount % 50000 === 0) { infoLog( 'loadout optimizer', processedCount, 'combinations processed, still going...', ); // Allow the event loop to do other things before we resume await delay(0); } } } } } } } } const tiers = Object.groupBy(tiersSet.keys(), (tierString: string) => sumBy(tierString.split('/'), (num) => parseInt(num, 10)), ); for (const tier of Object.values(tiers)) { tier.sort().reverse(); } const allSetTiers: string[] = []; const tierKeys = Object.keys(tiers); for (let t = tierKeys.length; t > tierKeys.length - 3; t--) { if (tierKeys[t]) { allSetTiers.push(`- Tier ${tierKeys[t]} -`); for (const set of tiers[tierKeys[t]]) { allSetTiers.push(set); } } } let activesets = ''; if (!allSetTiers.includes(activesets)) { activesets = allSetTiers[1]; } if (cancelToken.cancelled) { infoLog(TAG, 'cancelled processing'); return; } // Finish progress infoLog(TAG, 'processed', combos, 'combinations.'); return { allSetTiers, activesets, highestsets: setMap, }; // reset: lockedchanged, excludedchanged, perkschanged, hassets } function tierValue(value: number) { return Math.floor(Math.min(300, value) / 60); } ================================================ FILE: src/app/destiny1/loadout-builder/types.ts ================================================ import { DimCharacterStat } from 'app/inventory/store-types'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { D1GridNode, D1Item } from '../../inventory/item-types'; export interface D1ItemWithNormalStats extends D1Item { normalStats?: { [hash: number]: { statHash: number; base: number; scaled: number; bonus: number; split: number; qualityPercentage: number; }; }; vendorIcon?: string; } export type ArmorTypes = | BucketHashes.Helmet | BucketHashes.Gauntlets | BucketHashes.ChestArmor | BucketHashes.LegArmor | BucketHashes.ClassArmor | D1BucketHashes.Artifact | BucketHashes.Ghost; export type ClassTypes = DestinyClass.Titan | DestinyClass.Warlock | DestinyClass.Hunter; export interface ArmorSet { armor: { [armorType in ArmorTypes]: { item: D1ItemWithNormalStats; bonusType: string; }; }; stats: { [statHash: number]: DimCharacterStat; }; setHash: string; includesVendorItems: boolean; } export interface LockedPerk { icon: string; description: string; lockType: 'and' | 'or'; } export type ItemBucket = { [armorType in ArmorTypes]: D1ItemWithNormalStats[] }; export type PerkCombination = { [armorType in ArmorTypes]: D1GridNode[] }; export interface LockedPerkHash { [hash: number]: LockedPerk; } export interface SetType { set: ArmorSet; tiers: { [tierString: string]: { stats: ArmorSet['stats']; configs: { [armorType in ArmorTypes]: string }[]; }; }; } ================================================ FILE: src/app/destiny1/loadout-builder/utils.ts ================================================ import { D1_StatHashes, D1BucketHashes } from 'app/search/d1-known-values'; import { isEmpty, mapValues, uniqBy } from 'app/utils/collections'; import { itemCanBeEquippedBy } from 'app/utils/item-utils'; import { BucketHashes } from 'data/d2/generated-enums'; import { maxBy } from 'es-toolkit'; import { D1Item } from '../../inventory/item-types'; import { D1Store, DimStore } from '../../inventory/store-types'; import { D1StatHashes } from '../d1-manifest-types'; import { Vendor } from '../vendors/vendor.service'; import { ArmorSet, ArmorTypes, D1ItemWithNormalStats, ItemBucket, LockedPerkHash, SetType, } from './types'; export interface ItemWithBonus { item: D1ItemWithNormalStats; bonusType: string; } function getBonusType(armorpiece: D1ItemWithNormalStats): string { if (!armorpiece.normalStats) { return ''; } return ( (armorpiece.normalStats[D1StatHashes.Intellect].bonus > 0 ? 'int ' : '') + (armorpiece.normalStats[D1StatHashes.Discipline].bonus > 0 ? 'dis ' : '') + (armorpiece.normalStats[D1StatHashes.Strength].bonus > 0 ? 'str' : '') ); } function getBestItem( armor: D1ItemWithNormalStats[], stats: number[], type: string, scaleTypeArg: 'base' | 'scaled', nonExotic = false, ): ItemWithBonus { // for specific armor (Helmet), look at stats (int/dis), return best one. return { item: maxBy(armor, (o) => { if (nonExotic && o.isExotic) { return 0; } let bonus = 0; let total = 0; for (const stat of stats) { const scaleType = o.rarity === 'Rare' ? 'base' : scaleTypeArg; if (o.normalStats) { const normalStats = o.normalStats[stat]; total += normalStats[scaleType]; bonus = normalStats.bonus; } } return total + bonus; })!, bonusType: type, }; } export function calcArmorStats( pieces: ItemWithBonus[], stats: ArmorSet['stats'], scaleTypeArg: 'base' | 'scaled', ) { for (const armor of pieces) { const int = armor.item.normalStats![D1StatHashes.Intellect]; const dis = armor.item.normalStats![D1StatHashes.Discipline]; const str = armor.item.normalStats![D1StatHashes.Strength]; const scaleType = armor.item.rarity === 'Rare' ? 'base' : scaleTypeArg; // Mark of the Sunforged, Stormcaller Bond and Nightstalker cloak have special fixed stats // that do not scale correctly as the scaling is currently implemented. // See https://github.com/DestinyItemManager/DIM/issues/5191 for details if ([2820418554, 2122538507, 2300914892].includes(armor.item.hash)) { stats[D1StatHashes.Intellect].value += int.base; } else { stats[D1StatHashes.Intellect].value += int[scaleType]; stats[D1StatHashes.Discipline].value += dis[scaleType]; stats[D1StatHashes.Strength].value += str[scaleType]; } switch (armor.bonusType) { case 'int': stats[D1StatHashes.Intellect].value += int.bonus; break; case 'dis': stats[D1StatHashes.Discipline].value += dis.bonus; break; case 'str': stats[D1StatHashes.Strength].value += str.bonus; break; } } } export function getBonusConfig(armor: ArmorSet['armor']): { [armorType in ArmorTypes]: string } { return mapValues(armor, (armorPiece) => armorPiece.bonusType); } export function genSetHash(armorPieces: ItemWithBonus[]) { let hash = ''; for (const armorPiece of armorPieces) { hash += armorPiece.item.id; } return hash; } export function getBestArmor( bucket: ItemBucket, vendorBucket: ItemBucket, locked: { [armorType in ArmorTypes]: D1ItemWithNormalStats | null }, excluded: D1Item[], lockedPerks: { [armorType in ArmorTypes]: LockedPerkHash }, scaleTypeArg: 'base' | 'scaled', includeVendors = false, fullMode = false, ) { const statHashes = [ { stats: [D1StatHashes.Intellect, D1StatHashes.Discipline], type: 'intdis' }, { stats: [D1StatHashes.Intellect, D1StatHashes.Strength], type: 'intstr' }, { stats: [D1StatHashes.Discipline, D1StatHashes.Strength], type: 'disstr' }, { stats: [D1StatHashes.Intellect], type: 'int' }, { stats: [D1StatHashes.Discipline], type: 'dis' }, { stats: [D1StatHashes.Strength], type: 'str' }, ]; const armor: Partial<Record<ArmorTypes, ItemWithBonus[]>> = {}; let best: { item: D1ItemWithNormalStats; bonusType: string }[]; let curbest; let bestCombs: { item: D1ItemWithNormalStats; bonusType: string }[]; const excludedIndices = new Set(excluded.map((i) => i.index)); for (const armortypestr in bucket) { const armortype = parseInt(armortypestr, 10) as ArmorTypes; const combined = includeVendors ? bucket[armortype].concat(vendorBucket[armortype]) : bucket[armortype]; const lockedItem = locked[armortype]; if (lockedItem) { best = [{ item: lockedItem, bonusType: getBonusType(lockedItem) }]; } else { best = []; let hasPerks: (item: D1Item) => boolean = (_i) => true; if (!isEmpty(lockedPerks[armortype])) { const lockedPerkKeys = Object.keys(lockedPerks[armortype]).map((k) => parseInt(k, 10)); const andPerkHashes = lockedPerkKeys .filter((perkHash) => lockedPerks[armortype][perkHash].lockType === 'and') .map(Number); const orPerkHashes = lockedPerkKeys .filter((perkHash) => lockedPerks[armortype][perkHash].lockType === 'or') .map(Number); hasPerks = (item) => { if (!orPerkHashes.length && !andPerkHashes.length) { return true; } function matchNode(perkHash: number) { return item.talentGrid?.nodes.some((n) => n.hash === perkHash); } return Boolean( (orPerkHashes.length && orPerkHashes.some(matchNode)) || (andPerkHashes.length && andPerkHashes.every(matchNode)), ); }; } // Filter out excluded and non-wanted perks const filtered = combined.filter( (item) => !excludedIndices.has(item.index) && hasPerks(item), // Not excluded and has the correct locked perks ); if (filtered.length === 0) { continue; // No items in this bucket } for (const [index, hash] of statHashes.entries()) { if (!fullMode && index > 2) { continue; } curbest = getBestItem(filtered, hash.stats, hash.type, scaleTypeArg); best.push(curbest); // add the best -> if best is exotic -> get best legendary if (curbest.item.isExotic && armortype !== BucketHashes.ClassArmor) { best.push(getBestItem(filtered, hash.stats, hash.type, scaleTypeArg, true)); } } } bestCombs = []; for (const obj of uniqBy(best, (o) => o.item.index)) { obj.bonusType = getBonusType(obj.item); if (obj.bonusType === '') { bestCombs.push({ item: obj.item, bonusType: '' }); } if (obj.bonusType.includes('int')) { bestCombs.push({ item: obj.item, bonusType: 'int' }); } if (obj.bonusType.includes('dis')) { bestCombs.push({ item: obj.item, bonusType: 'dis' }); } if (obj.bonusType.includes('str')) { bestCombs.push({ item: obj.item, bonusType: 'str' }); } } armor[armortype] = bestCombs; } return armor; } export function getActiveHighestSets( setMap: { [setHash: number]: SetType }, activeSets: string, ): SetType[] { let count = 0; const topSets: SetType[] = []; for (const setType of Object.values(setMap)) { if (count >= 10) { continue; } if (setType.tiers[activeSets]) { topSets.push(setType); count += 1; } } return topSets; } export function mergeBuckets<T extends any[]>( bucket1: { [armorType in ArmorTypes]: T }, bucket2: { [armorType in ArmorTypes]: T }, ) { const merged: Partial<{ [armorType in ArmorTypes]: T }> = {}; for (const [type, bucket] of Object.entries(bucket1)) { merged[parseInt(type, 10) as ArmorTypes] = bucket.concat( bucket2[parseInt(type, 10) as ArmorTypes], ) as T; } return merged as { [armorType in ArmorTypes]: T }; } export function loadVendorsBucket( currentStore: DimStore, vendors?: { [vendorHash: number]: Vendor; }, ): ItemBucket { if (!vendors) { return { [BucketHashes.Helmet]: [], [BucketHashes.Gauntlets]: [], [BucketHashes.ChestArmor]: [], [BucketHashes.LegArmor]: [], [BucketHashes.ClassArmor]: [], [D1BucketHashes.Artifact]: [], [BucketHashes.Ghost]: [], }; } return Object.values(vendors) .map((vendor) => getBuckets( vendor.allItems .filter( (i) => i.item.stats && i.item.primaryStat?.statHash === D1_StatHashes.Defense && itemCanBeEquippedBy(i.item, currentStore), ) .map((i) => i.item), ), ) .reduce(mergeBuckets); } export function loadBucket(currentStore: DimStore, stores: D1Store[]): ItemBucket { return stores .map((store) => getBuckets( store.items.filter( (i) => i.stats && i.primaryStat?.statHash === D1_StatHashes.Defense && itemCanBeEquippedBy(i, currentStore), ), ), ) .reduce(mergeBuckets); } function getBuckets(items: D1Item[]): ItemBucket { return { [BucketHashes.Helmet]: items .filter((item) => item.bucket.hash === BucketHashes.Helmet) .map(normalizeStats), [BucketHashes.Gauntlets]: items .filter((item) => item.bucket.hash === BucketHashes.Gauntlets) .map(normalizeStats), [BucketHashes.ChestArmor]: items .filter((item) => item.bucket.hash === BucketHashes.ChestArmor) .map(normalizeStats), [BucketHashes.LegArmor]: items .filter((item) => item.bucket.hash === BucketHashes.LegArmor) .map(normalizeStats), [BucketHashes.ClassArmor]: items .filter((item) => item.bucket.hash === BucketHashes.ClassArmor) .map(normalizeStats), [D1BucketHashes.Artifact]: items .filter((item) => item.bucket.hash === D1BucketHashes.Artifact) .map(normalizeStats), [BucketHashes.Ghost]: items .filter((item) => item.bucket.hash === BucketHashes.Ghost) .map(normalizeStats), }; } function normalizeStats(item: D1ItemWithNormalStats) { item.normalStats = {}; if (item.stats) { for (const stat of item.stats) { item.normalStats[stat.statHash] = { statHash: stat.statHash, base: stat.base, scaled: stat.scaled ? stat.scaled.min : 0, bonus: stat.bonus, split: stat.split || 0, qualityPercentage: stat.qualityPercentage ? stat.qualityPercentage.min : 0, }; } } return item; } ================================================ FILE: src/app/destiny1/loadout-drawer/Buttons.m.scss ================================================ .add { composes: pullItemButton from '../../inventory-page/StoreBucket.m.scss'; align-items: center; box-sizing: border-box; display: flex; height: calc((var(--item-size) + ((var(--item-size) / 5) + 4px) - 1px)); justify-content: center; margin: 0; text-decoration: none; width: var(--item-size); } ================================================ FILE: src/app/destiny1/loadout-drawer/Buttons.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'add': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-drawer/Buttons.tsx ================================================ import { addIcon, AppIcon } from 'app/shell/icons'; import clsx from 'clsx'; import * as styles from './Buttons.m.scss'; interface AddButtonProps { /** An additional className to be passed to the component. */ className?: string; onClick: () => void; } /** * A button with a plus icon that is intended to sit next to items/mods. */ export function AddButton({ className, onClick }: AddButtonProps) { return ( <a className={clsx(styles.add, className)} onClick={onClick}> <AppIcon icon={addIcon} /> </a> ); } ================================================ FILE: src/app/destiny1/loadout-drawer/D1LoadoutDrawer.m.scss ================================================ @use '../../variables.scss' as *; .warnItems { composes: flexWrap from '../../dim-ui/common.m.scss'; gap: 4px; } .contents { composes: flexColumn from '../../dim-ui/common.m.scss'; margin-top: 8px; padding: 0 10px; } ================================================ FILE: src/app/destiny1/loadout-drawer/D1LoadoutDrawer.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'contents': string; 'warnItems': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-drawer/D1LoadoutDrawer.tsx ================================================ import { AlertIcon } from 'app/dim-ui/AlertIcon'; import ClosableContainer from 'app/dim-ui/ClosableContainer'; import Sheet from 'app/dim-ui/Sheet'; import { t } from 'app/i18next-t'; import ItemIcon from 'app/inventory/ItemIcon'; import { DimItem } from 'app/inventory/item-types'; import { allItemsSelector, createItemContextSelector } from 'app/inventory/selectors'; import { useItemPicker } from 'app/item-picker/item-picker'; import LoadoutDrawerDropTarget from 'app/loadout-drawer/LoadoutDrawerDropTarget'; import LoadoutDrawerFooter from 'app/loadout-drawer/LoadoutDrawerFooter'; import { addItem, removeItem, setNotes, toggleEquipped, } from 'app/loadout-drawer/loadout-drawer-reducer'; import { addItem$ } from 'app/loadout-drawer/loadout-events'; import { getItemsFromLoadoutItems } from 'app/loadout-drawer/loadout-item-conversion'; import { deleteLoadout, updateLoadout } from 'app/loadout/actions'; import { Loadout, ResolvedLoadoutItem } from 'app/loadout/loadout-types'; import { useD1Definitions } from 'app/manifest/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { useEventBusListener } from 'app/utils/hooks'; import { isItemLoadoutCompatible, itemCanBeInLoadout } from 'app/utils/item-utils'; import React, { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import TextareaAutosize from 'react-textarea-autosize'; import * as styles from './D1LoadoutDrawer.m.scss'; import LoadoutDrawerContents from './LoadoutDrawerContents'; import LoadoutDrawerOptions from './LoadoutDrawerOptions'; /** * The Loadout editor that shows up as a sheet on the Inventory screen. You can build and edit * loadouts from this interface. This one is only used for D1, see LoadoutDrawer2 for D2's new loadout editor. */ export default function D1LoadoutDrawer({ initialLoadout, storeId, showClass, onClose, }: { initialLoadout: Loadout; /** * The store that provides context to how this loadout is being edited from. * The store this edit session was launched from. This is to help pick which * mods are enabled, which subclass items to show, etc. */ storeId: string; showClass: boolean; onClose: () => void; }) { const dispatch = useThunkDispatch(); const [loadout, setLoadout] = useState(initialLoadout); const onSaveLoadout = ( e: React.FormEvent, loadoutToSave: Readonly<Loadout> | undefined = loadout, close: () => void, ) => { e.preventDefault(); if (!loadoutToSave) { return; } if (loadoutToSave.name === t('Loadouts.FromEquipped')) { loadoutToSave = { ...loadoutToSave, name: `${loadoutToSave.name} ${new Date().toLocaleString()}`, }; } dispatch(updateLoadout(loadoutToSave)); close(); }; const saveAsNew = (e: React.FormEvent, close: () => void) => { e.preventDefault(); if (!loadout) { return; } const newLoadout = { ...loadout, id: globalThis.crypto.randomUUID(), // Let it be a new ID }; onSaveLoadout(e, newLoadout, close); }; if (!loadout) { return null; } const onDeleteLoadout = (onClose: () => void) => { dispatch(deleteLoadout(loadout.id)); onClose(); }; const handleNotesChanged: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => setLoadout(setNotes(e.target.value)); const header = ( <div> <LoadoutDrawerOptions loadout={loadout} showClass={showClass} setLoadout={setLoadout} /> {loadout.notes !== undefined && ( <TextareaAutosize onChange={handleNotesChanged} value={loadout.notes} placeholder={t('Loadouts.NotesPlaceholder')} maxLength={2048} /> )} </div> ); const footer = ({ onClose }: { onClose: () => void }) => ( <LoadoutDrawerFooter loadout={loadout} onSaveLoadout={(e, isNew) => isNew ? saveAsNew(e, onClose) : onSaveLoadout(e, loadout, onClose) } onDeleteLoadout={() => onDeleteLoadout(onClose)} /> ); return ( <Sheet onClose={onClose} header={header} footer={footer}> <LoadoutDrawerBody loadout={loadout} storeId={storeId} setLoadout={setLoadout} /> </Sheet> ); } // This is mostly separated out so that its use of useItemPicker is under the Sheet in the component tree. function LoadoutDrawerBody({ loadout, storeId, setLoadout, }: { loadout: Loadout; storeId: string; setLoadout: Dispatch<SetStateAction<Loadout>>; }) { const defs = useD1Definitions()!; const showItemPicker = useItemPicker(); const allItems = useSelector(allItemsSelector); const itemCreationContext = useSelector(createItemContextSelector); const loadoutItems = loadout?.items; // Turn loadout items into real DimItems const [items, warnitems] = useMemo( () => getItemsFromLoadoutItems( itemCreationContext, loadoutItems, storeId, allItems, undefined, defs, ), [itemCreationContext, loadoutItems, storeId, allItems, defs], ); const onAddItem = useCallback( (item: DimItem, equip?: boolean) => setLoadout(addItem(defs, item, equip)), [defs, setLoadout], ); // If an item comes in on the addItem$ observable, add it. useEventBusListener(addItem$, onAddItem); const onRemoveItem = (resolvedItem: ResolvedLoadoutItem, e?: React.MouseEvent) => { e?.stopPropagation(); setLoadout(removeItem(defs, resolvedItem)); }; const handleToggleEquipped = (item: ResolvedLoadoutItem) => setLoadout(toggleEquipped(defs, item)); /** Prompt the user to select a replacement for a missing item. */ const fixWarnItem = async (li: ResolvedLoadoutItem) => { const warnItem = li.item; const item = await showItemPicker({ filterItems: (item: DimItem) => item.hash === warnItem.hash && itemCanBeInLoadout(item) && (!loadout || isItemLoadoutCompatible(item.classType, loadout.classType)), prompt: t('Loadouts.FindAnother', { name: warnItem.name }), }); if (item) { onAddItem(item); onRemoveItem(li); } }; return ( <LoadoutDrawerDropTarget onDroppedItem={onAddItem} classType={loadout.classType}> {warnitems.length > 0 && ( <div className={styles.contents}> <p> <AlertIcon /> {t('Loadouts.VendorsCannotEquip')} </p> <div className={styles.warnItems}> {warnitems.map((li) => ( <ClosableContainer key={li.item.id} onClose={(e) => onRemoveItem(li, e)}> <div onClick={() => fixWarnItem(li)}> <ItemIcon item={li.item} /> </div> </ClosableContainer> ))} </div> </div> )} <div className={styles.contents}> <LoadoutDrawerContents storeId={storeId} loadout={loadout} items={items} equip={handleToggleEquipped} remove={onRemoveItem} add={onAddItem} setLoadout={setLoadout} /> </div> </LoadoutDrawerDropTarget> ); } ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerBucket.m.scss ================================================ @use '../../variables.scss' as *; .equippedAddButton { width: var(--item-size); } // Use flex instead of grid so our flex-in-flex layout works. .itemGrid { display: flex; flex-flow: row wrap; min-height: var(--item-size); gap: var(--item-margin); align-content: flex-start; align-items: flex-start; padding-top: 4px; flex: 1; &.equipped { margin-right: var(--item-margin); flex: 0; } } .loadoutBucket { width: 100%; position: relative; display: flex; box-sizing: border-box; flex-direction: column; flex: 0; @include phone-portrait { margin-right: 0; } } .loadoutBucketName { text-transform: uppercase; font-size: 13px; } .items { display: flex; flex-direction: row; align-items: flex-start; /* prettier-ignore */ max-width: calc( #{$equipped-item-total-outset} + var(--character-column-width) ); // ↑ fit 1 equipped + 3-5 items /* prettier-ignore */ min-width: calc( #{$equipped-item-total-outset} + 2 * (var(--item-size) + var(--item-margin)) ); // ↑ fit at least 1 equipped + 1 unequipped item width: max-content; @include phone-portrait { max-width: 100%; width: 100%; } } ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerBucket.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'equipped': string; 'equippedAddButton': string; 'itemGrid': string; 'items': string; 'loadoutBucket': string; 'loadoutBucketName': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerBucket.tsx ================================================ import 'app/inventory-page/StoreBucket.scss'; import { InventoryBucket } from 'app/inventory/inventory-buckets'; import { ResolvedLoadoutItem } from 'app/loadout/loadout-types'; import clsx from 'clsx'; import { BucketHashes } from 'data/d2/generated-enums'; import { partition } from 'es-toolkit'; import React from 'react'; import { AddButton } from './Buttons'; import * as styles from './LoadoutDrawerBucket.m.scss'; import LoadoutDrawerItem from './LoadoutDrawerItem'; export default function LoadoutDrawerBucket({ bucket, items, pickLoadoutItem, equip, remove, }: { bucket: InventoryBucket; items: ResolvedLoadoutItem[]; pickLoadoutItem: (bucket: InventoryBucket) => void; equip: (resolvedItem: ResolvedLoadoutItem, e: React.MouseEvent) => void; remove: (resolvedItem: ResolvedLoadoutItem, e: React.MouseEvent) => void; }) { // This is never called with an empty items array if (!items.length) { return null; } const [equippedItems, unequippedItems] = partition(items, (li) => li.loadoutItem.equip); // Only allow one emblem const capacity = bucket.hash === BucketHashes.Emblems ? 1 : bucket.capacity; const mapItem = (li: ResolvedLoadoutItem) => ( <LoadoutDrawerItem key={li.item.index} resolvedLoadoutItem={li} equip={equip} remove={remove} /> ); return ( <div className={styles.loadoutBucket}> <div className={styles.loadoutBucketName}>{bucket.name}</div> <div className={styles.items}> <div className={clsx(styles.equipped, styles.itemGrid)}> <div className="equipped-item"> {equippedItems.length > 0 ? ( equippedItems.map(mapItem) ) : ( <AddButton className={styles.equippedAddButton} onClick={() => pickLoadoutItem(bucket)} /> )} </div> </div> {(equippedItems.length > 0 || unequippedItems.length > 0) && bucket.hash !== BucketHashes.Subclass && ( <div className={styles.itemGrid}> {unequippedItems.map(mapItem)} {equippedItems.length > 0 && unequippedItems.length < capacity - 1 && ( <AddButton onClick={() => pickLoadoutItem(bucket)} /> )} </div> )} </div> </div> ); } ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerContents.m.scss ================================================ @use '../../variables.scss' as *; .addTypes { composes: flexWrap from '../../dim-ui/common.m.scss'; gap: 4px; } .addedItems { composes: flexWrap from '../../dim-ui/common.m.scss'; min-height: calc(var(--item-size) + 4px); margin-top: 4px; margin-bottom: 1em; gap: 16px; @include phone-portrait { flex-flow: column nowrap; } } ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerContents.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'addTypes': string; 'addedItems': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerContents.tsx ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import type { InventoryBucket } from 'app/inventory/inventory-buckets'; import { DimItem } from 'app/inventory/item-types'; import { bucketsSelector, storesSelector } from 'app/inventory/selectors'; import { getStore } from 'app/inventory/stores-helpers'; import { ShowItemPickerFn, useItemPicker } from 'app/item-picker/item-picker'; import { LoadoutUpdateFunction, fillLoadoutFromEquipped, fillLoadoutFromUnequipped, } from 'app/loadout-drawer/loadout-drawer-reducer'; import { findSameLoadoutItemIndex, fromEquippedTypes } from 'app/loadout-drawer/loadout-utils'; import { Loadout, ResolvedLoadoutItem } from 'app/loadout/loadout-types'; import { useD1Definitions } from 'app/manifest/selectors'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { AppIcon, addIcon } from 'app/shell/icons'; import { filterMap } from 'app/utils/collections'; import { isItemLoadoutCompatible, itemCanBeInLoadout } from 'app/utils/item-utils'; import { BucketHashes } from 'data/d2/generated-enums'; import { partition } from 'es-toolkit'; import React from 'react'; import { useSelector } from 'react-redux'; import { D1ManifestDefinitions } from '../d1-definitions'; import LoadoutDrawerBucket from './LoadoutDrawerBucket'; import * as styles from './LoadoutDrawerContents.m.scss'; const loadoutTypes: (BucketHashes | D1BucketHashes)[] = [ BucketHashes.Subclass, BucketHashes.KineticWeapons, BucketHashes.EnergyWeapons, BucketHashes.PowerWeapons, BucketHashes.Helmet, BucketHashes.Gauntlets, BucketHashes.ChestArmor, BucketHashes.LegArmor, BucketHashes.ClassArmor, D1BucketHashes.Artifact, BucketHashes.Ghost, BucketHashes.Consumables, BucketHashes.Materials, BucketHashes.Emblems, D1BucketHashes.Shader, D1BucketHashes.D1Emotes, BucketHashes.Ships, BucketHashes.Vehicle, D1BucketHashes.Horn, ]; export default function LoadoutDrawerContents({ storeId, loadout, items, equip, remove, add, setLoadout, }: { storeId: string; loadout: Loadout; items: ResolvedLoadoutItem[]; setLoadout: (updater: LoadoutUpdateFunction) => void; equip: (resolvedItem: ResolvedLoadoutItem, e: React.MouseEvent) => void; remove: (resolvedItem: ResolvedLoadoutItem, e: React.MouseEvent) => void; add: (item: DimItem, equip?: boolean) => void; }) { const defs = useD1Definitions()!; const buckets = useSelector(bucketsSelector)!; const stores = useSelector(storesSelector); // The store to use for "fill from equipped/unequipped" const dimStore = getStore(stores, storeId)!; const doFillLoadoutFromEquipped = () => setLoadout(fillLoadoutFromEquipped(defs, dimStore, undefined)); const doFillLoadOutFromUnequipped = () => setLoadout(fillLoadoutFromUnequipped(defs, dimStore)); const availableTypes = filterMap(loadoutTypes, (h) => buckets.byHash[h]); const itemsByBucket = Object.groupBy(items, (li) => li.item.bucket.hash); const [typesWithItems, typesWithoutItems] = partition(availableTypes, (bucket) => Boolean(bucket.hash && itemsByBucket[bucket.hash]?.length), ); const showFillFromEquipped = typesWithoutItems.some((b) => fromEquippedTypes.includes(b.hash)); const showItemPicker = useItemPicker(); return ( <> <div className={styles.addTypes}> {showFillFromEquipped && ( <button type="button" className="dim-button" onClick={doFillLoadoutFromEquipped}> <AppIcon icon={addIcon} /> {t('Loadouts.AddEquippedItems')} </button> )} <button type="button" className="dim-button" onClick={doFillLoadOutFromUnequipped}> <AppIcon icon={addIcon} /> {t('Loadouts.AddUnequippedItems')} </button> {typesWithoutItems.length > 0 && typesWithoutItems.map((bucket) => ( <a key={bucket.hash} onClick={() => pickLoadoutItem(defs, loadout, bucket, add, showItemPicker)} className="dim-button" > <AppIcon icon={addIcon} /> {bucket.name} </a> ))} </div> <div className={styles.addedItems}> {typesWithItems.map((bucket) => ( <LoadoutDrawerBucket key={bucket.hash} bucket={bucket} items={itemsByBucket[bucket.hash] || []} pickLoadoutItem={(bucket) => pickLoadoutItem(defs, loadout, bucket, add, showItemPicker) } equip={equip} remove={remove} /> ))} </div> </> ); } async function pickLoadoutItem( defs: D1ManifestDefinitions | D2ManifestDefinitions, loadout: Loadout, bucket: InventoryBucket, add: (item: DimItem) => void, showItemPicker: ShowItemPickerFn, ) { const loadoutHasItem = (item: DimItem) => findSameLoadoutItemIndex(defs, loadout.items, item) !== -1; const item = await showItemPicker({ filterItems: (item: DimItem) => item.bucket.hash === bucket.hash && isItemLoadoutCompatible(item.classType, loadout.classType) && itemCanBeInLoadout(item) && !loadoutHasItem(item), prompt: t('Loadouts.ChooseItem', { name: bucket.name }), }); if (item) { add(item); } } ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerItem.m.scss ================================================ @use '../../variables.scss' as *; .classIcon { width: calc(var(--item-size) / 4) !important; height: calc(var(--item-size) / 4) !important; color: white; position: absolute; left: 3px; bottom: 3px; filter: drop-shadow(0 0 2px black); } ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerItem.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'classIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerItem.tsx ================================================ import ClassIcon from 'app/dim-ui/ClassIcon'; import ClosableContainer from 'app/dim-ui/ClosableContainer'; import ConnectedInventoryItem from 'app/inventory/ConnectedInventoryItem'; import { ResolvedLoadoutItem } from 'app/loadout/loadout-types'; import { BucketHashes } from 'data/d2/generated-enums'; import React from 'react'; import * as styles from './LoadoutDrawerItem.m.scss'; export default function LoadoutDrawerItem({ resolvedLoadoutItem, equip, remove, }: { resolvedLoadoutItem: ResolvedLoadoutItem; equip: (resolvedItem: ResolvedLoadoutItem, e: React.MouseEvent) => void; remove: (resolvedItem: ResolvedLoadoutItem, e: React.MouseEvent) => void; }) { const onClose = (e: React.MouseEvent) => { e.stopPropagation(); remove(resolvedLoadoutItem, e); }; const { item } = resolvedLoadoutItem; return ( <ClosableContainer onClose={onClose}> <ConnectedInventoryItem item={item} onClick={(e) => equip(resolvedLoadoutItem, e)} /> {item.bucket.hash === BucketHashes.Subclass && ( <ClassIcon classType={item.classType} className={styles.classIcon} /> )} </ClosableContainer> ); } ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerOptions.m.scss ================================================ @use '../../variables.scss' as *; .loadoutOptions { display: flex; flex-flow: row wrap; @include phone-portrait { font-size: 14px; } input { vertical-align: middle; } } .inputGroup { display: flex; flex-flow: row nowrap; align-items: center; margin-right: 4px; margin-bottom: 4px; gap: 4px; &:last-child { margin-bottom: 0; } } .loadoutName { width: 26em; } .dimInput { padding-left: 5px; height: 23px; background: rgb(46, 46, 46); border: none; border-bottom: 1px solid #555; color: var(--theme-text); width: 100%; @include phone-portrait { font-size: 14px; height: 32px; } @include interactive($hover: true, $focus: true) { background: black; outline: none; border-bottom: 1px solid var(--theme-accent-primary); } } ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerOptions.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'dimInput': string; 'inputGroup': string; 'loadoutName': string; 'loadoutOptions': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/loadout-drawer/LoadoutDrawerOptions.tsx ================================================ import { t } from 'app/i18next-t'; import { storesSelector } from 'app/inventory/selectors'; import { LoadoutUpdateFunction, setClassType, setClearSpace, setName, setNotes, } from 'app/loadout-drawer/loadout-drawer-reducer'; import { Loadout } from 'app/loadout/loadout-types'; import { uniqBy } from 'app/utils/collections'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import React from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import * as styles from './LoadoutDrawerOptions.m.scss'; const classTypeOptionsSelector = createSelector(storesSelector, (stores) => { const classTypeValues: { label: string; value: DestinyClass; }[] = uniqBy( stores.filter((s) => !s.isVault), (store) => store.classType, ).map((store) => ({ label: store.className, value: store.classType })); return [{ label: t('Loadouts.Any'), value: DestinyClass.Unknown }, ...classTypeValues]; }); export default function LoadoutDrawerOptions({ loadout, showClass, setLoadout, }: { loadout: Readonly<Loadout>; showClass: boolean; setLoadout: (updater: LoadoutUpdateFunction) => void; }) { const classTypeOptions = useSelector(classTypeOptionsSelector); const handleSetName = (e: React.ChangeEvent<HTMLInputElement>) => setLoadout(setName(e.target.value)); const handleSetClassType = (e: React.ChangeEvent<HTMLSelectElement>) => setLoadout(setClassType(parseInt(e.target.value, 10))); const handleSetClearSpace = ( e: React.ChangeEvent<HTMLInputElement>, category: 'Weapons' | 'Armor', ) => setLoadout(setClearSpace(e.target.checked, category)); const addNotes = () => setLoadout(setNotes('')); return ( <div className={styles.loadoutOptions}> <div className={clsx(styles.inputGroup, styles.loadoutName)}> <input className={styles.dimInput} name="name" onChange={handleSetName} minLength={1} maxLength={50} required={true} type="text" value={loadout.name} placeholder={t('Loadouts.LoadoutName')} /> {showClass && ( <select name="classType" onChange={handleSetClassType} value={loadout.classType}> {classTypeOptions.map((option) => ( <option key={option.value} value={option.value}> {option.label} </option> ))} </select> )} </div> {loadout.notes === undefined && ( <div className={styles.inputGroup}> <button className="dim-button" onClick={addNotes} type="button" title={t('Loadouts.AddNotes')} > {t('Loadouts.AddNotes')} </button> </div> )} <div className={styles.inputGroup}> <label> <input type="checkbox" checked={Boolean(loadout.parameters?.clearWeapons)} onChange={(e) => handleSetClearSpace(e, 'Weapons')} />{' '} {t('Loadouts.ClearSpaceWeapons')} </label> <label> <input type="checkbox" checked={Boolean(loadout.parameters?.clearArmor)} onChange={(e) => handleSetClearSpace(e, 'Armor')} />{' '} {t('Loadouts.ClearSpaceArmor')} </label> </div> </div> ); } ================================================ FILE: src/app/destiny1/record-books/RecordBooks.m.scss ================================================ @use '../../variables.scss' as *; .recordBooks { composes: dim-page from global; h1 { display: flex; align-items: flex-end; flex-direction: row; span { flex: 1; } } } // The controls to toggle hiding completed items .hideCompleted { margin: 1em var(--inventory-column-padding); } .bookIcon { height: 26px; } .recordBook { margin-bottom: 10px; } .recordBookPage { background: #555; margin: 8px 0 0 0; padding: 8px; p { margin: 0; color: #e0e0e0; } } .records { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px; margin-top: 8px; @include phone-portrait { grid-template-columns: 1fr; } } .record { composes: flexRow from '../../dim-ui/common.m.scss'; background: #333; padding: 8px; h3 { margin: 0; font-size: 14px; letter-spacing: 1px; font-weight: normal; } p { margin: 0 0 8px 0; color: #e0e0e0; } } .recordIcon { border-radius: 50%; background-color: rgb(245, 245, 245, 0.1); background-position: 50%; background-repeat: no-repeat; background-size: 75%; width: 40px; height: 40px; flex-shrink: 0; border: 2px solid #fff; .complete & { border-color: #ffce1f; background-color: rgb(255, 206, 31, 0.4); } } .recordInfo { flex: 1; margin-left: 10px; } ================================================ FILE: src/app/destiny1/record-books/RecordBooks.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bookIcon': string; 'complete': string; 'hideCompleted': string; 'record': string; 'recordBook': string; 'recordBookPage': string; 'recordBooks': string; 'recordIcon': string; 'recordInfo': string; 'records': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/record-books/RecordBooks.tsx ================================================ import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import Switch from 'app/dim-ui/Switch'; import { t } from 'app/i18next-t'; import { useLoadStores } from 'app/inventory/store/hooks'; import { useD1Definitions } from 'app/manifest/selectors'; import { useSetting } from 'app/settings/hooks'; import { count, sumBy } from 'app/utils/collections'; import { chainComparator, compareBy } from 'app/utils/comparators'; import { usePageTitle } from 'app/utils/hooks'; import clsx from 'clsx'; import { keyBy } from 'es-toolkit'; import { useSelector } from 'react-redux'; import { DestinyAccount } from '../../accounts/destiny-account'; import BungieImage, { bungieBackgroundStyle } from '../../dim-ui/BungieImage'; import CollapsibleTitle from '../../dim-ui/CollapsibleTitle'; import { storesSelector } from '../../inventory/selectors'; import { D1Store } from '../../inventory/store-types'; import Objective from '../../progress/Objective'; import { D1ManifestDefinitions } from '../d1-definitions'; import { D1ObjectiveProgress, D1RecordBook, D1RecordComponent } from '../d1-manifest-types'; import * as styles from './RecordBooks.m.scss'; interface RecordBook { hash: number; name: string; recordCount: number; completedCount: number; icon: string; banner: string; startDate: string; expirationDate: string; pages: RecordBookPage[]; complete: boolean; percentComplete?: number; } interface RecordBookPage { id: string; name: string; description: string; rewardsPage: boolean; records: { hash: number; complete: boolean; icon: string; name: string; description: string; objectives: D1ObjectiveProgress[]; }[]; complete: boolean; completedCount: number; } export default function RecordBooks({ account }: { account: DestinyAccount }) { usePageTitle(t('RecordBooks.RecordBooks')); const defs = useD1Definitions(); const stores = useSelector(storesSelector) as D1Store[]; const [hideCompletedRecords, setHideCompletedRecords] = useSetting('hideCompletedRecords'); const storesLoaded = useLoadStores(account); if (!defs || !storesLoaded) { return <ShowPageLoading message={t('Loading.Profile')} />; } const processRecordBook = ( defs: D1ManifestDefinitions, rawRecordBook: D1RecordBook, ): RecordBook => { const recordBookDef = defs.RecordBook.get(rawRecordBook.bookHash); const recordBook = { hash: rawRecordBook.bookHash, name: recordBookDef.displayName, recordCount: recordBookDef.recordCount, completedCount: rawRecordBook.completedCount, icon: recordBookDef.icon, banner: recordBookDef.bannerImage, startDate: rawRecordBook.startDate, expirationDate: rawRecordBook.expirationDate, pages: [] as RecordBookPage[], complete: false, percentComplete: undefined as number | undefined, }; const processRecord = (defs: D1ManifestDefinitions, record: D1RecordComponent) => { const recordDef = defs.Record.get(record.recordHash); return { hash: record.recordHash, icon: recordDef.icon, description: recordDef.description, name: recordDef.displayName, objectives: record.objectives, complete: record.objectives.every((o) => o.isComplete), }; }; const records = Object.values(rawRecordBook.records).map((r) => processRecord(defs, r)); const recordByHash = keyBy(records, (r) => r.hash); let i = 0; recordBook.pages = recordBookDef.pages.map((page) => { const createdPage: RecordBookPage = { id: `${recordBook.hash}-${i++}`, name: page.displayName, description: page.displayDescription, rewardsPage: page.displayStyle === 1, records: page.records.map((r) => recordByHash[r.recordHash]), complete: false, completedCount: 0, }; createdPage.complete = createdPage.records.every((r) => r.complete); createdPage.completedCount = count(createdPage.records, (r) => r.complete); return createdPage; }); if (rawRecordBook.progression) { rawRecordBook.progression = { ...rawRecordBook.progression, ...defs.Progression.get(rawRecordBook.progression.progressionHash), }; rawRecordBook.progress = rawRecordBook.progression; rawRecordBook.percentComplete = rawRecordBook.progress.currentProgress / sumBy(rawRecordBook.progress.steps, (s) => s.progressTotal); } else { recordBook.percentComplete = count(records, (r) => r.complete) / records.length; } recordBook.complete = recordBook.pages.every((p) => p.complete); return recordBook; }; const rawRecordBooks = stores[0].advisors.recordBooks; const recordBooks = Object.values(rawRecordBooks ?? {}) .map((rb) => processRecordBook(defs, rb)) .sort( chainComparator( compareBy((rb) => rb.complete), compareBy((rb) => new Date(rb.startDate).getTime()), ), ); return ( <div className={styles.recordBooks}> <div className={styles.hideCompleted}> <label> <Switch checked={hideCompletedRecords} onChange={setHideCompletedRecords} name="hideCompleted" /> <span>{t('RecordBooks.HideCompleted')}</span> </label> </div> {recordBooks.map((book) => ( <CollapsibleTitle key={book.hash} sectionId={`rb-${book.hash}`} title={ <> <BungieImage src={book.icon} className={styles.bookIcon} /> {book.name} </> } extra={ <> {book.completedCount} / {book.recordCount} </> } > <div className={styles.recordBook}> {book.pages.map( (page) => !page.rewardsPage && !(page.complete && hideCompletedRecords) && ( <div key={page.id} className={styles.recordBookPage}> <CollapsibleTitle sectionId={`rbpage-${page.id}`} title={page.name} extra={ <> {page.completedCount} / {page.records.length} </> } > <p>{page.description}</p> {page.records.length > 0 && ( <div className={styles.records}> {page.records.map( (record) => !(record.complete && hideCompletedRecords) && ( <div key={record.hash} className={clsx(styles.record, { [styles.complete]: record.complete, })} > <div className={styles.recordIcon} style={bungieBackgroundStyle(record.icon)} /> <div className={styles.recordInfo}> <h3>{record.name}</h3> <p>{record.description}</p> {record.objectives.map((objective) => ( <Objective key={objective.objectiveHash} objective={objective} /> ))} </div> </div> ), )} </div> )} </CollapsibleTitle> </div> ), )} </div> </CollapsibleTitle> ))} </div> ); } ================================================ FILE: src/app/destiny1/vendors/D1Vendor.tsx ================================================ import { VendorIcon, VendorLocation } from 'app/vendors/Vendor'; import CollapsibleTitle from '../../dim-ui/CollapsibleTitle'; import Countdown from '../../dim-ui/Countdown'; import D1VendorItems from './D1VendorItems'; import { Vendor } from './vendor.service'; /** * An individual Vendor in the "all vendors" page. Use SingleVendor for a page that only has one vendor on it. */ export default function D1Vendor({ vendor, totalCoins, }: { vendor: Vendor; totalCoins: { [currencyHash: number]: number; }; }) { return ( <div> <CollapsibleTitle title={ <> <VendorIcon src={vendor.icon} /> <span>{vendor.name}</span> <VendorLocation>{vendor.location}</VendorLocation> </> } extra={<Countdown endTime={new Date(vendor.nextRefreshDate)} />} sectionId={`d1vendor-${vendor.hash}`} > <D1VendorItems vendor={vendor} totalCoins={totalCoins} /> </CollapsibleTitle> </div> ); } ================================================ FILE: src/app/destiny1/vendors/D1VendorItem.m.scss ================================================ .notEnough { color: #cc5c58; } .currency img { height: 12px; width: 12px; margin-left: 4px; vertical-align: text-bottom; } .cost { display: flex; flex-direction: row; align-items: center; justify-content: flex-end; font-size: 10px; margin-top: 1px; } ================================================ FILE: src/app/destiny1/vendors/D1VendorItem.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'cost': string; 'currency': string; 'notEnough': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/vendors/D1VendorItem.tsx ================================================ import clsx from 'clsx'; import BungieImage from '../../dim-ui/BungieImage'; import { VendorItemDisplay } from '../../vendors/VendorItemComponent'; import * as styles from './D1VendorItem.m.scss'; import { VendorCost, VendorSaleItem } from './vendor.service'; interface Props { saleItem: VendorSaleItem; owned: boolean; totalCoins: { [currencyHash: number]: number; }; } export default function D1VendorItem({ saleItem, owned, totalCoins }: Props) { return ( <VendorItemDisplay item={saleItem.item} owned={owned} unavailable={!saleItem.unlocked} extraData={{ failureStrings: [saleItem.failureStrings] }} > {saleItem.costs.length > 0 && ( <div> {saleItem.costs.map((cost) => ( <D1VendorItemCost key={cost.currency.itemHash} cost={cost} totalCoins={totalCoins} /> ))} </div> )} </VendorItemDisplay> ); } function D1VendorItemCost({ cost, totalCoins, }: { cost: VendorCost; totalCoins: { [currencyHash: number]: number; }; }) { return ( <div className={clsx(styles.cost, { [styles.notEnough]: totalCoins[cost.currency.itemHash] < cost.value, })} > {cost.value}{' '} <span className={styles.currency}> <BungieImage src={cost.currency.icon} title={cost.currency.itemName} /> </span> </div> ); } ================================================ FILE: src/app/destiny1/vendors/D1VendorItems.tsx ================================================ import { ownedItemsSelector } from 'app/inventory/selectors'; import { isEmpty } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { useSelector } from 'react-redux'; import BungieImage from '../../dim-ui/BungieImage'; import * as styles from '../../vendors/VendorItems.m.scss'; import D1VendorItem from './D1VendorItem'; import { Vendor, VendorCost } from './vendor.service'; /** * Display the items for a single vendor, organized by category. */ export default function D1VendorItems({ vendor, totalCoins, }: { vendor: Vendor; totalCoins: { [currencyHash: number]: number; }; }) { const allCurrencies: { [hash: number]: VendorCost['currency'] } = {}; const ownedItemHashes = useSelector(ownedItemsSelector); for (const saleItem of vendor.allItems) { for (const cost of saleItem.costs) { allCurrencies[cost.currency.itemHash] = cost.currency; } } return ( <div className={styles.vendorContents}> {!isEmpty(allCurrencies) && ( <div className={styles.currencies}> {Object.values(allCurrencies).map((currency) => ( <div key={currency.itemHash}> {totalCoins?.[currency.itemHash] || 0}{' '} <BungieImage src={currency.icon} className={styles.currencyIcon} title={currency.itemName} /> </div> ))} </div> )} <div className={styles.itemCategories}> {vendor.categories.map((category) => ( <div key={category.index}> <h3 className={styles.categoryTitle}>{category.title || 'Unknown'}</h3> <div className={styles.vendorItems}> {category.saleItems.toSorted(compareBy((i) => i.item.name)).map((item) => ( <D1VendorItem key={item.index} saleItem={item} owned={ownedItemHashes.accountWideOwned.has(item.item.hash)} totalCoins={totalCoins} /> ))} </div> </div> ))} </div> </div> ); } ================================================ FILE: src/app/destiny1/vendors/D1Vendors.m.scss ================================================ @use '../../variables.scss' as *; .vendors { composes: dim-page from global; margin-top: 16px; flex-direction: column; } ================================================ FILE: src/app/destiny1/vendors/D1Vendors.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'vendors': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/destiny1/vendors/D1Vendors.tsx ================================================ import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { currenciesSelector, storesSelector } from 'app/inventory/selectors'; import { useLoadStores } from 'app/inventory/store/hooks'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { compareBy } from 'app/utils/comparators'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { DestinyAccount } from '../../accounts/destiny-account'; import { D1Store } from '../../inventory/store-types'; import D1Vendor from './D1Vendor'; import * as styles from './D1Vendors.m.scss'; import { Vendor, countCurrencies, loadVendors } from './vendor.service'; /** * The "All Vendors" page for D1 that shows all the rotating vendors. */ export default function D1Vendors({ account }: { account: DestinyAccount }) { const dispatch = useThunkDispatch(); const stores = useSelector(storesSelector) as D1Store[]; const currencies = useSelector(currenciesSelector); const [vendors, setVendors] = useState<{ [vendorHash: number]: Vendor; }>(); const storesLoaded = useLoadStores(account); useEffect(() => { (async () => { if (stores.length) { const vendors = await dispatch(loadVendors()); setVendors(vendors); } })(); }, [stores.length, dispatch]); if (!vendors || !storesLoaded) { return <ShowPageLoading message={t('Loading.Profile')} />; } const totalCoins = countCurrencies(stores, vendors, currencies); const sortedVendors = Object.values(vendors).sort(compareBy((v) => v.vendorOrder)); return ( <div className={styles.vendors}> {sortedVendors.map((vendor) => ( <D1Vendor key={vendor.hash} vendor={vendor} totalCoins={totalCoins} /> ))} </div> ); } ================================================ FILE: src/app/destiny1/vendors/vendor.service.ts ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { BungieError } from 'app/bungie-api/http-client'; import { InventoryBuckets } from 'app/inventory/inventory-buckets'; import { bucketsSelector, storesSelector } from 'app/inventory/selectors'; import { amountOfItem } from 'app/inventory/stores-helpers'; import { get, set } from 'app/storage/idb-keyval'; import { ThunkResult } from 'app/store/types'; import { compact, filterMap, isEmpty, sumBy } from 'app/utils/collections'; import { errorLog } from 'app/utils/log'; import { keyBy } from 'es-toolkit'; import { DestinyAccount } from '../../accounts/destiny-account'; import { getVendorForCharacter } from '../../bungie-api/destiny1-api'; import { D1Item } from '../../inventory/item-types'; import { AccountCurrency, D1Store } from '../../inventory/store-types'; import { processItems } from '../../inventory/store/d1-item-factory'; import { loadingTracker } from '../../shell/loading-tracker'; import { D1ManifestDefinitions } from '../d1-definitions'; import { factionAligned } from '../d1-factions'; import { D1ItemComponent, D1VendorDefinition } from '../d1-manifest-types'; /* const allVendors = [ 1990950, // Titan Vanguard 44395194, // Vehicles 134701236, // Guardian Outfitter 174528503, // Eris Morn 242140165, // Iron Banner 459708109, // Shipwright 570929315, // Gunsmith 614738178, // Emote Collection 1303406887, // Cryptarch (Reef) 1410745145, // Queen's Wrath 1460182514, // Exotic Weapon Blueprints 1527174714, // Bounty Tracker 1575820975, // Warlock Vanguard 1808244981, // New Monarchy 1821699360, // Future War Cult 1889676839, // Disciple of Osiris 1998812735, // House of Judgment 2021251983, // Postmaster 2190824860, // Vanguard Scout 2190824863, // Tyra Karn (Cryptarch) 2244880194, // Ship Collection 2420628997, // Shader Collection 2610555297, // Iron Banner 2648860054, // Iron Lord 2668878854, // Vanguard Quartermaster 2680694281, // The Speaker 2762206170, // Postmaster 2796397637, // Agent of the Nine 3003633346, // Hunter Vanguard 3301500998, // Emblem Collection 3611686524, // Dead Orbit 3658200622, // Crucible Quartermaster 3746647075, // Crucible Handler 3902439767, // Exotic Armor Blueprints 3917130357, // Eververse 4269570979 // Cryptarch (Tower) ]; */ // Vendors we don't want to load by default const vendorDenyList = [ 2021251983, // Postmaster, ]; // Hashes for 'Decode Engram' const categoryDenyList = [3574600435, 3612261728, 1333567905, 2634310414]; const xur = 2796397637; export interface VendorCost { currency: { icon: string; itemHash: number; itemName: string; }; value: number; } export interface VendorSaleItem { costs: VendorCost[]; failureStrings: string; index: number; item: D1Item; unlocked: boolean; unlockedByCharacter: string[]; } export interface Vendor { failed: boolean; nextRefreshDate: string; hash: number; name: string; icon: string; vendorOrder: number; faction: number; location: string; enabled: boolean; expires: number; factionLevel: number; factionAligned: boolean; allItems: VendorSaleItem[]; categories: { index: number; title: string; saleItems: VendorSaleItem[]; }[]; def: D1VendorDefinition; cacheKeys: { [storeId: string]: { expires: number; factionLevel: number; factionAligned: boolean; }; }; saleItemCategories: { saleItems: { item: D1ItemComponent; vendorItemIndex: number; costs: { value: number; itemHash: number }[]; failureIndexes: number[]; unlockStatuses: { isSet: boolean }[]; }[]; categoryIndex: number; }[]; } /** * Returns a promise for a fresh view of the vendors and their items. */ export function loadVendors(): ThunkResult<{ [vendorHash: number]: Vendor }> { return async (_dispatch, getState) => { const account = currentAccountSelector(getState())!; const stores = storesSelector(getState()) as D1Store[]; const characters = stores.filter((s) => !s.isVault); const defs = getState().manifest.d1Manifest!; const buckets = bucketsSelector(getState())!; const reloadPromise = (async () => { // Narrow down to only visible vendors (not packages and such) const vendorList = Object.values(defs.Vendor.getAll()).filter((v) => v.summary.visible); const vendors = compact( await Promise.all( vendorList.flatMap((vendorDef) => fetchVendor(vendorDef, characters, account, defs, buckets), ), ), ); return keyBy(vendors, (v) => v.hash); })(); loadingTracker.addPromise(reloadPromise); return reloadPromise; }; } async function fetchVendor( vendorDef: D1VendorDefinition, characters: D1Store[], account: DestinyAccount, defs: D1ManifestDefinitions, buckets: InventoryBuckets, ): Promise<Vendor | null> { if (vendorDenyList.includes(vendorDef.hash)) { return null; } const vendorsForCharacters = await Promise.all( characters.map((store) => loadVendorForCharacter(account, store, vendorDef, defs, buckets)), ); const nonNullVendors = compact(vendorsForCharacters); if (nonNullVendors.length) { return mergeVendors(nonNullVendors); } else { return null; } } function mergeVendors([firstVendor, ...otherVendors]: Vendor[]) { const mergedVendor = structuredClone(firstVendor); for (const vendor of otherVendors) { Object.assign(mergedVendor.cacheKeys, vendor.cacheKeys); for (const category of vendor.categories) { const existingCategory = mergedVendor.categories.find((c) => c.title === category.title); if (existingCategory) { mergeCategory(existingCategory, category); } else { mergedVendor.categories.push(category); } } } mergedVendor.allItems = mergedVendor.categories.flatMap((i) => i.saleItems); return mergedVendor; } function mergeCategory( mergedCategory: { index: number; title: string; saleItems: VendorSaleItem[]; }, otherCategory: { index: number; title: string; saleItems: VendorSaleItem[]; }, ) { for (const saleItem of otherCategory.saleItems) { const existingSaleItem = mergedCategory.saleItems.find((i) => i.index === saleItem.index); if (existingSaleItem) { existingSaleItem.unlocked ||= saleItem.unlocked; if (saleItem.unlocked) { existingSaleItem.unlockedByCharacter.push(saleItem.unlockedByCharacter[0]); } } else { mergedCategory.saleItems.push(saleItem); } } } async function loadVendorForCharacter( account: DestinyAccount, store: D1Store, vendorDef: D1VendorDefinition, defs: D1ManifestDefinitions, buckets: InventoryBuckets, ) { try { return await loadVendor(account, store, vendorDef, defs, buckets); } catch (e) { if (vendorDef.hash !== 2796397637 && vendorDef.hash !== 2610555297) { // Xur, IB errorLog( 'vendors', `Failed to load vendor ${vendorDef.summary.vendorName} for ${store.name}`, e, ); } return null; } } /** * Get this character's level for the given faction. */ function factionLevel(store: D1Store, factionHash: number) { const rep = store.progressions.find((rep) => rep.faction?.hash === factionHash); return rep?.level || 0; } /** * A cached vendor is only usable if it's not expired, and this character hasn't * changed level for the faction associated with this vendor (or changed whether * they're aligned with that faction). */ function cachedVendorUpToDate(vendor: Vendor, store: D1Store, vendorDef: D1VendorDefinition) { return ( vendor && vendor.expires > Date.now() && vendor.factionLevel === factionLevel(store, vendorDef.summary.factionHash) && vendor.factionAligned === factionAligned(store, vendorDef.summary.factionHash) ); } function loadVendor( account: DestinyAccount, store: D1Store, vendorDef: D1VendorDefinition, defs: D1ManifestDefinitions, buckets: InventoryBuckets, ) { const vendorHash = vendorDef.hash; const key = vendorKey(store, vendorHash); return get<Vendor>(key) .then((vendor) => { if (cachedVendorUpToDate(vendor, store, vendorDef)) { // log("loaded local", vendorDef.summary.vendorName, key, vendor); if (vendor.failed) { throw new Error(`Cached failed vendor ${vendorDef.summary.vendorName}`); } return vendor; } else { // log("load remote", vendorDef.summary.vendorName, key, vendorHash, vendor, vendor?.nextRefreshDate); return getVendorForCharacter(account, store, vendorHash) .then((vendor) => { vendor.expires = calculateExpiration(vendor.nextRefreshDate, vendorHash); vendor.factionLevel = factionLevel(store, vendorDef.summary.factionHash); vendor.factionAligned = factionAligned(store, vendorDef.summary.factionHash); return set(key, vendor).then(() => vendor); }) .catch((e) => { // log("vendor error", vendorDef.summary.vendorName, 'for', store.name, e, e.code, e.status); if (e instanceof BungieError && e.status === 'DestinyVendorNotFound') { const vendor = { failed: true, code: e.code, status: e.status, expires: Date.now() + 60 * 60 * 1000 + (Math.random() - 0.5) * (60 * 60 * 1000), factionLevel: factionLevel(store, vendorDef.summary.factionHash), factionAligned: factionAligned(store, vendorDef.summary.factionHash), }; return set(key, vendor).then(() => { throw new Error(`Cached failed vendor ${vendorDef.summary.vendorName}`); }); } throw new Error(`Failed to load vendor ${vendorDef.summary.vendorName}`); }); } }) .then((vendor) => { if (vendor?.enabled) { return processVendor(vendor, vendorDef, defs, store, buckets); } // log("Couldn't load", vendorDef.summary.vendorName, 'for', store.name); return Promise.resolve(null); }); } function vendorKey(store: D1Store, vendorHash: number) { return ['vendor', store.id, vendorHash].join('-'); } function calculateExpiration(nextRefreshDate: string, vendorHash: number): number { const date = new Date(nextRefreshDate).getTime(); if (vendorHash === xur) { // Xur always expires in an hour, because Bungie's data only // says when his stock will refresh, not when he becomes // unavailable. return Math.min(date, Date.now() + 60 * 60 * 1000); } // If the expiration is too far in the future, replace it with +8h if (date - Date.now() > 7 * 24 * 60 * 60 * 1000) { return Date.now() + 8 * 60 * 60 * 1000; } return date; } async function processVendor( vendor: Vendor, vendorDef: D1VendorDefinition, defs: D1ManifestDefinitions, store: D1Store, buckets: InventoryBuckets, ) { const def = vendorDef.summary; const createdVendor: Vendor = { def: vendorDef, hash: vendorDef.hash, name: def.vendorName, icon: def.factionIcon || def.vendorIcon, nextRefreshDate: vendor.nextRefreshDate, cacheKeys: { [store.id]: { expires: vendor.expires, factionLevel: vendor.factionLevel, factionAligned: vendor.factionAligned, }, }, vendorOrder: def.vendorSubcategoryHash + def.vendorOrder, faction: def.factionHash, // TODO: show rep! location: defs.VendorCategory.get(def.vendorCategoryHash).categoryName, failed: false, enabled: true, expires: 0, factionLevel: 0, factionAligned: false, allItems: [], categories: [], saleItemCategories: [], }; const saleItems = vendor.saleItemCategories.flatMap((categoryData) => categoryData.saleItems); for (const saleItem of saleItems) { saleItem.item.itemInstanceId = `vendor-${vendorDef.hash}-${saleItem.vendorItemIndex}`; } const items: (D1Item & { vendorIcon?: string })[] = processItems( undefined, saleItems.map((i) => i.item), defs, buckets, ); for (const item of items) { item.instanced = false; item.taggable = false; item.lockable = false; } const itemsById = keyBy(items, (i) => i.id); const categories = filterMap(Object.values(vendor.saleItemCategories), (category) => { const categoryInfo = vendorDef.categories[category.categoryIndex]; if (categoryDenyList.includes(categoryInfo.categoryHash)) { return undefined; } const categoryItems = category.saleItems.map((saleItem) => { const unlocked = isSaleItemUnlocked(saleItem); return { index: saleItem.vendorItemIndex, costs: saleItem.costs .map((cost) => { const { itemName, icon, itemHash } = defs.InventoryItem.get(cost.itemHash); return { value: cost.value, currency: { itemName, icon, itemHash }, }; }) .filter((c) => c.value > 0), item: itemsById[`vendor-${vendorDef.hash}-${saleItem.vendorItemIndex}`], // TODO: caveat, this won't update very often! unlocked, unlockedByCharacter: unlocked ? [store.id] : [], failureStrings: saleItem.failureIndexes.map((i) => vendorDef.failureStrings[i]).join('. '), }; }); return { index: category.categoryIndex, title: categoryInfo.displayTitle, saleItems: categoryItems, }; }); for (const item of items) { item.vendorIcon = createdVendor.icon; } createdVendor.categories = categories; return createdVendor; } function isSaleItemUnlocked(saleItem: { unlockStatuses: { isSet: boolean }[] }) { return saleItem.unlockStatuses.every((s) => s.isSet); } /** * Calculates a count of how many of each type of currency you * have on all characters, limited to only currencies required to * buy items from the provided vendors. */ export function countCurrencies( stores: D1Store[], vendors: { [vendorHash: number]: Vendor }, currencies: AccountCurrency[], ) { if (!stores || !vendors || !stores.length || isEmpty(vendors)) { return {}; } const categories = Object.values(vendors).flatMap((v) => v.categories); const saleItems = categories.flatMap((c) => c.saleItems); const costs = saleItems.flatMap((i) => i.costs); const totalCoins: { [currencyHash: number]: number } = {}; for (const c of costs) { const currencyHash = c.currency.itemHash; // Legendary marks and glimmer are special cases switch (currencyHash) { case 2534352370: case 3159615086: case 2749350776: totalCoins[currencyHash] = currencies.find((c) => c.itemHash === currencyHash)?.quantity || 0; break; default: totalCoins[currencyHash] = sumBy(stores, (store) => amountOfItem(store, { hash: currencyHash }), ); break; } } return totalCoins; } ================================================ FILE: src/app/destiny2/d2-bucket-categories.ts ================================================ import type { D2BucketCategory } from 'app/inventory/inventory-buckets'; import { BucketHashes } from 'data/d2/generated-enums'; export const D2Categories: { [key in D2BucketCategory]: BucketHashes[]; } = { Postmaster: [ BucketHashes.Engrams, BucketHashes.LostItems, BucketHashes.Messages, BucketHashes.SpecialOrders, ], Weapons: [BucketHashes.KineticWeapons, BucketHashes.EnergyWeapons, BucketHashes.PowerWeapons], Armor: [ BucketHashes.Helmet, BucketHashes.Gauntlets, BucketHashes.ChestArmor, BucketHashes.LegArmor, BucketHashes.ClassArmor, ], General: [ BucketHashes.Subclass, BucketHashes.Ghost, BucketHashes.Emblems, BucketHashes.Ships, BucketHashes.Vehicle, BucketHashes.Emotes, BucketHashes.Finishers, BucketHashes.SeasonalArtifact, ], Inventory: [BucketHashes.Accessories, BucketHashes.Consumables, BucketHashes.Modifications], }; ================================================ FILE: src/app/destiny2/d2-buckets.ts ================================================ import { VendorHashes } from 'app/search/d2-known-values'; import { filterMap } from 'app/utils/collections'; import { BucketCategory } from 'bungie-api-ts/destiny2'; import type { D2BucketCategory, InventoryBucket, InventoryBuckets, } from '../inventory/inventory-buckets'; import { D2Categories } from './d2-bucket-categories'; import { D2ManifestDefinitions } from './d2-definitions'; const bucketHashToSort: { [bucketHash: number]: D2BucketCategory } = {}; for (const [category, bucketHashes] of Object.entries(D2Categories)) { for (const bucketHash of bucketHashes) { bucketHashToSort[bucketHash] = category as D2BucketCategory; } } export function getBuckets(defs: D2ManifestDefinitions) { const buckets: InventoryBuckets = { byHash: {}, byCategory: {}, unknown: { description: 'Unknown items. DIM needs a manifest update.', name: 'Unknown', hash: -1, // default to false. an equipped item existing, will override this in inv display equippable: false, hasTransferDestination: false, capacity: Number.MAX_SAFE_INTEGER, sort: 'Unknown', accountWide: false, category: BucketCategory.Item, }, setHasUnknown() { this.byCategory[this.unknown.sort!] = [this.unknown]; }, }; for (const def of Object.values(defs.InventoryBucket.getAll())) { const sort = bucketHashToSort[def.hash]; const bucket: InventoryBucket = { description: def.displayProperties.description, name: def.displayProperties.name, hash: def.hash, equippable: def.category === BucketCategory.Equippable, hasTransferDestination: def.hasTransferDestination, capacity: def.itemCount, accountWide: def.scope === 1, category: def.category, sort, }; // Add an easy helper property like "inPostmaster" if (bucket.sort) { bucket[`in${bucket.sort}`] = true; } buckets.byHash[bucket.hash] = bucket; } const vaultMappings: { [bucketHash: number]: number } = {}; for (const items of defs.Vendor.get(VendorHashes.Vault).acceptedItems) { vaultMappings[items.acceptedInventoryBucketHash] = items.destinationInventoryBucketHash; } for (const bucket of Object.values(buckets.byHash)) { if (vaultMappings[bucket.hash]) { bucket.vaultBucket = buckets.byHash[vaultMappings[bucket.hash]]; } } for (const [category, bucketHashes] of Object.entries(D2Categories)) { buckets.byCategory[category] = filterMap( bucketHashes, (bucketHash) => buckets.byHash[bucketHash], ); } return buckets; } ================================================ FILE: src/app/destiny2/d2-definitions.test.ts ================================================ import { getTestDefinitions } from 'testing/test-utils'; import { D2ManifestDefinitions } from './d2-definitions'; let defs: D2ManifestDefinitions; beforeAll(async () => { defs = await getTestDefinitions(); }); test('something', () => { expect(Object.keys(defs.InventoryItem).length).toBeGreaterThan(0); }); ================================================ FILE: src/app/destiny2/d2-definitions.ts ================================================ import { UNSET_PLUG_HASH } from 'app/loadout/known-values'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { ThunkResult } from 'app/store/types'; import { warnLogCollapsedStack } from 'app/utils/log'; import { reportException } from 'app/utils/sentry'; import { AllDestinyManifestComponents, DestinyActivityDefinition, DestinyActivityModeDefinition, DestinyActivityModifierDefinition, DestinyBreakerTypeDefinition, DestinyClassDefinition, DestinyCollectibleDefinition, DestinyDamageTypeDefinition, DestinyDestinationDefinition, DestinyEquipableItemSetDefinition, DestinyEventCardDefinition, DestinyFactionDefinition, DestinyGenderDefinition, DestinyIconDefinition, DestinyInventoryBucketDefinition, DestinyInventoryItemConstantsDefinition, DestinyInventoryItemDefinition, DestinyItemCategoryDefinition, DestinyLoadoutColorDefinition, DestinyLoadoutIconDefinition, DestinyLoadoutNameDefinition, DestinyMaterialRequirementSetDefinition, DestinyMetricDefinition, DestinyMilestoneDefinition, DestinyObjectiveDefinition, DestinyPlaceDefinition, DestinyPlugSetDefinition, DestinyPresentationNodeDefinition, DestinyProgressionDefinition, DestinyRaceDefinition, DestinyRecordDefinition, DestinySandboxPerkDefinition, DestinySeasonDefinition, DestinySeasonPassDefinition, DestinySocketCategoryDefinition, DestinySocketTypeDefinition, DestinyStatDefinition, DestinyStatGroupDefinition, DestinyTraitDefinition, DestinyVendorDefinition, DestinyVendorGroupDefinition, } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import { setD2Manifest } from '../manifest/actions'; import { getManifest } from '../manifest/manifest-service-json'; import { HashLookupFailure } from './definitions'; type ManifestTablesShort = Exclude<keyof D2ManifestDefinitions, 'isDestiny2'>; export const allTables: ManifestTablesShort[] = [ 'InventoryItem', 'Objective', 'SandboxPerk', 'Stat', 'StatGroup', 'DamageType', 'Progression', 'ItemCategory', 'Activity', 'ActivityModifier', 'Vendor', 'SocketCategory', 'SocketType', 'MaterialRequirementSet', 'Season', 'SeasonPass', 'Milestone', 'Destination', 'Place', 'VendorGroup', 'PlugSet', 'Collectible', 'PresentationNode', 'Record', 'Metric', 'Trait', 'BreakerType', 'EventCard', 'LoadoutName', 'LoadoutIcon', 'LoadoutColor', 'InventoryBucket', 'Class', 'Gender', 'Race', 'Faction', 'ActivityMode', 'EquipableItemSet', 'Icon', 'InventoryItemConstants', ]; export interface DefinitionTable<T> { /** * for troubleshooting/questionable lookups, include second arg * and sentry can gather info about the source of the invalid hash. * `requestor` ideally a string/number, or a definition including a "hash" key */ readonly get: (hash: number, requestor?: { hash: number } | string | number) => T; /** for lookups that frequently may reasonably fail due to def data removal */ readonly getOptional: (hash: number) => T | undefined; readonly getAll: () => { [hash: number]: T }; } export interface D2ManifestDefinitions { InventoryBucket: DefinitionTable<DestinyInventoryBucketDefinition>; Class: DefinitionTable<DestinyClassDefinition>; Gender: DefinitionTable<DestinyGenderDefinition>; Race: DefinitionTable<DestinyRaceDefinition>; Faction: DefinitionTable<DestinyFactionDefinition>; // ActivityMode is used only from destiny-symbols.ts ActivityMode: DefinitionTable<DestinyActivityModeDefinition>; InventoryItem: DefinitionTable<DestinyInventoryItemDefinition>; Objective: DefinitionTable<DestinyObjectiveDefinition>; SandboxPerk: DefinitionTable<DestinySandboxPerkDefinition>; Stat: DefinitionTable<DestinyStatDefinition>; StatGroup: DefinitionTable<DestinyStatGroupDefinition>; Progression: DefinitionTable<DestinyProgressionDefinition>; ItemCategory: DefinitionTable<DestinyItemCategoryDefinition>; Activity: DefinitionTable<DestinyActivityDefinition>; ActivityModifier: DefinitionTable<DestinyActivityModifierDefinition>; Vendor: DefinitionTable<DestinyVendorDefinition>; SocketCategory: DefinitionTable<DestinySocketCategoryDefinition>; SocketType: DefinitionTable<DestinySocketTypeDefinition>; MaterialRequirementSet: DefinitionTable<DestinyMaterialRequirementSetDefinition>; Season: DefinitionTable<DestinySeasonDefinition>; SeasonPass: DefinitionTable<DestinySeasonPassDefinition>; Milestone: DefinitionTable<DestinyMilestoneDefinition>; Destination: DefinitionTable<DestinyDestinationDefinition>; Place: DefinitionTable<DestinyPlaceDefinition>; VendorGroup: DefinitionTable<DestinyVendorGroupDefinition>; PlugSet: DefinitionTable<DestinyPlugSetDefinition>; PresentationNode: DefinitionTable<DestinyPresentationNodeDefinition>; Record: DefinitionTable<DestinyRecordDefinition>; Metric: DefinitionTable<DestinyMetricDefinition>; Trait: DefinitionTable<DestinyTraitDefinition>; BreakerType: DefinitionTable<DestinyBreakerTypeDefinition>; DamageType: DefinitionTable<DestinyDamageTypeDefinition>; Collectible: DefinitionTable<DestinyCollectibleDefinition>; EventCard: DefinitionTable<DestinyEventCardDefinition>; LoadoutName: DefinitionTable<DestinyLoadoutNameDefinition>; LoadoutColor: DefinitionTable<DestinyLoadoutColorDefinition>; LoadoutIcon: DefinitionTable<DestinyLoadoutIconDefinition>; EquipableItemSet: DefinitionTable<DestinyEquipableItemSetDefinition>; Icon: DefinitionTable<DestinyIconDefinition>; InventoryItemConstants: DestinyInventoryItemConstantsDefinition; /** Check if these defs are from D2. Inside an if statement, these defs will be narrowed to type D2ManifestDefinitions. */ readonly isDestiny2: true; } /** * Manifest database definitions. This returns a promise for an * object that has a property named after each of the tables listed * above (defs.TalentGrid, etc.). */ export function getDefinitions(force = false): ThunkResult<D2ManifestDefinitions> { return async (dispatch, getState) => { let existingManifest = d2ManifestSelector(getState()); if (existingManifest && !force) { return existingManifest; } const db = await dispatch(getManifest(allTables)); existingManifest = d2ManifestSelector(getState()); if (existingManifest && !force) { return existingManifest; } const defs = buildDefinitionsFromManifest(db); dispatch(setD2Manifest(defs)); return defs; }; } /** * These are useful constants (mostly item images) that are in the manifest. * This is reassigned to a global because it will never change once loaded, and * we don't want every item image to gain a subscription to the manifest. */ export let itemConstants: DestinyInventoryItemConstantsDefinition | undefined; export function buildDefinitionsFromManifest(db: AllDestinyManifestComponents) { enhanceDBWithFakeEntries(db); const defs: { [table: string]: any; isDestiny2: true } = { isDestiny2: true, }; defs.InventoryItemConstants = itemConstants = db.DestinyInventoryItemConstantsDefinition[1]; for (const tableShort of allTables) { if (tableShort === 'InventoryItemConstants') { // InventoryItemConstants is a special case, it is not a table but a single entry in the // DestinyInventoryItemConstantsDefinition table. continue; } const table = `Destiny${tableShort}Definition` as const; const dbTable = db[table]; if (!dbTable) { throw new Error(`Table ${table} does not exist in the manifest`); } defs[tableShort] = { get(id: number, requestor?: { hash: number } | string | number) { const dbEntry = dbTable[id]; if (!dbEntry) { // there are valid negative hashes that we have added ourselves via enhanceDBWithFakeEntries, // but other than that they should be whole & reasonable sized numbers if (id < 1 || !Number.isSafeInteger(id)) { const requestingEntryInfo = typeof requestor === 'object' ? requestor.hash : requestor; reportException('invalidHash', new HashLookupFailure(table, id), { requestingEntryInfo, failedHash: id, failedComponent: table, }); } else if (id !== UNSET_PLUG_HASH) { // an invalid hash that, in new loadouts, just means lookup should fail warnLogCollapsedStack('hashLookupFailure', `${table}[${id}]`, requestor); } } return dbEntry; }, getOptional(id: number) { return dbTable[id]; }, getAll() { return dbTable; }, }; } return defs as D2ManifestDefinitions; } /** This adds fake entries to the DB for places where we've had to make stuff up. */ function enhanceDBWithFakeEntries(db: AllDestinyManifestComponents) { // We made up an item category for special grenade launchers. For now they can just be a copy // of the regular "Grenade Launcher" category but we could patch in localized descriptions if we wanted. db.DestinyItemCategoryDefinition[-ItemCategoryHashes.GrenadeLaunchers] = { ...db.DestinyItemCategoryDefinition[ItemCategoryHashes.GrenadeLaunchers], }; } ================================================ FILE: src/app/destiny2/definitions.ts ================================================ export class HashLookupFailure extends Error { table: string; id: number; constructor(table: string, id: number) { super(`hashLookupFailure: ${table}[${id}]`); this.table = table; this.id = id; this.name = 'HashLookupFailure'; } } ================================================ FILE: src/app/developer/Developer.tsx ================================================ import { registerApp } from 'app/dim-api/register-app'; import { errorMessage } from 'app/utils/errors'; import React, { useState } from 'react'; const createAppUrl = 'https://www.bungie.net/en/Application/Create'; export default function Developer(this: never) { const urlParams = new URLSearchParams(window.location.search); // Load parameters from either local storage or the URL function useDevParam(param: string) { return useState(() => localStorage.getItem(param) || urlParams.get(param) || ''); } const [apiKey, setApiKey] = useDevParam('apiKey'); const [clientId, setClientId] = useDevParam('oauthClientId'); const [clientSecret, setClientSecret] = useDevParam('oauthClientSecret'); const [dimApiKey, setDimApiKey] = useDevParam('dimApiKey'); const [dimAppName, setDimAppName] = useDevParam('dimAppName'); const URL = window.location.origin; const URLRet = `${URL}/return.html`; let warning; if (window.location.protocol === 'http:') { warning = 'Bungie.net will not accept the http protocol. Serve over https:// and try again.'; } const prefillLink = `${URL}/developer?apiKey=${apiKey}&oauthClientId=${clientId}&oauthClientSecret=${clientSecret}&dimApiKey=${dimApiKey}&dimAppName=${dimAppName}`; const save = (e: React.FormEvent) => { e.preventDefault(); if (apiKey && clientId && clientSecret && dimAppName && dimApiKey) { localStorage.setItem('apiKey', apiKey); localStorage.setItem('oauthClientId', clientId); localStorage.setItem('oauthClientSecret', clientSecret); localStorage.setItem('dimAppName', dimAppName); localStorage.setItem('dimApiKey', dimApiKey); localStorage.removeItem('dimApiToken'); localStorage.removeItem('authorization'); window.location.href = window.location.origin; } else { // eslint-disable-next-line no-alert alert('You need to fill in the whole form'); } }; const onChange = ( setter: React.Dispatch<React.SetStateAction<string>>, ): React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> => (e) => { setter(e.target.value); }; const getDimApiKey = async (e: React.MouseEvent) => { e.preventDefault(); if (!dimAppName || !apiKey) { return; } try { const app = await registerApp(dimAppName, apiKey); setDimApiKey(app.dimApiKey); } catch (e) { // eslint-disable-next-line no-alert alert(errorMessage(e)); } }; return ( <div className="dim-page"> <h1>Developer Settings</h1> <p> To run DIM locally, you need to create and register your own personal app with both the Bungie.net and DIM APIs. </p> {apiKey && clientId && clientSecret && dimAppName && dimApiKey && ( <a href={prefillLink}> Open this link in another browser to clone these settings to DIM there </a> )} {warning ? ( <div> <h3>Configuration Error</h3> <span>{warning}</span> </div> ) : ( <form onSubmit={save}> <h3>Bungie.net API Key</h3> <ol> <li> Visit{' '} <a href={createAppUrl} target="_blank" rel="noreferrer noopener"> {createAppUrl} </a> </li> <li> Paste <input name="redirectUrl" type="text" value={URLRet} readOnly size={30} /> into the "Redirect URL" section under "App Authentication". </li> <li> Paste <input name="originHeader" type="text" value={URL} readOnly size={20} /> into the "Origin Header" section under "Browser Based Apps". </li> <li>Select "Confidential" OAuth type.</li> <li> Select all scopes <i>except</i> for Administrate Groups/Clans </li> <li> After saving, copy the "API Key" here: <br /> <input name="apiKey" type="text" value={apiKey} onChange={onChange(setApiKey)} size={40} /> </li> <li> Copy the "OAuth client_id" here: <br /> <input name="clientId" type="text" value={clientId} onChange={onChange(setClientId)} size={5} /> </li> <li> Copy the "OAuth client_secret" here: <br /> <input name="clientSecret" type="text" value={clientSecret} onChange={onChange(setClientSecret)} size={50} /> </li> </ol> <h3>DIM API Key</h3> <ol> <li> Choose a name for your DIM API app (only required to create or recover your API key). This should be in the form of "yourname-dev" and will show up in API audit logs. (min length: 3, chars allowed [a-z0-9-]) <br /> <input name="dimAppName" type="text" value={dimAppName} onChange={onChange(setDimAppName)} size={25} /> <button type="button" className="dim-button" onClick={getDimApiKey} disabled={!apiKey || !dimAppName?.match(/^[a-z0-9-]{3,}$/)} > Get API Key </button> </li> <li> DIM API key <br /> <input name="clientSecret" type="dimApiKey" value={dimApiKey} size={36} readOnly /> </li> </ol> <button type="submit" className="dim-button" disabled={!(apiKey && clientId && clientSecret && dimAppName && dimApiKey)} > Save API Keys </button> </form> )} </div> ); } ================================================ FILE: src/app/dim-api/actions.ts ================================================ import { DeleteAllResponse } from '@destinyitemmanager/dim-api-types'; import { needsDeveloper } from 'app/accounts/actions'; import { DestinyAccount } from 'app/accounts/destiny-account'; import { accountsSelector, currentAccountSelector } from 'app/accounts/selectors'; import { FatalTokenError } from 'app/bungie-api/authenticated-fetch'; import { dimErrorToaster } from 'app/bungie-api/error-toaster'; import { getToken } from 'app/bungie-api/oauth-tokens'; import { t } from 'app/i18next-t'; import { showNotification } from 'app/notifications/notifications'; import { Settings, initialSettingsState } from 'app/settings/initial-settings'; import { readyResolve } from 'app/settings/settings'; import { refresh$ } from 'app/shell/refresh-events'; import { get, set } from 'app/storage/idb-keyval'; import { observe } from 'app/store/observerMiddleware'; import { RootState, ThunkResult } from 'app/store/types'; import { convertToError, errorMessage } from 'app/utils/errors'; import { errorLog, infoLog } from 'app/utils/log'; import { delay } from 'app/utils/promises'; import { debounce, once } from 'es-toolkit'; import { deepEqual } from 'fast-equals'; import { AnyAction, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { getPlatforms } from '../accounts/platforms'; import { deleteAllData, getDimApiProfile, getGlobalSettings, postUpdates, } from '../dim-api/dim-api'; import { promptForApiPermission } from './api-permission-prompt'; import { ProfileUpdateWithRollback } from './api-types'; import { ProfileIndexedDBState, allDataDeleted, finishedUpdates, flushUpdatesFailed, globalSettingsLoaded, prepareToFlushUpdates, profileLoadError, profileLoaded, profileLoadedFromIDB, setApiPermissionGranted, } from './basic-actions'; import { DimApiState } from './reducer'; import { apiPermissionGrantedSelector, makeProfileKeyFromAccount } from './selectors'; const TAG = 'dim sync'; const installApiPermissionObserver = once(<D extends Dispatch>(dispatch: D) => { // Observe API permission and reflect it into local storage // We could also use a thunk action instead of an observer... either way dispatch( observe({ id: 'api-permission-observer', runInitially: true, getObserved: (state) => state.dimApi.apiPermissionGranted, sideEffect: ({ current }) => { if (current !== null) { // Save the permission preference to local storage localStorage.setItem('dim-api-enabled', current ? 'true' : 'false'); } }, }), ); }); /** * Watch the redux store and write out values to indexedDB, etc. */ const installObservers = once((dispatch: ThunkDispatch<RootState, undefined, AnyAction>) => { // Watch the state and write it out to IndexedDB dispatch( observe({ id: 'profile-observer', getObserved: (state) => state.dimApi, sideEffect: debounce( ({ previous, current }: { previous: DimApiState | undefined; current: DimApiState }) => { if ( // Avoid writing back what we just loaded from IDB previous?.profileLoadedFromIndexedDb && // Check to make sure one of the fields we care about has changed (current.settings !== previous.settings || current.profiles !== previous.profiles || current.updateQueue !== previous.updateQueue || current.itemHashTags !== previous.itemHashTags || current.searches !== previous.searches || current.globalSettings !== previous.globalSettings) ) { // Only save the difference between the current and default settings const settingsToSave = subtractObject( current.settings, initialSettingsState, ) as Settings; const savedState: ProfileIndexedDBState = { settings: settingsToSave, profiles: current.profiles, updateQueue: current.updateQueue, itemHashTags: current.itemHashTags, searches: current.searches, globalSettings: current.globalSettings, }; infoLog(TAG, 'Saving profile data to IDB'); set('dim-api-profile', savedState); } }, 1000, ), }), ); // Watch the update queue and flush updates dispatch( observe({ id: 'queue-observer', getObserved: (state) => state.dimApi.updateQueue, sideEffect: debounce(({ current }: { current: ProfileUpdateWithRollback[] }) => { if (current.length) { dispatch(flushUpdates()); } }, 1000), }), ); // Every time data is refreshed, maybe load DIM API data too refresh$.subscribe(() => dispatch(loadDimApiData())); }); /** * Load global API configuration from the server. This doesn't even require the user to be logged in. */ function loadGlobalSettings(): ThunkResult { return async (dispatch, getState) => { // TODO: better to use a state machine (UNLOADED => LOADING => LOADED) if (!getState().dimApi.globalSettingsLoaded) { try { const globalSettings = await getGlobalSettings(); infoLog(TAG, 'globalSettings', globalSettings); dispatch(globalSettingsLoaded(globalSettings)); } catch (e) { errorLog(TAG, 'Failed to load global settings from DIM API', e); } } }; } /** * Wait, with exponential backoff - we'll try infinitely otherwise, in a tight loop! * Double the wait time, starting with 60 seconds, until we reach 10 minutes. */ function getBackoffWaitTime(backoff: number) { // Don't wait less than 10 seconds or more than 10 minutes return Math.max(10_000, Math.min(10 * 60 * 1000, Math.random() * Math.pow(2, backoff) * 30_000)); } // Backoff multiplier let getProfileBackoff = 0; let waitingForApiPermission = false; /** * Load all API data (including global settings). This should be called at start * and whenever the account is changed. It's also called whenever stores refresh * (via the refresh button or when auto refresh triggers). This action is meant * to be called repeatedly and be idempotent. * * Note that we block loading the manifest on this, because we need the user's * settings in order to choose the right language. * * TODO: If we can replace the manifest after load, maybe we just load using the * default language and switch it if the language in settings is different. * * This action drives a workflow for onboarding to DIM Sync, as well. We check * for whether the user has opted in to Sync, and if they haven't, we prompt. * Usually they already made their choice at login, though. */ export function loadDimApiData( options: { /** * forceLoad will load from the server even if the minimum refresh * interval has not passed. Keep in mind the server caches full-profile data for * up to 60 seconds. This will also skip using a sync token to load incremental changes. */ forceLoad?: boolean; } = {}, ): ThunkResult { return async (dispatch, getState) => { const { forceLoad = false } = options; installApiPermissionObserver(dispatch); // Load from indexedDB if needed const profileFromIDB = dispatch(loadProfileFromIndexedDB()); // Load global settings first. This fails open (we fall back to defaults) // but loading it first gives us a chance to find out if the API is disabled // and what the current refresh rate is, which gives us important // operational controls in case the API is knocked over. const globalSettingsLoad = dispatch(loadGlobalSettings()); // Don't let actions pile up blocked on the approval UI if (waitingForApiPermission) { return; } // Show a prompt if the user has not said one way or another whether they want to use the API const hasBungieToken = Boolean(getToken()); if (getState().dimApi.apiPermissionGranted === null && hasBungieToken) { waitingForApiPermission = true; try { const useApi = await promptForApiPermission(); dispatch(setApiPermissionGranted(useApi)); } finally { waitingForApiPermission = false; } } // Load accounts info - we can't load the profile-specific DIM API data without it. const getPlatformsPromise = dispatch(getPlatforms); // in parallel, we'll wait later await profileFromIDB; readyResolve(); installObservers(dispatch); // idempotent await Promise.race([globalSettingsLoad, delay(3_000)]); // don't wait forever for global settings // They don't want to sync from the server, or the API is disabled - stick with local data if ( !getState().dimApi.apiPermissionGranted || !getState().dimApi.globalSettings.dimApiEnabled ) { return; } // don't load from remote if there is already an update queue from IDB - we'd roll back data otherwise! if (getState().dimApi.updateQueue.length > 0) { try { await dispatch(flushUpdates()); // flushUpdates will call loadDimApiData again at the end return; } catch {} } // get current account await getPlatformsPromise; if (!accountsSelector(getState()).length) { // User isn't logged in or has no accounts, nothing to load! return; } const currentAccount = currentAccountSelector(getState()); // How long before the API data is considered stale is controlled from the server const profileOutOfDateOrMissing = profileLastLoaded(getState().dimApi, currentAccount) > getState().dimApi.globalSettings.dimProfileMinimumRefreshInterval * 1000; if (forceLoad || profileOutOfDateOrMissing) { try { const syncToken = currentAccount && $featureFlags.dimApiSync && !forceLoad ? getState().dimApi.profiles?.[makeProfileKeyFromAccount(currentAccount)]?.sync : undefined; const profileResponse = await getDimApiProfile(currentAccount, syncToken); dispatch(profileLoaded({ profileResponse, account: currentAccount })); infoLog(TAG, 'Loaded profile from DIM API', profileResponse); // Quickly heal from being failure backoff getProfileBackoff = Math.floor(getProfileBackoff / 2); } catch (err) { if (err instanceof FatalTokenError) { // We're already sent to login, don't keep trying to use DIM Sync. if ($DIM_FLAVOR === 'dev') { dispatch(needsDeveloper()); } return; } // Only notify error once if (!getState().dimApi.profileLoadedError) { showProfileLoadErrorNotification(err); } const e = convertToError(err); dispatch(profileLoadError(e)); errorLog(TAG, 'Unable to get profile from DIM API', e); // Wait, with exponential backoff getProfileBackoff++; const waitTime = getBackoffWaitTime(getProfileBackoff); infoLog(TAG, 'Waiting', waitTime, 'ms before re-attempting profile fetch'); // Wait, then retry. We don't await this here so we don't stop the finally block from running delay(waitTime).then(() => dispatch(loadDimApiData(options))); } } // Make sure any queued updates get sent to the server await dispatch(flushUpdates()); }; } /** * Get either the profile-specific last loaded time, or the global one if we don't have * an account selected. */ function profileLastLoaded(dimApi: DimApiState, account: DestinyAccount | undefined) { return ( Date.now() - (account ? (dimApi.profiles[makeProfileKeyFromAccount(account)]?.profileLastLoaded ?? 0) : dimApi.profileLastLoaded) ); } // Backoff multiplier let flushUpdatesBackoff = 0; /** * Process the queue of updates by sending them to the server */ function flushUpdates(): ThunkResult { return async (dispatch, getState) => { let dimApiState = getState().dimApi; // Skip flushing state if the API is disabled if (!dimApiState.globalSettings.dimApiEnabled) { return; } // Skip if there's already an update going on, or the queue is empty if (dimApiState.updateInProgressWatermark !== 0 || dimApiState.updateQueue.length === 0) { return; } // Prepare the queue dispatch(prepareToFlushUpdates()); dimApiState = getState().dimApi; if (dimApiState.updateInProgressWatermark === 0) { return; } infoLog(TAG, 'Flushing queue of', dimApiState.updateInProgressWatermark, 'updates'); // Only select the items that were frozen for update. They're guaranteed // to not change while we're updating and they'll be for a single profile. const updates = dimApiState.updateQueue.slice(0, dimApiState.updateInProgressWatermark); try { const firstWithAccount = updates.find((u) => u.platformMembershipId) || updates[0]; const results = await postUpdates( firstWithAccount.platformMembershipId, firstWithAccount.destinyVersion, updates, ); // Quickly heal from being failure backoff flushUpdatesBackoff = Math.floor(flushUpdatesBackoff / 2); dispatch(finishedUpdates(results)); dimApiState = getState().dimApi; if (dimApiState.updateQueue.length > 0) { // Flush more updates! dispatch(flushUpdates()); } else if (!dimApiState.profileLoaded) { // Load API data in case we didn't do it before dispatch(loadDimApiData()); } } catch (e) { if (flushUpdatesBackoff === 0) { showUpdateErrorNotification(e); } errorLog(TAG, 'Unable to save updates to DIM API', e); // Wait, with exponential backoff flushUpdatesBackoff++; const waitTime = getBackoffWaitTime(flushUpdatesBackoff); // Don't wait for the retry, so we don't block profile loading (async () => { infoLog(TAG, 'Waiting', waitTime, 'ms before re-attempting updates'); await delay(waitTime); // Now mark the queue failed so it can be retried. Until // updateInProgressWatermark gets reset no other flushUpdates call will // do anything. dispatch(flushUpdatesFailed()); // Try again dispatch(flushUpdates()); })(); throw e; } }; } function loadProfileFromIndexedDB(): ThunkResult { return async (dispatch, getState) => { if (getState().dimApi.profileLoadedFromIndexedDb) { return; } const profile = await get<ProfileIndexedDBState | undefined>('dim-api-profile'); dispatch(profileLoadedFromIDB(profile)); }; } /** Produce a new object that's only the key/values of obj that are also keys in defaults and which have values different from defaults. */ function subtractObject<T>(obj: T | undefined, defaults: T): Partial<T> { const result: Partial<T> = {}; if (obj) { for (const key in defaults) { if (obj[key] !== undefined && !deepEqual(obj[key], defaults[key])) { result[key] = obj[key]; } } } return result; } /** * Wipe out all data in the DIM Sync cloud storage. Not recoverable! */ export function deleteAllApiData(): ThunkResult<DeleteAllResponse['deleted']> { return async (dispatch, getState) => { const result = await deleteAllData(); // If they have the API enabled, also clear out everything locally. Otherwise we'll just clear out the remote data. if (apiPermissionGrantedSelector(getState())) { dispatch(allDataDeleted()); } return result; }; } function showProfileLoadErrorNotification(e: unknown) { showNotification( dimErrorToaster(t('Storage.ProfileErrorTitle'), t('Storage.ProfileErrorBody'), errorMessage(e)), ); } function showUpdateErrorNotification(e: unknown) { showNotification( dimErrorToaster(t('Storage.UpdateErrorTitle'), t('Storage.UpdateErrorBody'), errorMessage(e)), ); } ================================================ FILE: src/app/dim-api/api-permission-prompt.m.scss ================================================ .buttons { composes: flexRow from '../dim-ui/common.m.scss'; justify-content: space-evenly; > * { margin: 10px 8px 0 0; &:last-child { margin-right: 0; } } } ================================================ FILE: src/app/dim-api/api-permission-prompt.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttons': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-api/api-permission-prompt.tsx ================================================ import { t } from 'app/i18next-t'; import NotificationButton from 'app/notifications/NotificationButton'; import { showNotification } from 'app/notifications/notifications'; import { AppIcon, faCheck } from 'app/shell/icons'; import React from 'react'; import * as styles from './api-permission-prompt.m.scss'; /** * This asks the user if they want to use DIM Sync. It will stay up until a choice is made. * If the user chooses to enable sync, this also kicks off an immediate backup of legacy data. */ export function promptForApiPermission() { let returnValue: (result: boolean) => void; const promise = new Promise<boolean>((resolve) => { returnValue = resolve; }); const ok = async (e: React.MouseEvent) => { e.preventDefault(); returnValue(true); }; const no = (e: React.MouseEvent) => { e.preventDefault(); returnValue(false); }; showNotification({ type: 'success', onClick: () => false, promise, duration: 0, // close immediately on click title: t('Storage.ApiPermissionPrompt.Title'), body: ( <> <div>{t('Storage.ApiPermissionPrompt.Description')}</div> <div className={styles.buttons}> <NotificationButton onClick={ok}> <AppIcon icon={faCheck} /> {t('Storage.ApiPermissionPrompt.Yes')} </NotificationButton> <NotificationButton onClick={no}> {t('Storage.ApiPermissionPrompt.No')} </NotificationButton> </div> </> ), }); return promise; } ================================================ FILE: src/app/dim-api/api-types.ts ================================================ import { DeleteLoadoutUpdate, DeleteSearchUpdate, DestinyVersion, Loadout, ProfileUpdate, SearchType, } from '@destinyitemmanager/dim-api-types'; // https://stackoverflow.com/questions/51691235/typescript-map-union-type-to-another-union-type type AddUpdateInfo<U> = U extends ProfileUpdate ? U & { /** The state before this update - if it fails we can use this to roll back */ before: U['payload'] | undefined; /** The account (if any) this update refers to */ platformMembershipId?: string; destinyVersion?: DestinyVersion; } : never; export interface DeleteLoadoutUpdateWithRollback extends DeleteLoadoutUpdate { before: Loadout; platformMembershipId: string; destinyVersion: DestinyVersion; } export interface DeleteSearchUpdateWithRollback extends DeleteSearchUpdate { before: { query: string; type: SearchType; saved: boolean; }; platformMembershipId: string; destinyVersion: DestinyVersion; } /** * A version of ProfileUpdate that also includes rollback info in a "before" property. */ export type ProfileUpdateWithRollback = | DeleteSearchUpdateWithRollback | DeleteLoadoutUpdateWithRollback | AddUpdateInfo<ProfileUpdate>; ================================================ FILE: src/app/dim-api/basic-actions.ts ================================================ import { GlobalSettings, ProfileResponse, ProfileUpdateResult, SearchType, } from '@destinyitemmanager/dim-api-types'; import { DestinyAccount } from 'app/accounts/destiny-account'; import { createAction } from 'typesafe-actions'; import type { DimApiState } from './reducer'; /** * These are all the "basic" actions for the API - stuff that gets reacted to in the reducer. * * Thunk actions that coordinate more complex workflows are in ./actions. */ /** Bulk update global settings after they've been loaded. */ export const globalSettingsLoaded = createAction('dim-api/GLOBAL_SETTINGS_LOADED')< Partial<GlobalSettings> >(); export const profileLoaded = createAction('dim-api/PROFILE_LOADED')<{ profileResponse: ProfileResponse; account?: DestinyAccount; }>(); export const profileLoadError = createAction('dim-api/PROFILE_ERROR')<Error>(); export type ProfileIndexedDBState = Pick< DimApiState, 'settings' | 'profiles' | 'itemHashTags' | 'searches' | 'updateQueue' | 'globalSettings' >; export const profileLoadedFromIDB = createAction('dim-api/LOADED_PROFILE_FROM_IDB')< ProfileIndexedDBState | undefined >(); /** Track or untrack a Triumph */ export const trackTriumph = createAction('dim-api/TRACK_TRIUMPH')<{ recordHash: number; tracked: boolean; }>(); /** Record that a search was used */ export const searchUsed = createAction('dim-api/SEARCH_USED')<{ query: string; type: SearchType; }>(); /** Save or un-save a search */ export const saveSearch = createAction('dim-api/SAVE_SEARCH')<{ query: string; saved: boolean; type: SearchType; }>(); /** Delete a saved search */ export const searchDeleted = createAction('dim-api/DELETE_SEARCH')<{ query: string; type: SearchType; }>(); /** * This signals that we are about to flush the update queue. */ export const prepareToFlushUpdates = createAction('dim-api/PREPARE_UPDATES')(); export const flushUpdatesFailed = createAction('dim-api/UPDATES_FAILED')(); export const finishedUpdates = createAction('dim-api/FINISHED_UPDATES')<ProfileUpdateResult[]>(); export const setApiPermissionGranted = createAction('dim-api/SET_API_PERMISSION')<boolean>(); export const allDataDeleted = createAction('dim-api/ALL_DATA_DELETED')(); ================================================ FILE: src/app/dim-api/dim-api-helper.ts ================================================ import { FatalTokenError, getActiveToken as getBungieToken, } from 'app/bungie-api/authenticated-fetch'; import { dedupePromise } from 'app/utils/promises'; import { HttpClientConfig } from 'bungie-api-ts/http'; const DIM_API_HOST = 'https://api.destinyitemmanager.com'; export const API_KEY = $DIM_FLAVOR !== 'dev' ? $DIM_API_KEY : localStorage.getItem('dimApiKey')!; const localStorageKey = 'dimApiToken'; /** * Call one of the unauthenticated DIM APIs. */ export async function unauthenticatedApi<T>( config: HttpClientConfig, noApiKey?: boolean, ): Promise<T> { if (!noApiKey && !API_KEY) { throw new Error('No DIM API key configured'); } let url = `${DIM_API_HOST}${config.url}`; if (config.params) { // TODO: properly type HttpClientConfig url = `${url}?${new URLSearchParams(config.params as Record<string, string>).toString()}`; } const headers: RequestInit['headers'] = {}; if (config.body) { headers['Content-Type'] = 'application/json'; } if (!noApiKey) { headers['X-API-Key'] = API_KEY; } headers['X-DIM-Version'] = $DIM_VERSION; const response = await fetch( new Request(url, { method: config.method, body: config.body ? JSON.stringify(config.body) : undefined, headers, }), ); if (response.status === 401) { // Delete our token deleteDimApiToken(); throw new FatalTokenError(`Unauthorized call to ${config.url}`); } if (response.ok) { return response.json() as Promise<T>; } let responseData; try { responseData = (await response.json()) as { error: string; message: string }; } catch {} if (responseData?.error) { throw new Error(`${responseData.error}: ${responseData.message}`); } throw new Error(`Failed to call DIM API: ${response.status}`); } /** * Call one of the authenticated DIM APIs. */ export async function authenticatedApi<T>(config: HttpClientConfig): Promise<T> { if (!API_KEY) { throw new Error('No DIM API key configured'); } const token = await getAuthToken(); let url = `${DIM_API_HOST}${config.url}`; if (config.params) { // TODO: properly type HttpClientConfig url = `${url}?${new URLSearchParams(config.params as Record<string, string>).toString()}`; } const headers: RequestInit['headers'] = { Authorization: `Bearer ${token.accessToken}`, 'X-API-Key': API_KEY, 'X-DIM-Version': $DIM_VERSION, }; if (config.body) { headers['Content-Type'] = 'application/json'; } const response = await fetch( new Request(url, { method: config.method, body: config.body ? JSON.stringify(config.body) : undefined, headers, }), ); if (response.status === 401) { // Delete our token deleteDimApiToken(); } if (response.ok) { return response.json() as Promise<T>; } let responseData; try { responseData = (await response.json()) as { error: string; message: string }; } catch {} if (responseData?.error) { throw new Error(`${responseData.error}: ${responseData.message}`); } throw new Error(`Failed to call DIM API: ${response.status}`); } export interface DimAuthToken { /** Your DIM API access token, to be used in further requests. */ accessToken: string; /** How many seconds from now the token will expire. */ expiresInSeconds: number; /** A UTC epoch milliseconds timestamp representing when the token was acquired. */ inception: number; } /** * Get all token information from saved storage. */ export function getToken(): DimAuthToken | undefined { const tokenString = localStorage.getItem(localStorageKey); return tokenString ? (JSON.parse(tokenString) as DimAuthToken) : undefined; } /** * Save all the information about access/refresh tokens. */ function setToken(token: DimAuthToken) { localStorage.setItem(localStorageKey, JSON.stringify(token)); } export function deleteDimApiToken() { localStorage.removeItem(localStorageKey); } export interface AuthTokenRequest { /** The access token from authenticating with the Bungie.net API */ bungieAccessToken: string; /** The user's Bungie.net membership ID */ membershipId: string; } const refreshToken = dedupePromise(async () => { const bungieToken = await getBungieToken(); const authRequest: AuthTokenRequest = { bungieAccessToken: bungieToken.accessToken.value, membershipId: bungieToken.bungieMembershipId, }; try { const authToken = await unauthenticatedApi<DimAuthToken>({ url: '/auth/token', method: 'POST', body: authRequest, }); authToken.inception = Date.now(); setToken(authToken); return authToken; } catch (e) { if ($DIM_FLAVOR === 'dev') { throw new FatalTokenError('DIM API Key Incorrect'); } throw e; } }); async function getAuthToken(): Promise<DimAuthToken> { const token = getToken(); if (!token || hasTokenExpired(token)) { // Get a token! return refreshToken(); } return token; } /** * Has the token expired, based on its 'expires' property? */ function hasTokenExpired(token?: DimAuthToken) { if (!token) { return true; } const expires = token.inception + token.expiresInSeconds * 1000; const now = Date.now(); return now > expires; } ================================================ FILE: src/app/dim-api/dim-api.ts ================================================ import { DeleteAllResponse, DestinyVersion, ExportResponse, GetSharedLoadoutRequest, GetSharedLoadoutResponse, ImportResponse, Loadout, LoadoutShareRequest, LoadoutShareResponse, PlatformInfoResponse, ProfileResponse, ProfileUpdate, ProfileUpdateRequest, ProfileUpdateResponse, } from '@destinyitemmanager/dim-api-types'; import { DestinyAccount } from 'app/accounts/destiny-account'; import { authenticatedApi, unauthenticatedApi } from './dim-api-helper'; export async function getGlobalSettings() { const response = await unauthenticatedApi<PlatformInfoResponse>( { // This uses "app" instead of "release" because I misremembered it when implementing the server url: `/platform_info?flavor=${$DIM_FLAVOR === 'release' ? 'app' : $DIM_FLAVOR}`, method: 'GET', }, true, ); return response.settings; } export async function getDimApiProfile(account?: DestinyAccount, syncToken?: string) { const params: Record<string, string> = account ? { platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion.toString(), components: 'settings,loadouts,tags,hashtags,searches,triumphs', } : { components: 'settings', }; if (syncToken) { params.sync = syncToken; } return authenticatedApi<ProfileResponse>({ url: '/profile', method: 'GET', params, }); } export async function importData(data: ExportResponse) { return authenticatedApi<ImportResponse>({ url: '/import', method: 'POST', body: data, }); } export async function postUpdates( platformMembershipId: string | undefined, destinyVersion: DestinyVersion | undefined, updates: ProfileUpdate[], ) { // Strip properties updates = updates.map((u) => ({ action: u.action, payload: u.payload })) as ProfileUpdate[]; const request: ProfileUpdateRequest = platformMembershipId && destinyVersion ? { platformMembershipId, destinyVersion, updates, } : { updates, }; const response = await authenticatedApi<ProfileUpdateResponse>({ url: '/profile', method: 'POST', body: request, }); return response.results; } export async function createLoadoutShare(platformMembershipId: string, loadout: Loadout) { const request: LoadoutShareRequest = { platformMembershipId, loadout, }; const response = await authenticatedApi<LoadoutShareResponse>({ url: '/loadout_share', method: 'POST', body: request, }); return response.shareUrl; } export async function getSharedLoadout(shareId: string) { const params = { shareId, } satisfies GetSharedLoadoutRequest; const response = await unauthenticatedApi<GetSharedLoadoutResponse>({ url: '/loadout_share', method: 'GET', params, }); return response.loadout; } export async function deleteAllData() { const response = await authenticatedApi<DeleteAllResponse>({ url: '/delete_all_data', method: 'POST', }); return response.deleted; } export async function exportDimApiData() { return authenticatedApi<ExportResponse>({ url: '/export', method: 'GET', }); } ================================================ FILE: src/app/dim-api/import.ts ================================================ import { DestinyVersion, ExportResponse, ItemAnnotation, Loadout, } from '@destinyitemmanager/dim-api-types'; import { t } from 'app/i18next-t'; import { showNotification } from 'app/notifications/notifications'; import { Settings, initialSettingsState } from 'app/settings/initial-settings'; import { observe, unobserve } from 'app/store/observerMiddleware'; import { ThunkResult } from 'app/store/types'; import { errorMessage } from 'app/utils/errors'; import { errorLog, infoLog } from 'app/utils/log'; import { delay } from 'app/utils/promises'; import { keyBy } from 'es-toolkit'; import { Dispatch } from 'redux'; import { loadDimApiData } from './actions'; import { profileLoadedFromIDB } from './basic-actions'; import { importData } from './dim-api'; import { type DimApiState } from './reducer'; import { makeProfileKey } from './selectors'; const TAG = 'importData'; /** * Import data in the DIM Sync export format into DIM Sync or local storage. * This is from a user clicking "Import" and will always overwrite the data saved locally or on the server. */ export function importDataBackup(data: ExportResponse, silent = false): ThunkResult { return async (dispatch, getState) => { const dimApiData = getState().dimApi; if ( dimApiData.globalSettings.dimApiEnabled && dimApiData.apiPermissionGranted && !dimApiData.profileLoaded ) { await waitForProfileLoad(dispatch); } if (dimApiData.globalSettings.dimApiEnabled && dimApiData.apiPermissionGranted) { try { infoLog(TAG, 'Attempting to import data into DIM API'); const result = await importData(data); // Import immediately into local state dispatch(importBackupIntoLocalState(data, true)); // dim-api can cache the data for up to 60 seconds. Reload from the // server after that so we don't use our faked import data too long. We // won't wait for this. delay(60_000).then(() => dispatch(loadDimApiData({ forceLoad: true }))); infoLog(TAG, 'Successfully imported data into DIM API', result); showImportSuccessNotification(result, true); return; } catch (e) { if (!silent) { errorLog(TAG, 'Error importing data into DIM API', e); showImportFailedNotification(errorMessage(e)); } return; } } else { // Import directly into local state, since the user doesn't want to use DIM Sync dispatch(importBackupIntoLocalState(data, silent)); } }; } function importBackupIntoLocalState(data: ExportResponse, silent = false): ThunkResult { return async (dispatch, getState) => { const settings = data.settings; const loadouts = extractLoadouts(data); const tags = extractItemAnnotations(data); const triumphs: ExportResponse['triumphs'] = data.triumphs || []; const itemHashTags: ExportResponse['itemHashTags'] = data.itemHashTags || []; const importedSearches: ExportResponse['searches'] = data.searches || []; if (!loadouts.length && !tags.length) { if (!silent) { errorLog( 'importData', 'Error importing data into DIM - no data found in import file. (no settings upgrade/API upload attempted. DIM Sync is turned off)', data, ); showImportFailedNotification(t('Storage.ImportNotification.NoData')); } return; } const profiles: DimApiState['profiles'] = {}; for (const platformLoadout of loadouts) { const { platformMembershipId, destinyVersion, ...loadout } = platformLoadout; if (platformMembershipId && destinyVersion) { const key = makeProfileKey(platformMembershipId, destinyVersion); if (!profiles[key]) { profiles[key] = { profileLastLoaded: 0, loadouts: {}, tags: {}, triumphs: [], }; } profiles[key].loadouts[loadout.id] = loadout; } } for (const platformTag of tags) { const { platformMembershipId, destinyVersion, ...tag } = platformTag; if (platformMembershipId && destinyVersion) { const key = makeProfileKey(platformMembershipId, destinyVersion); if (!profiles[key]) { profiles[key] = { profileLastLoaded: 0, loadouts: {}, tags: {}, triumphs: [], }; } profiles[key].tags[tag.id] = tag; } } for (const triumphData of triumphs) { const { platformMembershipId, triumphs } = triumphData; if (platformMembershipId) { const key = makeProfileKey(platformMembershipId, 2); if (!profiles[key]) { profiles[key] = { profileLastLoaded: 0, loadouts: {}, tags: {}, triumphs: [], }; } profiles[key].triumphs = triumphs; } } const searches: DimApiState['searches'] = { 1: [], 2: [], }; for (const search of importedSearches) { searches[search.destinyVersion].push(search.search); } dispatch( profileLoadedFromIDB({ settings: { ...initialSettingsState, ...settings } as Settings, profiles, itemHashTags: keyBy(itemHashTags, (t) => t.hash), searches, updateQueue: [], globalSettings: getState().dimApi.globalSettings, }), ); if (!silent) { showImportSuccessNotification( { loadouts: loadouts.length, tags: tags.length, }, false, ); } }; } // Each observer that is used to observe the change in dimApi profileLoaded state // should be unique, so use a module reference counter. let profileLoadObserverCount = 0; /** Returns a promise that resolves when the profile is fully loaded. */ function waitForProfileLoad<D extends Dispatch>(dispatch: D) { const observerId = `profile-load-observer-${profileLoadObserverCount++}`; return new Promise((resolve) => { dispatch( observe({ id: observerId, runInitially: true, getObserved: (rootState) => rootState.dimApi.profileLoaded, sideEffect: ({ current }) => { if (current) { dispatch(unobserve(observerId)); resolve(undefined); } }, }), ); }); } function showImportSuccessNotification( result: { loadouts: number; tags: number }, dimSync: boolean, ) { showNotification({ type: 'success', title: t('Storage.ImportNotification.SuccessTitle'), body: dimSync ? t('Storage.ImportNotification.SuccessBodyForced', result) : t('Storage.ImportNotification.SuccessBodyLocal', result), duration: 15000, }); } function showImportFailedNotification(message: string) { showNotification({ type: 'error', title: t('Storage.ImportNotification.FailedTitle'), body: t('Storage.ImportNotification.FailedBody', { error: message }), duration: 15000, }); } type PlatformLoadout = Loadout & { platformMembershipId: string; destinyVersion: DestinyVersion; }; /** * Extract loadouts in DIM API format from an export. */ function extractLoadouts(importData: ExportResponse): PlatformLoadout[] { if (importData.loadouts) { return importData.loadouts.map((l) => ({ ...l.loadout, platformMembershipId: l.platformMembershipId, destinyVersion: l.destinyVersion, })); } return []; } type PlatformItemAnnotation = ItemAnnotation & { platformMembershipId: string; destinyVersion: DestinyVersion; }; /** * Extract tags/notes in DIM API format from an export. */ function extractItemAnnotations(importData: ExportResponse): PlatformItemAnnotation[] { if (importData.tags) { return importData.tags.map((t) => ({ ...t.annotation, platformMembershipId: t.platformMembershipId, destinyVersion: t.destinyVersion, })); } return []; } ================================================ FILE: src/app/dim-api/reducer.test.ts ================================================ import { ProfileUpdateResult, SearchType } from '@destinyitemmanager/dim-api-types'; import { DestinyAccount } from 'app/accounts/destiny-account'; import { setItemHashNote, setItemHashTag, setItemNote, setItemTag } from 'app/inventory/actions'; import { deleteLoadout, updateLoadout } from 'app/loadout/actions'; import { setSettingAction } from 'app/settings/actions'; import { identity } from 'app/utils/functions'; import { BungieMembershipType, DestinyClass } from 'bungie-api-ts/destiny2'; import { produce, WritableDraft } from 'immer'; import { ProfileUpdateWithRollback } from './api-types'; import { finishedUpdates, prepareToFlushUpdates, saveSearch, searchDeleted, searchUsed, } from './basic-actions'; import { initialState as apiInitialState, dimApi, DimApiAction, DimApiState, ensureProfile, } from './reducer'; import { makeProfileKeyFromAccount } from './selectors'; const currentAccount: DestinyAccount = { membershipId: '98765', destinyVersion: 2, displayName: 'Foobar', originalPlatformType: BungieMembershipType.TigerPsn, platformLabel: 'PlayStation', platforms: [BungieMembershipType.TigerPsn], lastPlayed: new Date(), }; const currentAccountKey = '98765-d2'; const initialState: DimApiState = { ...apiInitialState, apiPermissionGranted: true, }; describe('dim api reducer', () => { const cases: { name: string; /** * Actions to run to set the initial state before our test actions. These * will be "flushed" already. */ setup?: (state: WritableDraft<DimApiState>) => void; /** * A list of actions to run, followed by prepareToFlushUpdates. The tuple * option allows specifying a different account than `currentAccount`. */ actions: (DimApiAction | [DimApiAction, DestinyAccount])[]; /** * A function for checking expectations on the state after the action. */ checkState: (state: DimApiState) => void; /** * The expected queue after prepareToFlushUpdates. Only the action and * payload need to be included. */ expectedQueue: Pick<ProfileUpdateWithRollback, 'action' | 'payload'>[]; /** * Set this to skip the reverse-update check. */ noReverse?: boolean; }[] = [ { name: 'setSetting: changes settings', actions: [setSettingAction('showNewItems', true)], checkState: (state) => { expect(state.settings.showNewItems).toBe(true); }, expectedQueue: [ { action: 'setting', payload: { showNewItems: true, }, }, ], }, { name: 'setItemTag: sets tags if there were none before', actions: [setItemTag({ itemId: '1234', tag: 'favorite' })], checkState: (state) => { expect(state.profiles[currentAccountKey].tags['1234'].tag).toBe('favorite'); }, expectedQueue: [ { action: 'tag', payload: { id: '1234', tag: 'favorite', }, }, ], }, { name: 'setItemTag: clears set tags', actions: [ setItemTag({ itemId: '1234', tag: 'favorite' }), setItemTag({ itemId: '1234', tag: undefined }), ], checkState: (state) => { expect(state.profiles[currentAccountKey].tags['1234']).toBeUndefined(); }, expectedQueue: [ { action: 'tag', payload: { id: '1234', tag: null, }, }, ], }, { name: 'setItemHashTag: sets tags if there were none before', actions: [setItemHashTag({ itemHash: 1234, tag: 'favorite' })], checkState: (state) => { expect(state.itemHashTags[1234].tag).toBe('favorite'); }, expectedQueue: [ { action: 'item_hash_tag', payload: { hash: 1234, tag: 'favorite', }, }, ], }, { name: 'setItemHashTag: clears set tags', actions: [ setItemHashTag({ itemHash: 1234, tag: 'favorite' }), setItemHashTag({ itemHash: 1234, tag: undefined }), ], checkState: (state) => { expect(state.itemHashTags[1234]?.tag).toBeUndefined(); }, expectedQueue: [ { action: 'item_hash_tag', payload: { hash: 1234, tag: null, }, }, ], }, { name: 'setItemTag/setNote: can set both tag and note', actions: [ setItemTag({ itemId: '1234', tag: 'favorite' }), setItemNote({ itemId: '1234', note: 'foo' }), ], checkState: (state) => { expect(state.profiles[currentAccountKey].tags['1234'].tag).toBe('favorite'); expect(state.profiles[currentAccountKey].tags['1234'].notes).toBe('foo'); }, expectedQueue: [ { action: 'tag', payload: { id: '1234', tag: 'favorite', notes: 'foo', }, }, ], }, { name: 'setItemTag/setNote: can set both tag and note', actions: [ setItemHashTag({ itemHash: 1234, tag: 'favorite' }), setItemHashNote({ itemHash: 1234, note: 'foo' }), ], checkState: (state) => { expect(state.itemHashTags[1234].tag).toBe('favorite'); expect(state.itemHashTags[1234].notes).toBe('foo'); }, expectedQueue: [ { action: 'item_hash_tag', payload: { hash: 1234, tag: 'favorite', notes: 'foo', }, }, ], }, { name: 'searchUsed: can track valid queries', actions: [searchUsed({ query: '(is:masterwork) (is:weapon)', type: SearchType.Item })], checkState: (state) => { const search = state.searches[2][0]; expect(search.query).toBe('is:masterwork is:weapon'); expect(search.usageCount).toBe(1); expect(search.saved).toBe(false); }, expectedQueue: [ { action: 'search', payload: { query: 'is:masterwork is:weapon', type: SearchType.Item, }, }, ], noReverse: true, }, { name: 'saveSearch: can save valid queries', setup: (state) => { state.searches[2] = [ { usageCount: 1, lastUsage: 919191, saved: false, query: 'is:masterwork is:weapon', type: SearchType.Item, }, ]; }, actions: [ saveSearch({ query: '(is:masterwork) (is:weapon)', saved: true, type: SearchType.Item }), ], checkState: (state) => { const search = state.searches[2][0]; expect(search.query).toBe('is:masterwork is:weapon'); expect(search.saved).toBe(true); }, expectedQueue: [ { action: 'save_search', payload: { query: 'is:masterwork is:weapon', saved: true, type: SearchType.Item, }, }, ], }, { name: 'saveSearch: can unsave valid queries', setup: (state) => { state.searches[2] = [ { usageCount: 1, lastUsage: 919191, saved: true, query: 'is:masterwork is:weapon', type: SearchType.Item, }, ]; }, actions: [ saveSearch({ query: '(is:masterwork) (is:weapon)', saved: false, type: SearchType.Item }), ], checkState: (state) => { const search = state.searches[2][0]; expect(search.query).toBe('is:masterwork is:weapon'); expect(search.saved).toBe(false); }, expectedQueue: [ { action: 'save_search', payload: { query: 'is:masterwork is:weapon', saved: false, type: SearchType.Item, }, }, ], }, { name: 'saveSearch: does not save invalid queries', actions: [saveSearch({ query: 'deepsight:incomplete', saved: true, type: SearchType.Item })], checkState: (state) => { expect(state.searches).toMatchObject({ [1]: [], [2]: [], }); }, expectedQueue: [], }, { name: 'saveSearch: can unsave previously saved invalid queries', setup: (state) => { state.searches[2] = [ { usageCount: 1, lastUsage: 919191, saved: true, // This is invalid, but it might have been saved before we changed the rules query: 'deepsight:incomplete', type: SearchType.Item, }, ]; }, actions: [saveSearch({ query: 'deepsight:incomplete', saved: false, type: SearchType.Item })], checkState: (state) => { // FIXME maybe delete this outright? It'll be cleaned up the next time DIM loads the remote profile anyway... const search = state.searches[2][0]; expect(search.query).toBe('deepsight:incomplete'); expect(search.saved).toBe(false); }, expectedQueue: [ { action: 'save_search', payload: { query: 'deepsight:incomplete', saved: false, type: SearchType.Item, }, }, ], }, { name: 'deleteSearch: can delete previously saved invalid queries', setup: (state) => { state.searches[2] = [ { usageCount: 1, lastUsage: 1000, saved: true, // This is invalid, but it might have been saved before we changed the rules query: 'deepsight:incomplete', type: SearchType.Item, }, ]; }, actions: [searchDeleted({ query: 'deepsight:incomplete', type: SearchType.Item })], checkState: (state) => { // FIXME maybe delete this outright? It'll be cleaned up the next time DIM loads the remote profile anyway... expect(state.searches[2].length).toBe(0); }, expectedQueue: [ { action: 'delete_search', payload: { query: 'deepsight:incomplete', type: SearchType.Item, }, }, ], }, { name: 'updateLoadout: can save a loadout', actions: [ updateLoadout({ id: '1234', name: 'before foo', classType: DestinyClass.Warlock, items: [], clearSpace: false, }), ], checkState: (state) => { expect(state.profiles[currentAccountKey].loadouts['1234'].name).toBe('before foo'); }, expectedQueue: [ { action: 'loadout', payload: { id: '1234', name: 'before foo', classType: DestinyClass.Warlock, equipped: [], unequipped: [], clearSpace: false, }, }, ], }, { name: 'updateLoadout: can update a loadout', setup: (state) => { state.profiles[currentAccountKey].loadouts['1234'] = { id: '1234', name: 'before foo', classType: DestinyClass.Warlock, equipped: [], unequipped: [], clearSpace: false, }; return state; }, actions: [ updateLoadout({ id: '1234', name: 'foo', // changed name classType: DestinyClass.Warlock, items: [], clearSpace: false, }), ], checkState: (state) => { expect(state.profiles[currentAccountKey].loadouts['1234'].name).toBe('foo'); }, expectedQueue: [ { action: 'loadout', payload: { id: '1234', name: 'foo', classType: DestinyClass.Warlock, equipped: [], unequipped: [], clearSpace: false, }, }, ], }, { name: 'updateLoadout: can delete a loadout', setup: (state) => { state.profiles[currentAccountKey].loadouts['1234'] = { id: '1234', name: 'before foo', classType: DestinyClass.Warlock, equipped: [], unequipped: [], clearSpace: false, }; return state; }, actions: [deleteLoadout('1234')], checkState: (state) => { expect(state.profiles[currentAccountKey].loadouts['1234']).toBeUndefined(); }, expectedQueue: [ { action: 'delete_loadout', payload: '1234', }, ], }, ]; for (const { name, actions, checkState, expectedQueue = [], setup = identity, noReverse = false, } of cases) { it(name, () => { // Set up the state const setupState = produce(initialState, (draft) => { const profileKey = makeProfileKeyFromAccount(currentAccount); ensureProfile(draft, profileKey); setup(draft); draft.updateQueue = []; draft.updateInProgressWatermark = 0; return draft; }); // Apply all the input actions and call prepareToFlushUpdates const updatedState = [...actions, prepareToFlushUpdates()].reduce((s, action) => { if (Array.isArray(action)) { return dimApi(s, action[0], action[1]); } return dimApi(s, action, currentAccount); }, setupState); // Run test-specific checks checkState(updatedState); expect(updatedState.updateQueue.length).toEqual(expectedQueue.length); let i = 0; for (const entry of updatedState.updateQueue) { const { action, payload } = entry; const { action: expectedAction, payload: expectedPayload } = expectedQueue[i]; expect(action).toBe(expectedAction); if (typeof payload === 'string') { expect(payload).toBe(expectedPayload); } else { expect(payload).toMatchObject(expectedPayload); } i++; } // Fail all the updates in the queue and make sure they reverse back to the initial state const reversed = dimApi( updatedState, finishedUpdates( new Array<ProfileUpdateResult>(updatedState.updateInProgressWatermark).fill({ status: 'Failed', }), ), ); if (!noReverse) { expect(reversed).toEqual(setupState); } }); } }); describe('prepareToFlushUpdates', () => { const cases: { name: string; /** * Actions to run to set the initial state before our test actions. These * will be "flushed" already. */ setupActions?: DimApiAction[]; /** * A list of actions to run, followed by prepareToFlushUpdates. The tuple * option allows specifying a different account than `currentAccount`. */ actions: (DimApiAction | [DimApiAction, DestinyAccount])[]; /** * After prepareToFlushUpdates, the queue should look as if these actions * had been run (e.g. reordered, or with multiple updates consolidated). */ expectedActions?: (DimApiAction | [DimApiAction, DestinyAccount])[]; /** * The expected queue after prepareToFlushUpdates. Use this if you can't * express the queue state with expectedActions. */ expectedQueue?: ProfileUpdateWithRollback[]; /** * The expected inProgressWatermark value. If not set it defaults to * expectedQueue.length. */ expectedInProgressWatermark?: number; }[] = [ { name: 'can coalesce settings', actions: [ // Turn new items on setSettingAction('showNewItems', true), // Modify another setting setSettingAction('itemSize', 35), // Turn new items back off setSettingAction('showNewItems', false), ], expectedActions: [ // The showNewItems setting should cancel out setSettingAction('itemSize', 35), ], }, { name: 'can handle multiple profile updates', actions: [ // Turn new items on setSettingAction('showNewItems', true), // Save a tag for D2 setItemTag({ itemId: '1234', tag: 'favorite' }), // Save a tag for D1, same profile [setItemTag({ itemId: '1231903', tag: 'keep' }), { ...currentAccount, destinyVersion: 1 }], // Save a tag for D2, same profile setItemTag({ itemId: '76543', tag: 'junk' }), ], expectedInProgressWatermark: 3, // Because the D1 tag is outside the queue expectedActions: [ // Turn new items on setSettingAction('showNewItems', true), // Save a tag for D2 setItemTag({ itemId: '1234', tag: 'favorite' }), // Save a tag for D2, same profile setItemTag({ itemId: '76543', tag: 'junk' }), // The D1 tag should be moved to the end [setItemTag({ itemId: '1231903', tag: 'keep' }), { ...currentAccount, destinyVersion: 1 }], ], }, { name: 'can handle multiple profile updates with settings last', actions: [ // Save a tag for D2 setItemTag({ itemId: '1234', tag: 'favorite' }), // Save a tag for D2, same profile setItemTag({ itemId: '76543', tag: 'junk' }), // Turn new items on setSettingAction('showNewItems', true), ], // Exactly the same expectedActions: [ setItemTag({ itemId: '1234', tag: 'favorite' }), setItemTag({ itemId: '76543', tag: 'junk' }), setSettingAction('showNewItems', true), ], }, { name: 'can handle loadouts', setupActions: [ updateLoadout({ id: '1234', name: 'before foo', classType: DestinyClass.Warlock, items: [], clearSpace: false, }), ], actions: [ updateLoadout({ id: '1234', name: 'foo', classType: DestinyClass.Warlock, items: [], clearSpace: false, }), // Update the name updateLoadout({ id: '1234', name: 'foobar', classType: DestinyClass.Warlock, items: [], clearSpace: false, }), deleteLoadout('1234'), ], expectedActions: [deleteLoadout('1234')], }, { name: 'can handle setting both tags and notes', actions: [ setItemTag({ itemId: '1234', tag: 'favorite' }), setItemNote({ itemId: '1234', note: 'woohoo' }), setItemTag({ itemId: '1234', tag: undefined }), ], expectedQueue: [ // Tag and notes are coalesced into a single update { action: 'tag', payload: { id: '1234', tag: null, notes: 'woohoo', craftedDate: undefined, }, before: { id: '1234', tag: null, notes: null, }, platformMembershipId: currentAccount.membershipId, destinyVersion: 2, }, ], }, ]; for (const { name, actions, expectedQueue = [], expectedActions = [], setupActions = [], expectedInProgressWatermark = expectedQueue.length + expectedActions.length, } of cases) { it(name, () => { // Apply the setup actions const setupState = [ ...setupActions, prepareToFlushUpdates(), finishedUpdates( new Array<ProfileUpdateResult>(setupActions.length).fill({ status: 'Success' }), ), ].reduce((s, action) => dimApi(s, action, currentAccount), initialState); setupState.updateQueue = []; setupState.updateInProgressWatermark = 0; // Apply all the input actions, then prepareToFlushUpdates const updatedState = [...actions, prepareToFlushUpdates()].reduce((s, action) => { if (Array.isArray(action)) { return dimApi(s, action[0], action[1]); } return dimApi(s, action, currentAccount); }, setupState); expect(updatedState.updateInProgressWatermark).toBe(expectedInProgressWatermark); // Generate the expected queue const resolvedExpectedQueue = expectedQueue.length ? expectedQueue : expectedActions.reduce((s, action) => { if (Array.isArray(action)) { return dimApi(s, action[0], action[1]); } return dimApi(s, action, currentAccount); }, setupState).updateQueue; expect(updatedState.updateQueue).toEqual(resolvedExpectedQueue); }); } }); describe('finishedUpdates', () => { it('can mark success', () => { let state = dimApi(initialState, setSettingAction('showNewItems', true)); state = dimApi(state, setItemTag({ itemId: '1234', tag: 'favorite' }), currentAccount); state = dimApi(state, prepareToFlushUpdates()); const updatedState = dimApi( state, finishedUpdates([{ status: 'Success' }, { status: 'Success' }]), ); expect(updatedState.updateInProgressWatermark).toBe(0); expect(updatedState.updateQueue).toEqual([]); }); }); ================================================ FILE: src/app/dim-api/reducer.ts ================================================ import md5 from '@beyond-js/md5'; import { CustomStatWeights, DestinyVersion, GlobalSettings, ItemAnnotation, ItemHashTag, Loadout, ProfileResponse, ProfileUpdateResult, Search, SearchType, TagValue, defaultGlobalSettings, } from '@destinyitemmanager/dim-api-types'; import { DestinyAccount } from 'app/accounts/destiny-account'; import { t } from 'app/i18next-t'; import { convertDimLoadoutToApiLoadout } from 'app/loadout/loadout-type-converters'; import { showNotification } from 'app/notifications/notifications'; import { recentSearchComparator } from 'app/search/autocomplete'; import { CUSTOM_TOTAL_STAT_HASH } from 'app/search/d2-known-values'; import { FilterContext } from 'app/search/items/item-filter-types'; import { buildItemFiltersMap } from 'app/search/items/item-search-filter'; import { parseAndValidateQuery } from 'app/search/search-filter'; import { count, isEmpty, uniqBy } from 'app/utils/collections'; import { emptyArray } from 'app/utils/empty'; import { errorLog, infoLog } from 'app/utils/log'; import { reportException } from 'app/utils/sentry'; import { clearWishLists } from 'app/wishlists/actions'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { keyBy } from 'es-toolkit'; import { deepEqual } from 'fast-equals'; import { Draft, produce } from 'immer'; import { ActionType, getType } from 'typesafe-actions'; import * as inventoryActions from '../inventory/actions'; import * as loadoutActions from '../loadout/actions'; import { Loadout as DimLoadout } from '../loadout/loadout-types'; import * as settingsActions from '../settings/actions'; import { Settings, initialSettingsState } from '../settings/initial-settings'; import { DeleteLoadoutUpdateWithRollback, ProfileUpdateWithRollback } from './api-types'; import * as actions from './basic-actions'; import { makeProfileKey, makeProfileKeyFromAccount } from './selectors'; // After you've got a search history of more than this many items, we start deleting the older ones const MAX_SEARCH_HISTORY = 300; export interface DimApiState { globalSettings: GlobalSettings; globalSettingsLoaded: boolean; /** Has the user granted us permission to store their info? */ apiPermissionGranted: boolean | null; profileLoadedFromIndexedDb: boolean; profileLoaded: boolean; profileLoadedError?: Error; // unix timestamp for when any profile was last loaded profileLastLoaded: number; /** * App settings. Settings are global, not per-platform-membership */ settings: Settings; /** * Tags-by-item-hash are only available for D2 and are not profile-specific. Mostly for tagging shaders. */ itemHashTags: { [itemHash: string]: ItemHashTag; }; /* * DIM API profile data, per account. The key is `${platformMembershipId}-d${destinyVersion}`. */ profiles: { [accountKey: string]: { // unix timestamp for when this specific profile was last loaded profileLastLoaded: number; /** Loadouts stored by loadout ID */ loadouts: { [id: string]: Loadout; }; /** Tags/notes stored by inventory item ID */ tags: { [itemId: string]: ItemAnnotation; }; /** Tracked triumphs */ triumphs: number[]; /** This allows us to get just the items that changed from the DIM API instead of the whole deal. */ sync?: string; }; }; /** * Saved searches are per-Destiny-version */ searches: { [version in DestinyVersion]: Search[]; }; /** * Updates that haven't yet been flushed to the API. Each one is optimistic - we apply its * effects to local state immediately, but if they fail later we undo their effects. This * is stored locally to be redriven. */ updateQueue: ProfileUpdateWithRollback[]; /** * This watermark indicates how many items in the update queue (starting with the head of the * queue) are currently in the process of being flushed to the server. Items at indexes * less than the watermark should not be modified. Once the flush is done, those items can * be removed from the queue and this watermark set back to 0. */ updateInProgressWatermark: number; } function getInitialApiPermissionSetting() { const setting = localStorage.getItem('dim-api-enabled'); if (setting === null) { return null; } else if (setting === 'true') { return true; } else { return false; } } /** * Global DIM platform settings from the DIM API. */ export const initialState: DimApiState = { globalSettingsLoaded: false, globalSettings: { ...defaultGlobalSettings, showIssueBanner: false, }, apiPermissionGranted: getInitialApiPermissionSetting(), profileLoaded: false, profileLoadedFromIndexedDb: false, profileLastLoaded: 0, settings: initialSettingsState, itemHashTags: {}, profiles: {}, // TODO: move searches into profiles searches: { 1: [], 2: [], }, updateQueue: [], updateInProgressWatermark: 0, }; export type DimApiAction = | ActionType<typeof actions> | ActionType<typeof settingsActions> | ActionType<typeof clearWishLists> | ActionType<typeof loadoutActions> | ActionType<typeof inventoryActions>; export const dimApi = ( state: DimApiState = initialState, action: DimApiAction, // This is a specially-handled reducer (see reducers.ts) which gets the current account (based on incoming state) passed along account?: DestinyAccount, ): DimApiState => { switch (action.type) { case getType(actions.globalSettingsLoaded): return { ...state, globalSettingsLoaded: true, globalSettings: { ...state.globalSettings, ...action.payload, }, }; case getType(actions.profileLoadedFromIDB): { // When loading from IDB, merge with current state if (state.updateQueue) { // Undo all the changes, starting with the most recent state = state.updateQueue .toReversed() .reduce( (state, update) => produce(state, (draft) => reverseUpdateLocally(draft, update)), state, ); } const newUpdateQueue = action.payload ? // TODO: undo existing updates, add loaded updates, reapply them all [...(action.payload.updateQueue ?? []), ...state.updateQueue] : []; // Now apply all those updates, starting with the oldest state = newUpdateQueue.reduce( (state, update) => produce(state, (draft) => applyUpdateLocally(draft, update)), state, ); return action.payload ? migrateSettings({ ...state, profileLoadedFromIndexedDb: true, settings: { ...state.settings, ...action.payload.settings, }, profiles: { ...state.profiles, ...action.payload.profiles, }, updateQueue: newUpdateQueue, itemHashTags: action.payload.itemHashTags || initialState.itemHashTags, searches: { ...state.searches, ...action.payload.searches, }, globalSettings: state.globalSettings, }) : { ...state, profileLoadedFromIndexedDb: true, }; } case getType(actions.profileLoaded): { const { profileResponse, account } = action.payload; return profileLoaded(state, profileResponse, account); } case getType(actions.profileLoadError): { return { ...state, profileLoadedError: action.payload, }; } case getType(actions.setApiPermissionGranted): { const apiPermissionGranted = action.payload; return apiPermissionGranted ? { ...state, apiPermissionGranted, } : // If we're disabling DIM Sync, unset profile loaded and clear the update queue { ...state, apiPermissionGranted, profileLoaded: false, updateQueue: [], updateInProgressWatermark: 0, }; } case getType(actions.prepareToFlushUpdates): { return prepareUpdateQueue(state); } case getType(actions.allDataDeleted): { return { ...state, profiles: initialState.profiles, settings: initialState.settings, updateQueue: [], updateInProgressWatermark: 0, }; } case getType(actions.finishedUpdates): { return applyFinishedUpdatesToQueue(state, action.payload); } // For now, a failed update just resets state so we can flush again. Note that flushing will happen immediately... case getType(actions.flushUpdatesFailed): return { ...state, updateInProgressWatermark: 0, }; // *** Settings *** case getType(settingsActions.setSettingAction): return changeSetting(state, action.payload.property, action.payload.value); case getType(settingsActions.toggleCollapsedSection): return changeSetting(state, 'collapsedSections', { ...state.settings.collapsedSections, [action.payload]: !state.settings.collapsedSections[action.payload], }); case getType(settingsActions.setCharacterOrder): { const order = action.payload; return changeSetting( state, 'customCharacterSort', // The order includes characters from multiple profiles, so we can't just replace it state.settings.customCharacterSort.filter((id) => !order.includes(id)).concat(order), ); } // Clearing wish lists also clears the wishListSource setting case getType(clearWishLists): return changeSetting(state, 'wishListSource', ''); // *** Loadouts *** case getType(loadoutActions.deleteLoadout): return deleteLoadout(state, action.payload); case getType(loadoutActions.updateLoadout): return updateLoadout(state, action.payload, account!); // *** Tags/Notes *** case getType(inventoryActions.setItemTag): { const { itemId, tag, craftedDate } = action.payload; return produce(state, (draft) => { setTag(draft, itemId, tag, craftedDate, account!); }); } case getType(inventoryActions.setItemTagsBulk): return produce(state, (draft) => { for (const info of action.payload) { setTag(draft, info.itemId, info.tag, info.craftedDate, account!); } }); case getType(inventoryActions.setItemNote): { const { itemId, note, craftedDate } = action.payload; return produce(state, (draft) => { setNote(draft, itemId, note, craftedDate, account!); }); } case getType(inventoryActions.tagCleanup): return tagCleanup(state, action.payload, account!); case getType(inventoryActions.setItemHashTag): return produce(state, (draft) => { setItemHashTag(draft, action.payload.itemHash, action.payload.tag, account!); }); case getType(inventoryActions.setItemHashNote): return produce(state, (draft) => { setItemHashNote(draft, action.payload.itemHash, action.payload.note, account!); }); // *** Searches *** case getType(actions.searchUsed): return produce(state, (draft) => { searchUsed(draft, account!, action.payload.query, action.payload.type); }); case getType(actions.saveSearch): return produce(state, (draft) => { saveSearch( account!, draft, action.payload.query, action.payload.saved, action.payload.type, ); }); case getType(actions.searchDeleted): return produce(state, (draft) => { deleteSearch(account!, draft, action.payload.query, action.payload.type); }); // *** Triumphs *** case getType(actions.trackTriumph): return produce(state, (draft) => { trackTriumph(draft, account!, action.payload.recordHash, action.payload.tracked); }); default: return state; } }; function profileLoaded( state: DimApiState, profileResponse: ProfileResponse, account?: DestinyAccount, ) { const profileKey = account ? makeProfileKeyFromAccount(account) : ''; // If the response is a sync, we'll start with the existing items and merge in // changed items. Otherwise we don't keep anything from the existing profile. let itemHashTags = state.itemHashTags; if (profileResponse.itemHashTags || profileResponse.deletedItemHashTagHashes?.length) { const existingItemHashTags = profileResponse.sync ? state.itemHashTags : undefined; itemHashTags = { ...existingItemHashTags, ...keyBy(profileResponse.itemHashTags ?? [], (t) => t.hash), }; for (const t of profileResponse.deletedItemHashTagHashes ?? []) { delete itemHashTags[t.toString()]; } } let searches = state.searches ?? {}; if (account && (profileResponse.searches || profileResponse.deletedSearchHashes?.length)) { const existingSearches = profileResponse.sync ? state.searches[account.destinyVersion] : []; const newSearches = !profileResponse.searches?.length ? existingSearches : [...existingSearches]; for (const search of profileResponse.searches ?? []) { const foundSearchIndex = newSearches.findIndex( (s) => s.query === search.query && search.type === s.type, ); if (foundSearchIndex >= 0) { newSearches[foundSearchIndex] = search; } else { newSearches.push(search); } } for (const searchHash of profileResponse.deletedSearchHashes ?? []) { const foundSearchIndex = newSearches.findIndex((s) => md5(s.query) === searchHash); if (foundSearchIndex >= 0) { newSearches.splice(foundSearchIndex, 1); } } searches = { ...searches, [account.destinyVersion]: newSearches }; } let profiles = state.profiles; if (account) { const existingProfile = state.profiles[profileKey]; const existingLoadouts = profileResponse.sync ? existingProfile?.loadouts : {}; const newLoadouts = profileResponse.sync && !profileResponse.loadouts?.length && !profileResponse.deletedLoadoutIds?.length ? existingLoadouts : { ...existingLoadouts, ...keyBy(profileResponse.loadouts ?? [], (l) => l.id), }; for (const l of profileResponse.deletedLoadoutIds ?? []) { delete newLoadouts[l]; } const existingTags = profileResponse.sync ? existingProfile?.tags : {}; const newTags = profileResponse.sync && !profileResponse.tags?.length && !profileResponse.deletedTagsIds?.length ? existingTags : { ...existingTags, ...keyBy(profileResponse.tags ?? [], (t) => t.id) }; for (const t of profileResponse.deletedTagsIds ?? []) { delete newTags[t]; } const existingTriumphs = profileResponse.sync ? existingProfile?.triumphs : undefined; const newTriumphs = new Set([ ...(existingTriumphs ?? []), ...(profileResponse.triumphs ?? []).map((t) => parseInt(t.toString(), 10)), ]); for (const t of profileResponse.deletedTriumphs ?? []) { newTriumphs.delete(t); } profiles = { ...state.profiles, // Overwrite just this account's profile. If a specific key is missing from the response, don't overwrite it. [profileKey]: { profileLastLoaded: Date.now(), loadouts: newLoadouts, tags: newTags, triumphs: [...newTriumphs], sync: profileResponse.syncToken, }, }; } // TODO: clean out invalid/simple searches on first load? const newState: DimApiState = migrateSettings({ ...state, profileLoaded: true, profileLoadedError: undefined, profileLastLoaded: Date.now(), settings: { ...state.settings, ...(profileResponse.settings as Settings), }, itemHashTags, profiles, searches, }); // If this is the first load, cleanup searches if ( account && profileResponse.searches?.length && !state.searches[account.destinyVersion].length ) { return produce(newState, (state) => cleanupInvalidSearches(state, account)); } return newState; } /** * Migrates deprecated settings to their new equivalent, and erroneous settings values to their correct value. * This updates the settings state and adds their updates to the update queue */ function migrateSettings(state: DimApiState) { // Fix some integer settings being stored as strings if (typeof state.settings.charCol === 'string') { state = changeSetting(state, 'charCol', parseInt(state.settings.charCol, 10)); } if (typeof state.settings.charColMobile === 'string') { state = changeSetting(state, 'charColMobile', parseInt(state.settings.charColMobile, 10)); } if (typeof state.settings.inventoryClearSpaces === 'string') { state = changeSetting( state, 'inventoryClearSpaces', parseInt(state.settings.inventoryClearSpaces, 10), ); } if (typeof state.settings.itemSize === 'string') { state = changeSetting(state, 'itemSize', parseInt(state.settings.itemSize, 10)); } // Using undefined for the absence of a watermark was a bad idea if (state.settings.itemFeedWatermark === undefined) { state = changeSetting(state, 'itemFeedWatermark', initialSettingsState.itemFeedWatermark); } // Replace 'element' sort with 'elementWeapon' and 'elementArmor' const sortOrder = [...new Set(state.settings.itemSortOrderCustom || [])]; const reversals = [...new Set(state.settings.itemSortReversals || [])]; if (sortOrder.includes('element')) { sortOrder.splice(sortOrder.indexOf('element'), 1, 'elementWeapon', 'elementArmor'); } if (sortOrder.length !== (state.settings.itemSortOrderCustom?.length ?? 0)) { state = changeSetting(state, 'itemSortOrderCustom', sortOrder); } if (reversals.includes('element')) { reversals.splice(reversals.indexOf('element'), 1, 'elementWeapon', 'elementArmor'); } if (reversals.length !== (state.settings.itemSortReversals?.length ?? 0)) { state = changeSetting(state, 'itemSortReversals', reversals); } // converts any old custom stats stored in the old settings key, to the new format const oldCustomStats = state.settings.customTotalStatsByClass; if (!isEmpty(oldCustomStats)) { // this existing array should 100% be empty if the user's stats are in old format... // but not taking any chances. we'll preserve what's there. const customStats = [...state.settings.customStats]; for (const classEnumString in oldCustomStats) { const classEnum: DestinyClass = parseInt(classEnumString, 10); const statHashList = oldCustomStats[classEnum]; if (classEnum !== DestinyClass.Unknown && statHashList?.length > 0) { const weights: CustomStatWeights = {}; for (const statHash of statHashList) { weights[statHash] = 1; } customStats.push({ label: t('Stats.Custom'), shortLabel: 'custom', class: classEnum, weights, // converted old stats get special permission to use stat hashes higher than CUSTOM_TOTAL_STAT_HASH // other are decremented from CUSTOM_TOTAL_STAT_HASH statHash: CUSTOM_TOTAL_STAT_HASH + 1 + classEnum, }); } } // empty out the old-format setting. eventually phase out this old settings key? state = changeSetting(state, 'customStats', customStats); state = changeSetting(state, 'customTotalStatsByClass', {}); } // A previous bug ins settings migration could cause duplicate custom stats if (state.settings.customStats.length) { const uniqCustomStats = uniqBy(state.settings.customStats, (stat) => stat.statHash); if (uniqCustomStats.length !== state.settings.customStats.length) { state = changeSetting(state, 'customStats', uniqCustomStats); } } return state; } function changeSetting<V extends keyof Settings>(state: DimApiState, prop: V, value: Settings[V]) { // Don't worry about changing settings to their current value if (deepEqual(state.settings[prop], value)) { return state; } return produce(state, (draft) => { const beforeValue = draft.settings[prop]; const update: ProfileUpdateWithRollback = { action: 'setting', payload: { [prop]: value, }, before: { [prop]: beforeValue, }, }; applyUpdateLocally(draft, update); draft.updateQueue.push(update); }); } /** * This prepares the update queue to be flushed to the DIM API. It first * compacts the updates so that there aren't redundant actions, and then sets * the update watermark. */ function prepareUpdateQueue(state: DimApiState) { return produce(state, (draft) => { // If the user only wants to save data locally, then throw away the update queue. if (state.apiPermissionGranted === false) { draft.updateQueue = emptyArray(); draft.updateInProgressWatermark = 0; return; } let platformMembershipId: string | undefined; let destinyVersion: DestinyVersion | undefined; // Multiple updates to a particular object can be coalesced into a single update // before being sent. We iterate from beginning (oldest update) to end (newest update). const compacted: { [key: string]: ProfileUpdateWithRollback; } = {}; const rest: ProfileUpdateWithRollback[] = []; for (const update of draft.updateQueue) { // The first time we see a profile-specific update, keep track of which // profile it was, and reject updates for the other profiles. This is // because DIM API update can only work one profile at a time. if (!platformMembershipId && !destinyVersion) { platformMembershipId = update.platformMembershipId; destinyVersion = update.destinyVersion; } else if ( update.platformMembershipId && (update.platformMembershipId !== platformMembershipId || update.destinyVersion !== destinyVersion) ) { // Put it on the list of other updates that won't be flushed, and move on. // Some updates, like settings, aren't profile-specific and can always // be sent. rest.push(update); continue; } compactUpdate(compacted, update); } draft.updateQueue = Object.values(compacted); // Set watermark to what we're going to flush. // TODO: Maybe add a maximum update length? draft.updateInProgressWatermark = draft.updateQueue.length; // Put the other updates we aren't going to send back on the end of the queue. draft.updateQueue.push(...rest); }); } let unique = 0; /** * Combine this update with any update to the same object that's already in the queue. * This is meant to reduce how many updates the API has to process - especially if the * app has been offline for some time. * * For example, if I edit a loadout twice then delete it, we can just issue a delete. * * Note that this may result in taking two updates, one of which would succeed and one * which would fail, and turning them into a single update that will fail and roll back * to the initial state before either of them. Hopefully this is rare. */ function compactUpdate( compacted: { [key: string]: ProfileUpdateWithRollback; }, update: ProfileUpdateWithRollback, ) { // Figure out the ID of the object being acted on let key: string; switch (update.action) { case 'setting': case 'tag_cleanup': // These don't act on a specific object key = update.action; break; case 'loadout': case 'tag': // These store their ID in an object key = `${update.action}-${update.payload.id}`; break; case 'delete_loadout': // The payload is the ID, and it should coalesce with other loadout actions key = `loadout-${update.payload}`; break; case 'save_search': key = `${update.action}-${update.payload.query}`; break; case 'item_hash_tag': // These store their ID in an object key = `${update.action}-${update.payload.hash}`; break; case 'track_triumph': key = `${update.action}-${update.payload.recordHash}`; break; case 'search': case 'delete_search': // These don't combine (though maybe they should be extended to include an array of usage times?) key = `unique-${unique++}`; break; } const existingUpdate = compacted[key]; if (!existingUpdate) { compacted[key] = update; return; } let combinedUpdate: ProfileUpdateWithRollback | undefined; // The if statements checking existingUpdate's action are to inform types switch (update.action) { case 'setting': { if (existingUpdate.action === 'setting') { const payload = { // Merge settings, newer overwriting older ...existingUpdate.payload, ...update.payload, }; const before = { // Reversed order ...update.before, ...existingUpdate.before, }; // Eliminate chains of settings that get back to the initial state for (const key in payload) { const typedKey = key as keyof typeof payload; if (payload[typedKey] === before[typedKey]) { delete payload[typedKey]; delete before[typedKey]; } } if (isEmpty(payload)) { break; } combinedUpdate = { ...existingUpdate, payload, before, }; } break; } case 'tag_cleanup': { if (existingUpdate.action === 'tag_cleanup') { combinedUpdate = { ...existingUpdate, // Combine into a unique set payload: [...new Set([...existingUpdate.payload, ...update.payload])], }; } break; } case 'loadout': { if (existingUpdate.action === 'loadout') { combinedUpdate = { ...existingUpdate, // Loadouts completely overwrite payload: update.payload, // We keep the "before" from the existing update }; } else if (existingUpdate.action === 'delete_loadout') { // Someone deleted then recreated. Maybe a future undo delete case? It's not possible today. combinedUpdate = { ...update, // Before is whatever loadout existed before being deleted. before: existingUpdate.before as Loadout, }; } break; } case 'delete_loadout': { if (existingUpdate.action === 'loadout') { // If there was no before (a new loadout) and now we're deleting it, there's nothing to update. if (!existingUpdate.before) { break; } combinedUpdate = { // Turn it into a delete loadout ...update, // Loadouts completely overwrite before: existingUpdate.before, // We keep the "before" from the existing update } as DeleteLoadoutUpdateWithRollback; } else if (existingUpdate.action === 'delete_loadout') { // Doesn't seem like we should get two delete loadouts for the same thing. Ignore the new update. combinedUpdate = existingUpdate; } break; } case 'tag': { if (existingUpdate.action === 'tag') { // Successive tag/notes updates overwrite combinedUpdate = { ...existingUpdate, payload: { ...existingUpdate.payload, ...update.payload, }, before: { ...update.before!, ...existingUpdate.before!, }, }; } break; } case 'item_hash_tag': { if (existingUpdate.action === 'item_hash_tag') { // Successive tag/notes updates overwrite combinedUpdate = { ...existingUpdate, payload: { ...existingUpdate.payload, ...update.payload, }, before: { ...update.before!, ...existingUpdate.before!, }, }; } break; } case 'track_triumph': { if (existingUpdate.action === 'track_triumph') { // Successive track state updates overwrite combinedUpdate = { ...existingUpdate, payload: { ...existingUpdate.payload, ...update.payload, }, before: existingUpdate.before, }; } break; } case 'save_search': { if (existingUpdate.action === 'save_search') { // Successive save state updates overwrite combinedUpdate = { ...existingUpdate, payload: { ...existingUpdate.payload, ...update.payload, }, before: existingUpdate.before, }; } break; } case 'search': case 'delete_search': break; } if (combinedUpdate) { compacted[key] = combinedUpdate; } else { delete compacted[key]; } } /** * Record the result of an update call to the API */ function applyFinishedUpdatesToQueue(state: DimApiState, results: ProfileUpdateResult[]) { const total = Math.min(state.updateInProgressWatermark, results?.length || 0); for (let i = 0; i < total; i++) { const update = state.updateQueue[i]; const result = results[i]; let message = 'unknown'; switch (update.action) { case 'search': message = update.payload.query; break; case 'delete_search': message = update.payload.query; break; case 'delete_loadout': message = update.payload; break; case 'tag': message = `${update.payload.id}: ${update.before?.tag}/${update.before?.notes} => ${update.payload.tag}/${update.payload.notes}`; break; case 'item_hash_tag': message = `${update.payload.hash}: ${update.before?.tag}/${update.before?.notes} => ${update.payload.tag}/${update.payload.notes}`; break; case 'tag_cleanup': message = update.payload.length.toString(); break; case 'loadout': message = update.payload.name; break; case 'track_triumph': message = update.payload.recordHash.toString(); break; case 'save_search': message = update.payload.query; break; case 'setting': break; } if (!(result.status === 'Success' || result.status === 'NotFound')) { showNotification({ type: 'error', title: t('Storage.UpdateInvalid'), body: `${ update.action === 'loadout' && update.payload ? t('Storage.UpdateInvalidBodyLoadout', { name: update.payload.name }) : t('Storage.UpdateInvalidBody') }\n\n${result.status}(${message}): ${result.message}`, }); errorLog('dim sync', update.action, result.status, message, result.message, update); reportException('dim sync', new Error('invalid dim api update'), { action: update.action, status: result.status, message: result.message, update, resultMessage: result.message, }); state = produce(state, (draft) => reverseUpdateLocally(draft, update)); } else { infoLog('dim sync', update.action, result.status, message, update); } } return { ...state, // There's currently no error that would leave them in the array updateQueue: state.updateQueue.toSpliced(0, state.updateInProgressWatermark), updateInProgressWatermark: 0, }; } /** * Delete a loadout by ID, from any profile it may be in. */ function deleteLoadout(state: DimApiState, loadoutId: string) { return produce(state, (draft) => { let profileWithLoadout: string | undefined; let loadout: Loadout | undefined; for (const profile in draft.profiles) { const loadouts = draft.profiles[profile]?.loadouts; if (loadouts[loadoutId]) { profileWithLoadout = profile; loadout = loadouts[loadoutId]; break; } } if (!loadout || !profileWithLoadout) { return; } const [platformMembershipId, destinyVersion] = parseProfileKey(profileWithLoadout); const update: ProfileUpdateWithRollback = { action: 'delete_loadout', payload: loadoutId, before: loadout, platformMembershipId, destinyVersion, }; applyUpdateLocally(draft, update); draft.updateQueue.push(update); }); } function updateLoadout(state: DimApiState, loadout: DimLoadout, account: DestinyAccount) { if (loadout.id === 'equipped') { throw new Error('You have to change the ID before saving the equipped loadout'); } return produce(state, (draft) => { const profileKey = makeProfileKey(account.membershipId, account.destinyVersion); const profile = ensureProfile(draft, profileKey); const loadouts = profile.loadouts; const newLoadout = convertDimLoadoutToApiLoadout(loadout); const update: ProfileUpdateWithRollback = { action: 'loadout', payload: newLoadout, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, before: loadouts[loadout.id], }; applyUpdateLocally(draft, update); draft.updateQueue.push(update); }); } function setTag( draft: Draft<DimApiState>, itemId: string, tag: TagValue | undefined, craftedDate: number | undefined, account: DestinyAccount, ) { if (!itemId || itemId === '0') { errorLog('setTag', 'Cannot tag a non-instanced item. Use setItemHashTag instead'); return; } const profileKey = makeProfileKeyFromAccount(account); const profile = ensureProfile(draft, profileKey); const tags = profile.tags; const existingTag = tags[itemId]; if (tag) { if (existingTag?.tag === tag) { return; // nothing to do } } else if (!existingTag?.tag) { return; // nothing to do } const updateAction: ProfileUpdateWithRollback = { action: 'tag', payload: { id: itemId, tag: tag ?? null, craftedDate: craftedDate ?? existingTag?.craftedDate, }, before: existingTag ? { id: itemId, tag: existingTag.tag ?? null } : { id: itemId, tag: null }, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, }; applyUpdateLocally(draft, updateAction); draft.updateQueue.push(updateAction); } function setItemHashTag( draft: Draft<DimApiState>, itemHash: number, tag: TagValue | undefined, account: DestinyAccount, ) { const tags = draft.itemHashTags; const existingTag = tags[itemHash]; if (tag) { if (existingTag?.tag === tag) { return; // nothing to do } } else if (!existingTag?.tag) { return; // nothing to do } const updateAction: ProfileUpdateWithRollback = { action: 'item_hash_tag', payload: { hash: itemHash, tag: tag ?? null, }, before: existingTag ? { hash: itemHash, tag: existingTag.tag ?? null } : { hash: itemHash, tag: null }, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, }; applyUpdateLocally(draft, updateAction); draft.updateQueue.push(updateAction); } function setNote( draft: Draft<DimApiState>, itemId: string, notes: string | undefined, craftedDate: number | undefined, account: DestinyAccount, ) { if (!itemId || itemId === '0') { errorLog('setNote', 'Cannot note a non-instanced item. Use setItemHashNote instead'); return; } const profileKey = makeProfileKeyFromAccount(account); const profile = ensureProfile(draft, profileKey); const tags = profile.tags; const existingTag = tags[itemId]; const updateAction: ProfileUpdateWithRollback = { action: 'tag', payload: { id: itemId, notes: notes || null, craftedDate: craftedDate ?? existingTag?.craftedDate, }, before: existingTag ? { id: itemId, notes: existingTag.notes || null } : { id: itemId, notes: null }, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, }; applyUpdateLocally(draft, updateAction); draft.updateQueue.push(updateAction); } function setItemHashNote( draft: Draft<DimApiState>, itemHash: number, notes: string | undefined, account: DestinyAccount, ) { const tags = draft.itemHashTags; const existingTag = tags[itemHash]; const updateAction: ProfileUpdateWithRollback = { action: 'item_hash_tag', payload: { hash: itemHash, notes: notes || null, }, before: existingTag ? { hash: itemHash, notes: existingTag.notes || null } : { hash: itemHash, notes: null }, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, }; applyUpdateLocally(draft, updateAction); draft.updateQueue.push(updateAction); } function tagCleanup(state: DimApiState, itemIdsToRemove: string[], account: DestinyAccount) { if (!state.profileLoaded) { // Don't try to cleanup anything if we haven't loaded yet return state; } return produce(state, (draft) => { const updateAction: ProfileUpdateWithRollback = { action: 'tag_cleanup', payload: itemIdsToRemove, // "before" isn't really valuable here platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, before: undefined, }; applyUpdateLocally(draft, updateAction); draft.updateQueue.push(updateAction); }); } function trackTriumph( draft: Draft<DimApiState>, account: DestinyAccount, recordHash: number, tracked: boolean, ) { const updateAction: ProfileUpdateWithRollback = { action: 'track_triumph', payload: { recordHash: recordHash, tracked, }, before: { recordHash: recordHash, tracked: !tracked, }, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, }; applyUpdateLocally(draft, updateAction); draft.updateQueue.push(updateAction); } function searchUsed( draft: Draft<DimApiState>, account: DestinyAccount, query: string, type: SearchType, ) { const destinyVersion = account.destinyVersion; // Note: memoized const filtersMap = buildItemFiltersMap(destinyVersion); // Canonicalize the query so we always save it the same way const { canonical, saveInHistory } = parseAndValidateQuery(query, filtersMap, { customStats: draft.settings.customStats ?? [], } as FilterContext); if (!saveInHistory) { errorLog('searchUsed', 'Query not eligible to be saved in history', query); return; } query = canonical; const updateAction: ProfileUpdateWithRollback = { action: 'search', payload: { query, type, }, before: undefined, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, }; applyUpdateLocally(draft, updateAction); draft.updateQueue.push(updateAction); // Trim excess searches // TODO: maybe this should be max per type? const searches = draft.searches[destinyVersion]; if (searches.length > MAX_SEARCH_HISTORY) { const sortedSearches = searches.toSorted(recentSearchComparator); const numBuiltinSearches = count(sortedSearches, (s) => s.usageCount <= 0); // remove bottom-sorted search until we get to the limit while (sortedSearches.length > MAX_SEARCH_HISTORY - numBuiltinSearches) { const lastSearch = sortedSearches.pop()!; // Never try to delete the built-in searches or saved searches if (!lastSearch.saved && lastSearch.usageCount > 0) { deleteSearch(account, draft, lastSearch.query, lastSearch.type); } } } } function saveSearch( account: DestinyAccount, draft: Draft<DimApiState>, query: string, saved: boolean, type: SearchType, ) { const destinyVersion = account.destinyVersion; // Note: memoized const filtersMap = buildItemFiltersMap(destinyVersion); // Canonicalize the query so we always save it the same way const { canonical, saveable } = parseAndValidateQuery(query, filtersMap, { customStats: draft.settings.customStats ?? [], } as FilterContext); if (!saveable && saved) { errorLog('searchUsed', 'Query not eligible to be saved', query); return; } query = canonical; // Look for any existing search, either by exact query match or canonical match let existingSearch = draft.searches[destinyVersion].find((s) => s.query === query); if (!existingSearch && !saved) { // If we're trying to unsave a search that doesn't exist, maybe it's saved under another version. existingSearch = draft.searches[destinyVersion].find((s) => { if (!s.saved) { return false; } const { canonical } = parseAndValidateQuery(s.query, filtersMap, { customStats: draft.settings.customStats ?? [], } as FilterContext); return canonical === query; }); } if (!existingSearch && saveable) { // Save this as a "used" search first. This may happen if it's a type of // search we wouldn't normally save to history like a "simple" filter. We // don't go through searchUsed since that errors if the search isn't // saveable. const searchUsedUpdate: ProfileUpdateWithRollback = { action: 'search', payload: { query, type, }, before: undefined, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, }; applyUpdateLocally(draft, searchUsedUpdate); draft.updateQueue.push(searchUsedUpdate); } const updateAction: ProfileUpdateWithRollback = { action: 'save_search', payload: { query, saved, type, }, before: { query, saved: !saved, type, }, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, }; applyUpdateLocally(draft, updateAction); draft.updateQueue.push(updateAction); } function deleteSearch( account: DestinyAccount, draft: Draft<DimApiState>, query: string, type: SearchType, ) { const destinyVersion = account.destinyVersion; // Note: memoized const filtersMap = buildItemFiltersMap(destinyVersion); // Canonicalize the query so we always save it the same way const { canonical } = parseAndValidateQuery(query, filtersMap, { customStats: draft.settings.customStats ?? [], } as FilterContext); const existingSearches = // Find the canonical match first, then the exact match draft.searches[destinyVersion].filter((s) => s.query === canonical || s.query === query); for (const s of existingSearches) { const updateAction: ProfileUpdateWithRollback = { action: 'delete_search', payload: { query: s.query, type, }, before: { query: s.query, saved: s?.saved ?? false, type, }, platformMembershipId: account.membershipId, destinyVersion: account.destinyVersion, }; applyUpdateLocally(draft, updateAction); draft.updateQueue.push(updateAction); } } function cleanupInvalidSearches(draft: Draft<DimApiState>, account: DestinyAccount) { // Filter out saved and builtin searches const searches = draft.searches[account.destinyVersion].filter( (s) => !s.saved && s.usageCount > 0, ); if (!searches.length) { return; } // Note: memoized const filtersMap = buildItemFiltersMap(account.destinyVersion); for (const search of draft.searches[account.destinyVersion]) { if (search.saved || search.usageCount <= 0) { continue; } const { saveInHistory } = parseAndValidateQuery(search.query, filtersMap, { customStats: draft.settings.customStats ?? [], } as FilterContext); if (!saveInHistory) { deleteSearch(account, draft, search.query, search.type); } } } export function parseProfileKey(profileKey: string): [string, DestinyVersion] { const match = profileKey.match(/(\d+)-d(1|2)/); if (!match) { throw new Error("Profile key didn't match expected format"); } return [match[1], parseInt(match[2], 10) as DestinyVersion]; } export function ensureProfile(draft: Draft<DimApiState>, profileKey: string) { if (!draft.profiles[profileKey]) { draft.profiles[profileKey] = { profileLastLoaded: 0, loadouts: {}, tags: {}, triumphs: [], }; } return draft.profiles[profileKey]; } function applyUpdateLocally(draft: Draft<DimApiState>, update: ProfileUpdateWithRollback) { switch (update.action) { case 'setting': { Object.assign(draft.settings, update.payload); break; } case 'search': { const { destinyVersion } = update; const { query, type } = update.payload; const searches = draft.searches[destinyVersion!]; const existingSearch = searches.find((s) => s.query === query); const lastUsage = $DIM_FLAVOR === 'test' ? 1000 : Date.now(); if (existingSearch) { existingSearch.lastUsage = lastUsage; existingSearch.usageCount++; } else { searches.push({ query, usageCount: 1, saved: false, lastUsage, type, }); } break; } case 'delete_search': { const { query } = update.payload; const { destinyVersion } = update; draft.searches[destinyVersion!] = draft.searches[destinyVersion!].filter( (s) => s.query !== query, ); break; } case 'delete_loadout': { const { platformMembershipId, destinyVersion } = update; const loadoutId = update.payload; const profile = makeProfileKey(platformMembershipId!, destinyVersion!); delete draft.profiles[profile]?.loadouts[loadoutId]; break; } case 'tag': { const itemAnnotation = update.payload; const itemId = itemAnnotation.id; const { platformMembershipId, destinyVersion } = update; const profileKey = makeProfileKey(platformMembershipId!, destinyVersion!); const tags = ensureProfile(draft, profileKey).tags; const existingAnnotation = tags[itemId]; if (existingAnnotation) { if (itemAnnotation.tag === null) { delete existingAnnotation.tag; } else if (itemAnnotation.tag) { existingAnnotation.tag = itemAnnotation.tag; } if (itemAnnotation.notes === null) { delete existingAnnotation.notes; } else if (itemAnnotation.notes) { existingAnnotation.notes = itemAnnotation.notes; } if (!existingAnnotation.tag && !existingAnnotation.notes) { delete tags[itemId]; } } else if (itemAnnotation.tag || itemAnnotation.notes) { tags[itemId] = itemAnnotation; } break; } case 'item_hash_tag': { const itemAnnotation = update.payload; const tags = draft.itemHashTags; const existingAnnotation = tags[itemAnnotation.hash]; if (existingAnnotation) { if (itemAnnotation.tag === null) { delete existingAnnotation.tag; } else if (itemAnnotation.tag) { existingAnnotation.tag = itemAnnotation.tag; } if (itemAnnotation.notes === null) { delete existingAnnotation.notes; } else if (itemAnnotation.notes) { existingAnnotation.notes = itemAnnotation.notes; } if (!existingAnnotation.tag && !existingAnnotation.notes) { delete tags[itemAnnotation.hash]; } } else if (itemAnnotation.tag || itemAnnotation.notes) { tags[itemAnnotation.hash] = itemAnnotation; } break; } case 'tag_cleanup': { const { platformMembershipId, destinyVersion } = update; const profileKey = makeProfileKey(platformMembershipId!, destinyVersion!); const profile = ensureProfile(draft, profileKey); for (const itemId of update.payload) { delete profile.tags[itemId]; } break; } case 'loadout': { const { platformMembershipId, destinyVersion, payload: loadout } = update; const profileKey = makeProfileKey(platformMembershipId!, destinyVersion!); if (loadout) { ensureProfile(draft, profileKey).loadouts[loadout.id] = update.payload; } else if (update.before?.id) { // This handles the case where we're reversing a create-loadout action. delete ensureProfile(draft, profileKey).loadouts[update.before.id]; } break; } case 'track_triumph': { const { platformMembershipId, destinyVersion } = update; const profileKey = makeProfileKey(platformMembershipId!, destinyVersion!); const profile = ensureProfile(draft, profileKey); const { recordHash, tracked } = update.payload; const triumphs = profile.triumphs.filter((h) => h !== recordHash); if (tracked) { triumphs.push(recordHash); } profile.triumphs = triumphs; break; } case 'save_search': { const { query, saved } = update.payload; const { destinyVersion } = update; const searches = draft.searches[destinyVersion!]; const existingSearch = searches.find((s) => s.query === query); const lastUsage = $DIM_FLAVOR === 'test' ? 1000 : Date.now(); // This might not exist if reversing a delete_search if (existingSearch) { existingSearch.saved = saved; } else { searches.push({ query, usageCount: 1, saved, lastUsage, type: update.payload.type, }); } break; } } } function reverseUpdateLocally(draft: Draft<DimApiState>, update: ProfileUpdateWithRollback) { try { switch (update.action) { case 'delete_loadout': { const { platformMembershipId, destinyVersion } = update; const loadoutId = update.payload; const profileKey = makeProfileKey(platformMembershipId!, destinyVersion!); const loadouts = ensureProfile(draft, profileKey).loadouts; loadouts[loadoutId] = update.before as Loadout; break; } case 'delete_search': { // delete_search reverses to save_search applyUpdateLocally(draft, { ...update, action: 'save_search', payload: update.before, before: update.payload, } as ProfileUpdateWithRollback); break; } case 'search': // You can't reverse a search usage break; case 'tag_cleanup': // You can't reverse a tag cleanup break; default: applyUpdateLocally(draft, { ...update, payload: update.before, before: update.payload, } as ProfileUpdateWithRollback); break; } } catch (e) { // We don't want to endlessly retry the update if we fail to reverse. The // next profile load will reset the info. errorLog('reverseUpdateLocally', e, update); reportException('reverseUpdateLocally', e, { update }); if ($DIM_FLAVOR === 'test') { throw e; } } } ================================================ FILE: src/app/dim-api/register-app.ts ================================================ import { ApiApp, ErrorResponse } from '@destinyitemmanager/dim-api-types'; import { unauthenticatedApi } from './dim-api-helper'; export async function registerApp(dimAppName: string, bungieApiKey: string) { const appResponse = await unauthenticatedApi<{ app: ApiApp } | ErrorResponse>( { url: '/new_app', method: 'POST', body: { id: dimAppName, bungieApiKey, origin: window.location.origin, }, }, true, ); // Check if request failed for various possible reasons if ('error' in appResponse) { const failResponse: ErrorResponse = appResponse; // Unexpected result, recast throw new Error(`Could not register app: ${failResponse.error} - ${failResponse.message}`); } return appResponse.app; } ================================================ FILE: src/app/dim-api/selectors.ts ================================================ import { DestinyVersion, SearchType, defaultLoadoutParameters, } from '@destinyitemmanager/dim-api-types'; import { DestinyAccount } from 'app/accounts/destiny-account'; import { currentAccountSelector, destinyVersionSelector } from 'app/accounts/selectors'; import { Settings } from 'app/settings/initial-settings'; import { RootState } from 'app/store/types'; import { createSelector } from 'reselect'; export function makeProfileKeyFromAccount(account: DestinyAccount) { return makeProfileKey(account.membershipId, account.destinyVersion); } export function makeProfileKey(platformMembershipId: string, destinyVersion: DestinyVersion) { return `${platformMembershipId}-d${destinyVersion}`; } export const settingsSelector = (state: RootState) => state.dimApi.settings; /** A selector for a particular setting by property name */ export const settingSelector = <K extends keyof Settings>(key: K) => (state: RootState) => state.dimApi.settings[key]; /** * The last used Loadout Optimizer settings, with defaults filled in */ export const savedLoadoutParametersSelector = createSelector( (state: RootState) => settingsSelector(state).loParameters, (loParams) => ({ ...defaultLoadoutParameters, ...loParams }), ); export const savedLoStatConstraintsByClassSelector = (state: RootState) => settingsSelector(state).loStatConstraintsByClass; export const languageSelector = (state: RootState) => settingsSelector(state).language; export const collapsedSelector = (sectionId: string) => (state: RootState): boolean | undefined => settingsSelector(state).collapsedSections[sectionId]; export const customStatsSelector = (state: RootState) => settingsSelector(state).customStats; export const apiPermissionGrantedSelector = (state: RootState) => state.dimApi.apiPermissionGranted === true; export const dimSyncErrorSelector = (state: RootState) => state.dimApi.profileLoadedError; export const updateQueueLengthSelector = (state: RootState) => state.dimApi.updateQueue.length; /** * Return saved API data for the currently active profile (account). */ export const currentProfileSelector = createSelector( currentAccountSelector, (state: RootState) => state.dimApi.profiles, (currentAccount, profiles) => currentAccount ? profiles[makeProfileKeyFromAccount(currentAccount)] : undefined, ); const recentSearchesSelectorCached = createSelector( (state: RootState) => state.dimApi.searches[destinyVersionSelector(state)], (_state: RootState, searchType: SearchType) => searchType, (searches, searchType) => searches.filter((s) => (s.type ?? SearchType.Item) === searchType), ); /** * Returns all recent/saved searches of the given type. */ export const recentSearchesSelector = (searchType: SearchType) => (state: RootState) => recentSearchesSelectorCached(state, searchType); export const trackedTriumphsSelector = createSelector( currentProfileSelector, (profile) => profile?.triumphs || [], ); /** Server control over the issue/campaign banner */ export const issueBannerEnabledSelector = (state: RootState) => state.dimApi.globalSettings.showIssueBanner; ================================================ FILE: src/app/dim-ui/AlertIcon.m.scss ================================================ .alertIcon { color: yellow; } ================================================ FILE: src/app/dim-ui/AlertIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'alertIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/AlertIcon.tsx ================================================ import { AppIcon, faExclamationTriangle } from 'app/shell/icons'; import clsx from 'clsx'; import * as styles from './AlertIcon.m.scss'; export function AlertIcon({ className, title }: { className?: string; title?: string }) { return ( <AppIcon className={clsx(className, styles.alertIcon)} title={title} icon={faExclamationTriangle} /> ); } ================================================ FILE: src/app/dim-ui/AnimatedNumber.tsx ================================================ import { animate, motion, Transition, useMotionValue, useTransform } from 'motion/react'; import { useEffect } from 'react'; const spring: Transition<number> = { type: 'tween', duration: 0.3, ease: 'easeOut', }; /** * A number that animates between values. */ export default function AnimatedNumber({ value, className, }: { value: number; className?: string; }) { const val = useMotionValue(value); const transformedVal = useTransform(val, (v) => Math.floor(v)); useEffect(() => { animate(val, value, spring); }, [val, value]); return <motion.span className={className}>{transformedVal}</motion.span>; } ================================================ FILE: src/app/dim-ui/AutoRefresh.tsx ================================================ import { autoRefreshEnabledSelector } from 'app/inventory/selectors'; import { dimNeedsUpdate$, reloadDIM } from 'app/register-service-worker'; import { hasSearchQuerySelector } from 'app/shell/selectors'; import { RootState } from 'app/store/types'; import { useEventBusListener } from 'app/utils/hooks'; import { EventBus } from 'app/utils/observable'; import { useCallback, useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import { isDragging$ } from '../inventory/drag-events'; import { loadingTracker } from '../shell/loading-tracker'; import { refresh$, refresh as triggerRefresh } from '../shell/refresh-events'; import { sheetsOpen } from './sheets-open'; const globalSettingsSelector = (state: RootState) => state.dimApi.globalSettings; // An intermediate trigger that means we should think about refreshing. This gets us // out of some circular dependencies and decouples the triggering of "we might want to refresh" // from the decision of whether to actually refresh. const tryRefresh$ = new EventBus<undefined>(); function triggerTryRefresh() { tryRefresh$.next(undefined); } /** * All the AutoRefresh component does is play host to the useAutoRefresh hook. It's still * a component so that whenever useAutoRefresh triggers a re-render it's only re-rendering this * component and not the whole app. */ export default function AutoRefresh() { useAutoRefresh(); return null; } /** * Watch the state of the page and fire refresh signals when appropriate. This pauses the auto * refresh when the page isn't visible or when it's on the wrong page, throttles refreshes, and * does other things to be nice to the API. */ function useAutoRefresh() { const { pathname } = useLocation(); const onOptimizerPage = pathname.endsWith('/optimizer'); // Throttle calls to refresh to no more often than destinyProfileMinimumRefreshInterval const throttledRefresh = useThrottledRefresh(); // A timer for auto refreshing on a schedule, if that's enabled. const startTimer = useScheduledAutoRefresh(); // When we go online, refresh useOnlineRefresh(); // When the page changes visibility, maybe refresh useVisibilityRefresh(); // Listen to the signal to try to refresh, and decide whether to actually refresh. // If the page isn't visible or the user isn't online, or the page has been forgotten, don't fire. useEventBusListener( tryRefresh$, useCallback(() => { const hasActivePromises = loadingTracker.active(); const isVisible = !document.hidden; const isOnline = navigator.onLine; const currentlyDragging = isDragging$.getCurrentValue(); if ( !hasActivePromises && isVisible && isOnline && !currentlyDragging && // Don't auto reload on the optimizer page, it makes it recompute all the time !onOptimizerPage ) { throttledRefresh(); // Trigger the refresh assuming we haven't just refreshed } else if (hasActivePromises) { // We were blocked by active promises (transfers, etc.) Try again once // the loading tracker goes back to inactive. const unsubscribe = loadingTracker.active$.subscribe((active) => { if (!active) { unsubscribe(); // Trigger the event bus which will run the most recent version of this handler triggerTryRefresh(); } }); } else if (currentlyDragging) { // Same deal as above, but waiting for the drag to end const unsubscribe = isDragging$.subscribe((dragging) => { if (!dragging) { unsubscribe(); // Trigger the event bus which will run the most recent version of this handler triggerTryRefresh(); } }); } else { // If we didn't refresh because of any of the conditions above, restart the timer to try again next time startTimer(); } }, [onOptimizerPage, throttledRefresh, startTimer]), ); } /** * If autoRefresh is on, ping triggerTryRefresh on a fixed interval. This isn't literally a setInterval * because we want the timing to be between actual refreshes. */ function useScheduledAutoRefresh() { const { destinyProfileRefreshInterval } = useSelector(globalSettingsSelector); const autoRefresh = useSelector(autoRefreshEnabledSelector); // A timer for auto refreshing on a schedule, if that's enabled. const refreshAccountDataInterval = useRef<number>(0); const clearTimer = () => window.clearTimeout(refreshAccountDataInterval.current); const startTimer = useCallback(() => { // Cancel any ongoing timer before restarting clearTimer(); if (autoRefresh) { refreshAccountDataInterval.current = window.setTimeout( triggerTryRefresh, destinyProfileRefreshInterval * 1000, ); } }, [autoRefresh, destinyProfileRefreshInterval]); // Every time we refresh for any reason, restart the timer useEventBusListener(refresh$, startTimer); // Start the timer right away useEffect(() => { startTimer(); return () => clearTimer(); }, [startTimer, autoRefresh /* start/stop the timer if autorefresh changes */]); return startTimer; } /** * Make a function that will call triggerRefresh (the real refresh signal that pages listen to) only * once per destinyProfileMinimumRefreshInterval. */ function useThrottledRefresh() { const { destinyProfileMinimumRefreshInterval } = useSelector(globalSettingsSelector); const lastRefreshTimestamp = useRef(0); const refresh = useCallback(() => { if (Date.now() - lastRefreshTimestamp.current < destinyProfileMinimumRefreshInterval * 1000) { return; } // Ping the refresh$ event bus, which pages can listen to to actually perform data refreshing triggerRefresh(); lastRefreshTimestamp.current = Date.now(); }, [destinyProfileMinimumRefreshInterval]); return refresh; } /** * Trigger a refresh attempt when the browser goes online or offline (the * refresh handler will not refresh if it's offline). */ function useOnlineRefresh() { useEffect(() => { document.addEventListener('online', triggerTryRefresh); return () => { document.removeEventListener('online', triggerTryRefresh); }; }, []); } // TODO: https://developer.mozilla.org/en-US/docs/Web/API/Idle_Detection_API /** * Trigger a refresh attempt whenever the page becomes visible. This also includes * "sneaky updates" where DIM will try to reload itself if it becomes invisible while * it needs an update. * * We try not to do the sneaky update when the user is in the middle of something (sheets, etc) * * TODO: Chrome can and will also kill tabs randomly now to save memory. They suggest apps store state in IDB and * restore it on load instead (there's a flag you can read to see if you were killed automatically). */ function useVisibilityRefresh() { const { refreshProfileOnVisible } = useSelector(globalSettingsSelector); const hasSearchQuery = useSelector(hasSearchQuerySelector); const { pathname } = useLocation(); const onOptimizerPage = pathname.endsWith('/optimizer'); useEffect(() => { const visibilityHandler = () => { if (!document.hidden) { if (refreshProfileOnVisible) { triggerTryRefresh(); } } else if ( dimNeedsUpdate$.getCurrentValue() && // Don't wipe out a user's in-progress search !hasSearchQuery && // Loadout optimizer is all about state, don't reload it !onOptimizerPage && // If a sheet is up, the user is doing something. We check sheetsOpen here, because it is not reactive! sheetsOpen.open <= 0 ) { // Sneaky updates - if DIM is hidden and needs an update, do the update. reloadDIM(); } }; document.addEventListener('visibilitychange', visibilityHandler); return () => { document.removeEventListener('visibilitychange', visibilityHandler); }; }, [hasSearchQuery, onOptimizerPage, refreshProfileOnVisible]); } ================================================ FILE: src/app/dim-ui/BungieImage.tsx ================================================ import clsx from 'clsx'; import React, { memo } from 'react'; /** * A relative path to a Bungie.net image asset. */ export type BungieImagePath = string; export type BungieImageProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'onClick'> & { src: BungieImagePath; }; /** * An image tag that links its src to bungie.net. Other props pass through to the underlying image. */ export default memo(function BungieImage(props: BungieImageProps) { const { src, ...otherProps } = props; return ( <img src={bungieNetPath(src)} loading="lazy" {...otherProps} className={clsx(otherProps.className, 'no-pointer-events')} /> ); }); /** * Produce a style object that sets the background image to an image on bungie.net. */ export function bungieBackgroundStyle(src: BungieImagePath) { return { backgroundImage: `url("${bungieNetPath(src)}")`, }; } /** * Produce a style object that sets the background image to an image on bungie.net. */ export function bungieBackgroundStyles(src: BungieImagePath[]) { if (src.length === 0) { return {}; } return { backgroundImage: src.map((src) => `url("${bungieNetPath(src)}")`).join(', '), }; } /** * Produce a style object that sets the background image to an image on bungie.net. * * Has extra settings because sometimes life throws bad CSS choices your way */ export function bungieBackgroundStyleAdvanced(src: BungieImagePath, stacks = 1) { const backgrounds = Array(stacks).fill(`url("${bungieNetPath(src)}")`); return { backgroundImage: backgrounds.join(', '), }; } /** * Expand a relative bungie.net asset path to a full path. */ export function bungieNetPath(src: BungieImagePath): string { if (!src) { return ''; } if (src.startsWith('~')) { return src.substr(1); } return `https://www.bungie.net${src}`; } ================================================ FILE: src/app/dim-ui/CharacterSelect.m.scss ================================================ @use '../variables.scss' as *; .tile { height: $emblem-height; @include phone-portrait { min-width: $emblem-width; > div { margin: 0 5px; } } } .vertical { composes: flexColumn from './common.m.scss'; gap: 8px; } // The > * > * is so we don't dim out the hover border .unselected > * > * { filter: grayscale(0.6); opacity: 0.4; } .frame { max-width: 250px; margin: 8px auto; overflow: visible !important; position: relative; @include phone-portrait { max-width: 260px; } } .track { display: block; // Don't let the browser handle touches, we'll do it ourselves touch-action: none; > * { display: inline-block; vertical-align: top; } } ================================================ FILE: src/app/dim-ui/CharacterSelect.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'frame': string; 'tile': string; 'track': string; 'unselected': string; 'vertical': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/CharacterSelect.tsx ================================================ import { hideItemPopup } from 'app/item-popup/item-popup'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { infoLog } from 'app/utils/log'; import clsx from 'clsx'; import { clamp } from 'es-toolkit'; import { animate, motion, PanInfo, Transition, useMotionValue, useTransform } from 'motion/react'; import { useEffect, useRef } from 'react'; import CharacterTileButton from '../character-tile/CharacterTileButton'; import { DimStore } from '../inventory/store-types'; import * as styles from './CharacterSelect.m.scss'; const spring: Transition<number> = { type: 'spring', stiffness: 100, damping: 20, mass: 1, restSpeed: 0.01, restDelta: 0.01, }; /** * The swipable header for selecting from a list of characters. * * This is currently a copy/paste of PhoneStoresHeader once both are done, if they are still similar, recombine them. */ export default function CharacterSelect({ stores, selectedStore, onCharacterChanged, }: { stores: DimStore[]; selectedStore: DimStore; onCharacterChanged: (storeId: string) => void; }) { const isPhonePortrait = useIsPhonePortrait(); stores = stores.filter((s) => !s.isVault); if (!isPhonePortrait) { return ( <ListCharacterSelect stores={stores} selectedStore={selectedStore} onCharacterChanged={onCharacterChanged} /> ); } return ( <SwipableCharacterSelect stores={stores} selectedStore={selectedStore} onCharacterChanged={onCharacterChanged} /> ); } function ListCharacterSelect({ stores, selectedStore, onCharacterChanged, }: { stores: DimStore[]; selectedStore: DimStore; onCharacterChanged: (storeId: string) => void; }) { return ( <div className={styles.vertical}> {stores.map((store) => ( <div key={store.id} className={clsx(styles.tile, { [styles.unselected]: store.id !== selectedStore.id, })} > <CharacterTileButton character={store} onClick={onCharacterChanged} /> </div> ))} </div> ); } function SwipableCharacterSelect({ stores, selectedStore, onCharacterChanged, }: { stores: DimStore[]; selectedStore: DimStore; onCharacterChanged: (storeId: string) => void; }) { const onIndexChanged = (index: number) => { onCharacterChanged(stores[index].id); hideItemPopup(); }; // TODO: carousel // TODO: wrap StoreHeading in a div? // TODO: optional external motion control const index = stores.indexOf(selectedStore); const trackRef = useRef<HTMLDivElement>(null); // The track is divided into "segments", with one item per segment const numSegments = stores.length; // This is a floating-point, animated representation of the position within the segments! const offset = useMotionValue(index); // Keep track of the starting point when we begin a gesture const startOffset = useRef<number>(0); useEffect(() => { const index = stores.indexOf(selectedStore); animate(offset, index, spring); }, [selectedStore, offset, stores]); // We want a bit more control than Framer Motion's drag gesture can give us, so fall // back to the pan gesture and implement our own elasticity, etc. const onPanStart = () => { startOffset.current = offset.get(); }; const onPan = (_e: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { if (!trackRef.current) { return; } const trackWidth = trackRef.current.clientWidth; // The offset as a proportion of segments let newValue = startOffset.current + -info.offset.x / (trackWidth / numSegments); // Apply elasticity outside the extents const elasticity = 0.5; const minExtent = 0; const maxExtent = numSegments - 1; if (newValue < minExtent) { newValue = elasticity * newValue; } else if (newValue > maxExtent) { newValue = elasticity * (newValue - maxExtent) + maxExtent; } offset.set(newValue); }; const onPanEnd = (_e: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { if (!trackRef.current) { return; } // Animate to one of the settled whole-number indexes let newIndex = clamp(Math.round(offset.get()), 0, numSegments - 1); const scale = trackRef.current.clientWidth / numSegments; if (index === newIndex) { const swipe = (info.velocity.x * info.offset.x) / (scale * scale); infoLog('swipe', swipe); if (swipe > 0.05) { const direction = -Math.sign(info.velocity.x); newIndex = clamp(newIndex + direction, 0, numSegments - 1); } } animate(offset, newIndex, spring); if (index !== newIndex) { onIndexChanged(newIndex); } }; // Transform the segment-relative offset back into pixels const offsetPercent = useTransform(offset, (o) => trackRef.current ? (trackRef.current.clientWidth / numSegments) * -o : 0, ); return ( <div className={styles.frame}> <motion.div ref={trackRef} className={styles.track} onPanStart={onPanStart} onPan={onPan} onPanEnd={onPanEnd} style={{ width: `${100 * stores.length}%`, x: offsetPercent }} > {stores.map((store) => ( <div key={store.id} style={{ width: `${100 / stores.length}%` }} className={clsx(styles.tile, { [styles.unselected]: store.id !== selectedStore.id, })} > <CharacterTileButton character={store} onClick={onCharacterChanged} /> </div> ))} </motion.div> </div> ); } ================================================ FILE: src/app/dim-ui/CheckButton.m.scss ================================================ @use '../variables.scss' as *; // TODO: Move to common button library! .checkButton { composes: dim-button from global; display: flex; flex-direction: row; align-items: center; box-sizing: border-box; padding: 2px 4px 2px 10px; @include phone-portrait { padding: 6px 10px 6px 16px; } // Don't do hover @include interactive($hover: true, $active: true) { background-color: rgb(255, 255, 255, 0.2) !important; color: var(--theme-text) !important; } > *:nth-last-child(n + 2) { margin-right: auto; } > *:last-child { margin-left: 6px; flex-shrink: 0; } } ================================================ FILE: src/app/dim-ui/CheckButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'checkButton': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/CheckButton.tsx ================================================ import clsx from 'clsx'; import React from 'react'; import * as styles from './CheckButton.m.scss'; import Switch from './Switch'; export default function CheckButton({ name, onChange, className, checked, children, }: { name: string; checked: boolean; className?: string; children: React.ReactNode; onChange: (checked: boolean) => void; }) { return ( <label className={clsx(styles.checkButton, className)}> <span>{children}</span> <Switch name={name} checked={checked} onChange={(checked) => onChange(checked)} /> </label> ); } ================================================ FILE: src/app/dim-ui/ClassIcon.tsx ================================================ import { AppIcon, globeIcon, hunterIcon, titanIcon, warlockIcon } from 'app/shell/icons'; import dimHunterProportionalIcon from 'app/shell/icons/custom/HunterProportional'; import dimTitanProportionalIcon from 'app/shell/icons/custom/TitanProportional'; import dimWarlockProportionalIcon from 'app/shell/icons/custom/WarlockProportional'; import { DestinyClass } from 'bungie-api-ts/destiny2'; const classIcons = { [DestinyClass.Hunter]: hunterIcon, [DestinyClass.Titan]: titanIcon, [DestinyClass.Warlock]: warlockIcon, [DestinyClass.Unknown]: globeIcon, [DestinyClass.Classified]: globeIcon, } as const; const classIconsProportional = { [DestinyClass.Hunter]: dimHunterProportionalIcon, [DestinyClass.Titan]: dimTitanProportionalIcon, [DestinyClass.Warlock]: dimWarlockProportionalIcon, [DestinyClass.Unknown]: globeIcon, [DestinyClass.Classified]: globeIcon, } as const; /** * Displays a class icon given a class type. */ export default function ClassIcon({ classType, proportional, className, }: { classType: DestinyClass; proportional?: boolean; className?: string; }) { return ( <AppIcon icon={(proportional ? classIconsProportional : classIcons)[classType]} className={className} /> ); } ================================================ FILE: src/app/dim-ui/ClickOutside.tsx ================================================ import { useEventBusListener } from 'app/utils/hooks'; import { EventBus } from 'app/utils/observable'; import React, { createContext, use, useCallback, useEffect, useRef } from 'react'; export const ClickOutsideContext = createContext(new EventBus<React.MouseEvent>()); /** * Component that fires an event if you click or tap outside of it. * * This uses a parent element that's connected through context so we can continue to work within the * React DOM hierarchy rather than the real one. This is important for things like sheets * spawned through portals from the item popup. */ export default function ClickOutside({ onClickOutside, children, extraRef, onClick, ref, ...other }: React.HTMLAttributes<HTMLDivElement> & { children: React.ReactNode; /** An optional second ref that will be excluded from being considered "outside". This is good for preventing the triggering button from double-counting clicks. */ extraRef?: React.RefObject<HTMLElement | null>; onClickOutside: (event: React.MouseEvent | MouseEvent) => void; ref?: React.Ref<HTMLDivElement>; }) { const localRef = useRef<HTMLDivElement>(null); if (ref && !('current' in ref)) { throw new Error('only works with a ref object'); } const wrapperRef = ref || localRef; const mouseEvents = use(ClickOutsideContext); /** * Alert if clicked on outside of element */ const handleClickOutside = useCallback( (event: React.MouseEvent) => { const target = event.target as Node; if ( wrapperRef.current && !wrapperRef.current.contains(target) && !extraRef?.current?.contains(target) ) { onClickOutside(event); } }, [onClickOutside, wrapperRef, extraRef], ); useEventBusListener(mouseEvents, handleClickOutside); // Handle clicks directly on the body as always outside. This handles the case where the ClickoutsideRoot doesn't cover the whole screen. useEffect(() => { const handler = (e: MouseEvent) => { if (e.target === document.body) { onClickOutside(e); } }; document.addEventListener('click', handler); return () => document.removeEventListener('click', handler); }); return ( <div ref={wrapperRef} {...other}> {children} </div> ); } ================================================ FILE: src/app/dim-ui/ClickOutsideRoot.tsx ================================================ import { EventBus } from 'app/utils/observable'; import React, { useState } from 'react'; import { ClickOutsideContext } from './ClickOutside'; /** * The root element that lets ClickOutside work. This defines the * "Outside" for any ClickOutside children. * * This uses a parent element that's connected through context so we can continue to work within the * React DOM hierarchy rather than the real one. This is important for things like sheets * spawned through portals from the item popup. */ export default function ClickOutsideRoot({ children, className, }: { children: React.ReactNode; className?: string; }) { const [clickOutsideSubject] = useState(() => new EventBus<React.MouseEvent>()); const onClick = (e: React.MouseEvent) => { clickOutsideSubject.next(e); }; return ( <ClickOutsideContext value={clickOutsideSubject}> <div className={className} onClick={onClick}> {children} </div> </ClickOutsideContext> ); } ================================================ FILE: src/app/dim-ui/ClosableContainer.m.scss ================================================ @use '../variables' as *; .container { position: relative; } .close { z-index: 1; width: calc(var(--item-size) / 3); height: calc(var(--item-size) / 3); background-size: calc(var(--item-size) / 3); display: none; position: absolute; top: 2px; right: 2px; background-image: url('images/close.png'); background-color: rgb(100, 100, 100, 0.8); @include interactive($hover: true, $focus: true) { background-color: var(--theme-accent-primary); outline: none; } // This intentionally doesn't use the interactive mixin because it relies on // hover emulation to show the button on mobile. .container:hover & { display: inline-block; } } ================================================ FILE: src/app/dim-ui/ClosableContainer.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'close': string; 'container': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/ClosableContainer.tsx ================================================ import clsx from 'clsx'; import React from 'react'; import * as styles from './ClosableContainer.m.scss'; /** * A generic wrapper that adds a "close" button in the top right corner. * If the onClose function isn't passed in the close button won't appear * allowing dynamic enable/disable functionality. */ export default function ClosableContainer({ children, className, onClose, }: { children: React.ReactNode; className?: string; onClose?: (e: React.MouseEvent) => void; }) { return ( <div className={clsx(className, styles.container)}> {children} {Boolean(onClose) && ( <div className={styles.close} onClick={onClose} role="button" tabIndex={0} /> )} </div> ); } ================================================ FILE: src/app/dim-ui/CollapsibleTitle.m.scss ================================================ @use '../variables.scss' as *; // The wrapping H3 element .title { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-top: 0; margin-bottom: 0; background-color: rgb(0, 0, 0, 0.2); padding-right: 16px; gap: 8px; @include interactive($hover: true, $focusWithin: true) { background-color: rgb(0, 0, 0, 0.4); } // The interactive button within > button { // Reset button color: inherit; appearance: none; background: transparent; border: 0; margin: 0; cursor: pointer; font-family: inherit; text-align: left; display: flex; flex-direction: row; align-items: center; flex: 1; padding: 0 0 0 12px; min-height: 34px; gap: 6px; text-transform: uppercase; letter-spacing: 2px; font-size: 14px; @include interactive($hover: true, $focus: true) { color: var(--theme-accent-primary); @include phone-portrait { color: var(--theme-text); background-color: transparent; } } } /* stylelint-disable-next-line no-descending-specificity */ &.collapsed { background-color: rgb(0, 0, 0, 0.4); @include phone-portrait { color: var(--theme-text); } } &.disabled { color: #888; > button { color: #888 !important; cursor: default; } } } .collapseIcon { font-size: 16px; width: 10px; transition: transform 0.1s ease-in-out; line-height: 8px; &.iconCollapsed { transform: rotate(-90deg); } } .content { overflow: hidden; } ================================================ FILE: src/app/dim-ui/CollapsibleTitle.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'collapseIcon': string; 'collapsed': string; 'content': string; 'disabled': string; 'iconCollapsed': string; 'title': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/CollapsibleTitle.tsx ================================================ import { collapsedSelector } from 'app/dim-api/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import clsx from 'clsx'; import { AnimatePresence, Transition, Variants, motion } from 'motion/react'; import React, { useCallback, useEffect, useId, useRef } from 'react'; import { useSelector } from 'react-redux'; import { toggleCollapsedSection } from '../settings/actions'; import { AppIcon, collapseIcon } from '../shell/icons'; import * as styles from './CollapsibleTitle.m.scss'; import ErrorBoundary from './ErrorBoundary'; export function Title({ title, collapsed, extra, showExtraOnlyWhenCollapsed, className, disabled, style, headerId, contentId, onClick, ref, }: { headerId: string; contentId: string; collapsed: boolean; title: React.ReactNode; /** right-aligned content that's in the title bar, but isn't the title */ extra?: React.ReactNode; /** if true, the `extra` content shows up only when this section is collapsed */ showExtraOnlyWhenCollapsed?: boolean; /** if true, this section is forced closed and ignores clicks */ disabled?: boolean; style?: React.CSSProperties; className?: string; onClick: () => void; ref?: React.Ref<HTMLHeadingElement>; }) { return ( <h3 className={clsx(styles.title, className, { [styles.collapsed]: collapsed, [styles.disabled]: disabled, })} style={style} ref={ref} > <button type="button" aria-expanded={!collapsed} aria-controls={contentId} onClick={onClick} disabled={disabled} id={headerId} > {!disabled && <CollapseIcon collapsed={collapsed} />} {title} </button> {showExtraOnlyWhenCollapsed ? collapsed && extra : extra} </h3> ); } export default function CollapsibleTitle({ title, defaultCollapsed, children, extra, showExtraOnlyWhenCollapsed, className, disabled, sectionId, style, }: { sectionId: string; defaultCollapsed?: boolean; title: React.ReactNode; /** right-aligned content that's in the title bar, but isn't the title */ extra?: React.ReactNode; /** if true, the `extra` content shows up only when this section is collapsed */ showExtraOnlyWhenCollapsed?: boolean; /** if true, this section is forced closed and ignores clicks */ disabled?: boolean; children?: React.ReactNode; style?: React.CSSProperties; className?: string; }) { const dispatch = useThunkDispatch(); const collapsedSetting = useSelector(collapsedSelector(sectionId)); const collapsed = Boolean(disabled) || (collapsedSetting ?? Boolean(defaultCollapsed)); const toggle = useCallback( () => dispatch(toggleCollapsedSection(sectionId)), [dispatch, sectionId], ); const id = useId(); const contentId = `content-${id}`; const headerId = `header-${id}`; return ( <> <Title title={title} collapsed={collapsed} extra={extra} showExtraOnlyWhenCollapsed={showExtraOnlyWhenCollapsed} className={className} disabled={disabled} style={style} headerId={headerId} contentId={contentId} onClick={toggle} /> <CollapsedSection collapsed={collapsed} headerId={headerId} contentId={contentId}> <ErrorBoundary name={`collapse-${sectionId}`} key={contentId}> {children} </ErrorBoundary> </CollapsedSection> </> ); } export function CollapseIcon({ collapsed }: { collapsed: boolean }) { return ( <AppIcon className={clsx(styles.collapseIcon, { [styles.iconCollapsed]: collapsed })} icon={collapseIcon} ariaHidden /> ); } const collapsibleTitleAnimateVariants: Variants = { open: { height: 'auto' }, collapsed: { height: 0 }, }; const collapsibleTitleAnimateTransition: Transition<number> = { type: 'spring', duration: 0.5, bounce: 0, }; export function CollapsedSection({ collapsed, children, headerId, contentId, }: { collapsed: boolean; children: React.ReactNode; headerId: string; contentId: string; }) { const initialMount = useRef(true); useEffect(() => { initialMount.current = false; }, [initialMount]); return ( <AnimatePresence> {!collapsed && ( <motion.div id={contentId} aria-labelledby={headerId} key="content" initial={initialMount.current ? false : 'collapsed'} animate="open" exit="collapsed" variants={collapsibleTitleAnimateVariants} transition={collapsibleTitleAnimateTransition} className={styles.content} > {children} </motion.div> )} </AnimatePresence> ); } ================================================ FILE: src/app/dim-ui/ConfirmButton.m.scss ================================================ @use '../variables.scss' as *; .confirmButton { display: inline-block; overflow: hidden; max-width: 100%; // keep flexed things from trying to extend past the edges & div, & span { max-width: 100%; overflow: hidden; } // applies to both the icon, children container and the "confirm" message container & > div { // while transitioning out of confirm mode, linger the message, then sweep it away slowly transition: height 0.5s ease-in 0.5s; overflow: hidden; display: flex; align-items: center; justify-content: center; } &.confirmMode { color: var(--theme-text) !important; @include interactive($hover: true) { background-color: #af7d27 !important; } // as elements transition into confirm mode, snap confirm message into place quickly & > div { transition: height 0.1s; } &[class~='danger'] { @include interactive($hover: true) { background-color: #a22 !important; } } } } ================================================ FILE: src/app/dim-ui/ConfirmButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'confirmButton': string; 'confirmMode': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/ConfirmButton.tsx ================================================ import { t } from 'app/i18next-t'; import clsx from 'clsx'; import React, { useEffect, useRef, useState } from 'react'; import * as styles from './ConfirmButton.m.scss'; /** * a button that requests confirmation, and requires a second * click before it runs the provided onClick function * * this uses a goofy height transition to switch between two * different contents (normal content, and confirm message), * so please ensure the provided child content is a single line */ export function ConfirmButton({ /** apply "danger" styling, for destructive actions like deletion */ danger, /** this will be executed once the users confirms the action */ onClick, className, /** button content. confine this to 1 text line and 1 line-height */ children, }: React.PropsWithChildren<{ danger?: boolean; onClick: () => void; className?: string }>) { // controls whether the button is in "ask for confirmation" state const [confirmMode, setConfirmMode] = useState(false); // controls whether the button is ready to submit the requested function // (available 100ms after "ask for confirmation" state) const [confirmReady, setConfirmReady] = useState(false); const [contentHeight, setContentHeight] = useState(0); const [containerHeight, setContainerHeight] = useState(0); const containerRef = useRef<HTMLButtonElement>(null); const childrenRef = useRef<HTMLDivElement>(null); useEffect(() => { setContentHeight(childrenRef.current?.offsetHeight || 0); setContainerHeight(containerRef.current?.offsetHeight || 0); }, []); const onClickAction = confirmMode && confirmReady ? () => { setConfirmMode(false); setConfirmReady(false); onClick(); } : () => { setConfirmMode(true); setTimeout(() => { setConfirmReady(true); }, 100); }; return ( <button key="save" type="button" className={clsx('dim-button', className, styles.confirmButton, { [styles.confirmMode]: confirmMode, danger, })} ref={containerRef} onClick={onClickAction} onMouseLeave={() => { setConfirmMode(false); setConfirmReady(false); }} style={{ height: containerHeight || 'auto' }} > <div style={{ height: confirmMode ? 0 : contentHeight || 'auto' }} ref={childrenRef}> {children} </div> <div style={{ height: confirmMode ? contentHeight : 0 }}>{t('General.Confirm')}</div> </button> ); } ================================================ FILE: src/app/dim-ui/Countdown.tsx ================================================ import { i15dDurationFromMs } from 'app/utils/time'; import { useEffect, useState } from 'react'; /** * Render a countdown to a specific date. */ export default function Countdown({ endTime, compact, className, }: { endTime: Date; /** Render the time as a compact string instead of spelled out */ compact?: boolean; className?: string; }) { const [diff, setDiff] = useState(endTime.getTime() - Date.now()); useEffect(() => { let interval = 0; const update = () => { const diff = endTime.getTime() - Date.now(); // We set the diff just to make it re-render. We could just as easily set this to now(), or an incrementing number setDiff(diff); if (diff <= 0) { clearInterval(interval); } }; interval = window.setInterval(update, 60000); update(); return () => clearInterval(interval); }, [endTime]); return ( <time dateTime={endTime.toISOString()} className={className} title={endTime.toLocaleString()}> {i15dDurationFromMs(diff, compact)} </time> ); } ================================================ FILE: src/app/dim-ui/CustomStatTotal.m.scss ================================================ .inlineStatIcon { cursor: pointer; display: inline-block; img { display: inline-block; vertical-align: bottom; height: 1.5em; } } .divider::after { opacity: 0; content: '+'; } .activeStatLabels { .divider::after { opacity: 1; } } .inactiveStatLabels { img { opacity: 0.4; } &.readOnly { display: none; } &:not(:empty) { &::before { content: '('; } &::after { content: ')'; } } } ================================================ FILE: src/app/dim-ui/CustomStatTotal.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'activeStatLabels': string; 'divider': string; 'inactiveStatLabels': string; 'inlineStatIcon': string; 'readOnly': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/CustomStatTotal.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import { useD2Definitions } from 'app/manifest/selectors'; import { armorStats } from 'app/search/d2-known-values'; import { useSetting } from 'app/settings/hooks'; import { addDividers } from 'app/utils/react'; import { DestinyClass, DestinyStatDefinition } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import * as styles from './CustomStatTotal.m.scss'; export type StatHashListsKeyedByDestinyClass = Record<number, number[]>; export function StatTotalToggle({ forClass = DestinyClass.Unknown, readOnly = false, className, }: { className?: string; forClass?: DestinyClass; readOnly?: boolean; }) { const defs = useD2Definitions(); const [customTotalStatsByClass, setCustomTotalStatsByClass] = useSetting('customTotalStatsByClass'); const toggleStat = (statHash: number) => { setCustomTotalStatsByClass({ ...customTotalStatsByClass, ...{ [forClass]: toggleArrayElement(statHash, customTotalStatsByClass[forClass] ?? []), }, }); }; const activeStats = customTotalStatsByClass[forClass]?.length ? customTotalStatsByClass[forClass] : []; if (!defs) { return null; } return ( <div className={clsx(className)}> {addDividers( [ { className: styles.activeStatLabels, includesCheck: true }, { className: styles.inactiveStatLabels, includesCheck: false }, ].map(({ className, includesCheck }) => ( <span key={className} className={clsx(className, { [styles.readOnly]: readOnly })}> {addDividers( armorStats .filter((statHash) => activeStats.includes(statHash) === includesCheck) .map((statHash) => ( <StatToggleButton key={statHash} stat={defs.Stat.get(statHash)} toggleStat={toggleStat} readOnly={readOnly} /> )), <span className={styles.divider} />, )} </span> )), <span className={styles.divider} />, )} </div> ); } /** * this check shouldn't be necessary :| * maybe it isn't if we're just hardcoding armor stats */ function StatToggleButton({ stat, toggleStat, readOnly = false, }: { stat: DestinyStatDefinition; toggleStat: (statHash: number) => void; readOnly: boolean; }) { return ( <span onClick={ !readOnly ? (e) => { e.stopPropagation(); toggleStat(stat.hash); } : undefined } role="button" > {stat.displayProperties.hasIcon ? ( <span title={stat.displayProperties.name} className={styles.inlineStatIcon}> <BungieImage src={stat.displayProperties.icon} /> </span> ) : ( stat.displayProperties.name )} </span> ); } /** adds missing, or removes existing, element in arr */ function toggleArrayElement<T>(element: T, arr: T[]) { return arr.includes(element) ? arr.filter((v) => v !== element) : arr.concat(element); } ================================================ FILE: src/app/dim-ui/CustomStatWeights.m.scss ================================================ .statWeightRow { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; gap: 2px; & > * { display: flex; align-items: center; justify-content: center; } .divider::after { content: '+'; opacity: 0.7; } img { height: 1.5em; } } ================================================ FILE: src/app/dim-ui/CustomStatWeights.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'divider': string; 'statWeightRow': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/CustomStatWeights.tsx ================================================ import { CustomStatDef } from '@destinyitemmanager/dim-api-types'; import { customStatsSelector } from 'app/dim-api/selectors'; import BungieImage from 'app/dim-ui/BungieImage'; import { useD2Definitions } from 'app/manifest/selectors'; import { armorStats } from 'app/search/d2-known-values'; import { filterMap } from 'app/utils/collections'; import { addDividers } from 'app/utils/react'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; import * as styles from './CustomStatWeights.m.scss'; export function CustomStatWeightsFromHash({ customStatHash, className, }: { customStatHash: number; className?: string; }) { const customStatsList = useSelector(customStatsSelector); const customStat = customStatsList.find((c) => c.statHash === customStatHash); if (!customStat) { return null; } return <CustomStatWeightsDisplay className={className} customStat={customStat} />; } /** * displays the up-to-six stats a custom stat total is comprised of. * if the weights are only 0 or 1, it'll just be icons. * if some weights are above 1, the stat icons will also include numeric weights. */ export function CustomStatWeightsDisplay({ customStat, className, singleStatClass, }: { customStat: CustomStatDef; className?: string; singleStatClass?: string; }) { const defs = useD2Definitions()!; // if true, this stat is only include/exclude, no weighting const binaryWeights = Object.values(customStat.weights).every((v) => v === 1 || v === 0); return ( <div className={clsx(styles.statWeightRow, className)}> {addDividers( filterMap(armorStats, (statHash) => { const stat = defs.Stat.get(statHash); const weight = customStat.weights[statHash] || 0; if (!weight) { return undefined; } return ( <span key={statHash} title={stat.displayProperties.name} className={singleStatClass}> <BungieImage className="stat-icon" title={stat.displayProperties.name} src={stat.displayProperties.icon} /> {!binaryWeights && <span>{weight}</span>} </span> ); }), <span className={styles.divider} />, )} </div> ); } ================================================ FILE: src/app/dim-ui/DestinyTooltipText.m.scss ================================================ @use '../variables.scss' as *; .shapedIcon { vertical-align: middle; margin-right: 0.5em; margin-bottom: 4px; height: 12px; width: 12px; color: $shaped; } .seasonalExpiration { color: rgb(218, 149, 45); :global(.app-icon) { margin-right: 4px; } } ================================================ FILE: src/app/dim-ui/DestinyTooltipText.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'seasonalExpiration': string; 'shapedIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/DestinyTooltipText.tsx ================================================ import { DimItem } from 'app/inventory/item-types'; import { AppIcon, faClock, shapedIcon } from 'app/shell/icons'; import { DestinyItemTooltipNotification } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import * as styles from './DestinyTooltipText.m.scss'; import RichDestinyText from './destiny-symbols/RichDestinyText'; export function DestinyTooltipText({ item }: { item: DimItem }) { if (!item.tooltipNotifications) { return null; } return ( <> {item.tooltipNotifications .filter((tip) => !isEnhancementTooltip(item, tip)) .map((tip) => ( <div key={tip.displayString} className={clsx('quest-expiration item-details', { [styles.seasonalExpiration]: isExpirationTooltip(tip), })} > {isExpirationTooltip(tip) && <AppIcon icon={faClock} />} {isPatternTooltip(tip) && <AppIcon className={styles.shapedIcon} icon={shapedIcon} />} <RichDestinyText text={tip.displayString} ownerId={item.vendor?.characterId ?? item.owner} /> </div> ))} </> ); } function isExpirationTooltip(tip: DestinyItemTooltipNotification) { return tip.displayStyle.endsWith('_expiration') || tip.displayStyle.endsWith('_seasonal'); } function isPatternTooltip(tip: DestinyItemTooltipNotification) { return tip.displayStyle === 'ui_display_style_deepsight'; } function isEnhancementTooltip(item: DimItem, tip: DestinyItemTooltipNotification) { return ( tip.displayStyle === 'ui_display_style_crafting' || // assume weapons with this tooltip style are non-enhanced weapons offering enhancement (tip.displayStyle === 'ui_display_style_info' && item.itemCategoryHashes?.includes(ItemCategoryHashes.Weapon)) ); } ================================================ FILE: src/app/dim-ui/DiamondProgress.m.scss ================================================ @use '../variables' as *; .level { box-sizing: border-box; border-radius: calc((var(--item-size) / 6)); min-width: calc((var(--item-size) / 3)); height: calc((var(--item-size) / 3)); font-size: calc(#{$badge-font-size}); padding: 0 0.2em; display: flex; align-items: center; justify-content: center; position: absolute; bottom: calc((var(--item-size) / -12)); right: calc((var(--item-size) / -12)); background-color: #ddd; color: black; } ================================================ FILE: src/app/dim-ui/DiamondProgress.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'level': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/DiamondProgress.tsx ================================================ import * as styles from './DiamondProgress.m.scss'; interface Props { /** 0-1 progress for the outer ring */ progress: number; /** Level to display */ level?: number; /** The icon to use */ icon: string; className?: string; } /** * A diamond-shaped progress bar (from faction icons). */ export default function DiamondProgress({ progress, level, icon, className }: Props) { const style = { strokeDashoffset: 121.622368 - 121.622368 * progress, }; return ( <div className={className}> <svg viewBox="0 0 48 48"> <image xlinkHref={icon} width="48" height="48" /> {progress > 0 && ( <polygon strokeDasharray="121.622368" style={style} fillOpacity="0" stroke="#FFF" strokeWidth="3" points="24,2.5 45.5,24 24,45.5 2.5,24" strokeLinecap="butt" /> )} </svg> {level !== undefined && <div className={styles.level}>{level}</div>} </div> ); } ================================================ FILE: src/app/dim-ui/Dropdown.m.scss ================================================ @use '../variables.scss' as *; .button { composes: dim-button from global; display: block; } .menu { position: absolute; background: var(--theme-dropdown-menu-bg); color: var(--theme-text); font-size: 12px; z-index: 100; } .menuItem { display: flex; flex-direction: row; align-items: center; min-width: 10em; padding: 6px 9px; white-space: nowrap; @include phone-portrait { padding: 8px 16px; font-size: 14px; } :global(.app-icon) { color: var(--theme-text) !important; width: 16px; text-align: center; margin-right: 4px; font-size: 12px !important; height: 12px; } img { width: 16px; height: 16px; margin-right: 4px; } } .highlighted { background-color: var(--theme-accent-primary); color: var(--theme-text-invert) !important; :global(.app-icon) { color: var(--theme-text-invert) !important; } } .disabled { color: #999 !important; :global(.app-icon) { color: #999 !important; } &.highlighted { background-color: transparent; } } .kebabButton { composes: resetButton from './common.m.scss'; padding: 4px 8px; color: #999; @include interactive($hover: true, $active: true, $focus: true) { color: white; } } .separator { height: 1px; background-color: #555; } .arrow { font-size: 10px !important; width: 10px !important; height: 10px !important; margin-left: 6px; } ================================================ FILE: src/app/dim-ui/Dropdown.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'arrow': string; 'button': string; 'disabled': string; 'highlighted': string; 'kebabButton': string; 'menu': string; 'menuItem': string; 'separator': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/Dropdown.tsx ================================================ import { Placement } from '@popperjs/core'; import { expandDownIcon, kebabIcon } from 'app/shell/icons'; import AppIcon from 'app/shell/icons/AppIcon'; import clsx from 'clsx'; import { useSelect } from 'downshift'; import { ReactNode, useRef } from 'react'; import * as styles from './Dropdown.m.scss'; import { usePopper } from './usePopper'; interface Separator { key: string; } interface DropdownOption { key: string; content: ReactNode; disabled?: boolean; onSelected: () => void; } export type Option = Separator | DropdownOption; interface Props { /** The contents of the button */ children?: ReactNode; /** Kebab mode - just show a single kebab icon */ kebab?: boolean; className?: string; disabled?: boolean; options: Option[]; offset?: number; fixed?: boolean; placement?: Placement; label: string; } function isDropdownOption(option: Option): option is DropdownOption { return (option as DropdownOption).content !== undefined; } /** * A generic dropdown menu, triggered from a button, with a list of menu items * which can each trigger a command. No state is kept about the selected item - * use Select for that. * * @see Select for a single item selector * @see MultiSelect for multiple-item selector */ export default function Dropdown({ children, kebab, className, disabled, options: items, offset, fixed, placement = kebab ? 'bottom-end' : 'bottom-start', label, }: Props) { const { isOpen, getToggleButtonProps, getMenuProps, highlightedIndex, getItemProps, reset } = useSelect({ items, itemToString: (i) => i?.key || 'none', onSelectedItemChange: ({ selectedItem }) => { if (selectedItem && isDropdownOption(selectedItem) && !selectedItem.disabled) { selectedItem.onSelected(); } // Unselect to reset the state reset(); }, isItemDisabled: (item) => (isDropdownOption(item) ? Boolean(item.disabled) : true), }); const buttonRef = useRef<HTMLButtonElement>(null); const menuRef = useRef<HTMLDivElement>(null); usePopper( { contents: menuRef, reference: buttonRef, placement, offset, fixed, }, [isOpen, items], ); return ( <div className={className}> <button type="button" {...getToggleButtonProps({ ref: buttonRef, disabled, title: label })} className={kebab ? styles.kebabButton : styles.button} > {kebab ? ( <AppIcon icon={kebabIcon} /> ) : ( <> {children} <AppIcon icon={expandDownIcon} className={styles.arrow} /> </> )} </button> <div {...getMenuProps({ ref: menuRef, className: styles.menu })}> {isOpen && items.map((item, index) => !isDropdownOption(item) ? ( <div key={item.key} className={styles.separator} {...getItemProps({ item, index, })} /> ) : ( <div className={clsx(styles.menuItem, { [styles.highlighted]: highlightedIndex === index, [styles.disabled]: item.disabled, })} key={item.key} {...getItemProps({ item, index, })} > {item.content} </div> ), )} </div> </div> ); } ================================================ FILE: src/app/dim-ui/ElementIcon.m.scss ================================================ @use '../variables.scss' as *; .element { width: calc(#{dim-item-px(8)}); height: calc(#{dim-item-px(8)}); margin-right: 1px; background-size: 100%; background-repeat: no-repeat; display: inline-block; filter: saturate(2.5); // The D1 icons are a bit too small in the item badge, so we'll make them a // bit bigger &.d1Badge { width: calc(#{dim-item-px(10)}); height: calc(#{dim-item-px(10)}); } } ================================================ FILE: src/app/dim-ui/ElementIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'd1Badge': string; 'element': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/ElementIcon.tsx ================================================ import { useD2Definitions } from 'app/manifest/selectors'; import { DestinyDamageTypeDefinition } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { bungieBackgroundStyle } from './BungieImage'; import * as styles from './ElementIcon.m.scss'; export default function ElementIcon({ element, className, d1Badge, }: { element: DestinyDamageTypeDefinition | null; className?: string; d1Badge?: boolean; }) { if (!element) { return null; } const icon = element.displayProperties?.icon; if (!icon) { return null; } return ( <div style={bungieBackgroundStyle(icon)} title={element.displayProperties.name} className={clsx(className, styles.element, { [styles.d1Badge]: d1Badge })} /> ); } /** * The energy cost icon (a Masterwork hammer) */ export function EnergyCostIcon({ className }: { className?: string }) { const defs = useD2Definitions()!; const energyCostStat = defs.Stat.get(3578062600); // "Any Energy Type Cost" const icon = energyCostStat?.displayProperties.iconSequences[0].frames[3]; if (!icon) { return null; } return <div style={bungieBackgroundStyle(icon)} className={clsx(className, styles.element)} />; } ================================================ FILE: src/app/dim-ui/EnergyIncrements.m.scss ================================================ // <div class=`energyMeterIncrements small`> (or medium) // <div class=used/><div class=used/><div class=unused/><div class=unavailable/><div class=unavailable/> // </div> // // results: // ■ ■ ■ □ ▬ ▬ @use '../variables.scss' as *; .energyMeterIncrements { --cell-height: 2px; --cell-margin: 0.5px; --cell-border: 1px; composes: flexRow from './common.m.scss'; align-items: center; &.medium { --cell-height: 6px; --cell-margin: 1px; --cell-border: 3px; } // The actual cells > div { display: block; flex-grow: 1; padding: 0 var(--cell-margin); &[role='button'] { // Expand buttons a bit to make them easier to hover and click padding-top: 3px; padding-bottom: 3px; margin-top: -3px; margin-bottom: -3px; // Apply the "used" style to all cells up to and including the hovered // cell - this is a pure-CSS hover preview (we used to use React to do // this)! &:is(:has(~ *:hover), :hover)::before { border-color: white; height: var(--cell-height); margin-top: 0; margin-bottom: 0; } // And apply the "unavailable" style to all cells after the hovered cell &:hover ~ *::before { border-color: #888; height: 0; } } // Remove leading/trailing padding for the first/last cell &:first-child { padding-left: 0; } &:last-child { padding-right: 0; } // The actual boxes you see are these generated elements - this is so we can // eliminate the gap between items and then put them back with *padding* // (instead of margin). Padding still triggers hover, so this prevents the // hover effect from disappearing when you move the mouse between cells. &::before { content: ''; display: block; border: var(--cell-border) solid white; height: var(--cell-height); } &.used::before { background: white; } &.unavailable::before { border-color: #888; height: 0; margin-top: 3px; margin-bottom: 3px; } } } .costs { display: flex; flex-direction: row; gap: 6px; .cost { font-size: 12px !important; } } ================================================ FILE: src/app/dim-ui/EnergyIncrements.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'cost': string; 'costs': string; 'energyMeterIncrements': string; 'medium': string; 'unavailable': string; 'used': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/EnergyIncrements.tsx ================================================ import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { getEnergyUpgradeHashes, sumModCosts } from 'app/inventory/store/energy'; import { EnergySwap } from 'app/loadout-builder/generated-sets/GeneratedSetItem'; import { useD2Definitions } from 'app/manifest/selectors'; import { compareBy } from 'app/utils/comparators'; import Cost from 'app/vendors/Cost'; import clsx from 'clsx'; import * as styles from './EnergyIncrements.m.scss'; import { PressTip } from './PressTip'; // TODO special display for T10 -> T10 + exotic artifice? /** this accepts either an item, or a partial DimItem.energy */ function EnergyIncrements({ item, energy, }: | { item: DimItem; energy?: undefined } | { item?: undefined; energy: { energyCapacity: number; energyUsed: number; }; }) { const { energyCapacity, energyUsed } = item?.energy ?? energy!; return ( <EnergyMeterIncrements energyCapacity={energyCapacity} energyUsed={energyUsed} variant="small" /> ); } export function EnergyMeterIncrements({ energyCapacity, energyUsed, minCapacity, previewUpgrade, variant, }: { energyCapacity: number; energyUsed: number; minCapacity?: number; previewUpgrade?: (i: number) => void; variant: 'medium' | 'small'; }) { // This works because Tier 5 armor with 11 energy drops with all energy unlocked. const maxEnergyCapacity = Math.max(10, energyCapacity); // layer in possible total slots, then earned slots, then currently used slots const meterIncrements = Array<string | undefined>(maxEnergyCapacity) .fill(styles.unavailable) .fill(undefined, 0, energyCapacity) .fill(styles.used, 0, energyUsed); return ( <div className={clsx(styles.energyMeterIncrements, { [styles.medium]: variant === 'medium' })}> {meterIncrements.map((incrementStyle, i) => ( <div key={i} className={incrementStyle} role={minCapacity !== undefined && i + 1 > minCapacity ? 'button' : undefined} onClick={previewUpgrade ? () => previewUpgrade(i + 1) : undefined} /> ))} </div> ); } export function EnergyIncrementsWithPresstip({ energy, wrapperClass, item, }: { energy: { energyCapacity: number; energyUsed: number; }; wrapperClass?: string | undefined; item: DimItem; }) { const { energyCapacity, energyUsed } = energy; const energyUnused = Math.max(energyCapacity - energyUsed, 0); const defs = useD2Definitions()!; if (!item.energy) { return null; } const energyModHashes = getEnergyUpgradeHashes(item, energyUsed || 0); const costs = sumModCosts( defs, energyModHashes.map((h) => defs.InventoryItem.get(h)), ).sort(compareBy((c) => c.quantity)); return ( <PressTip tooltip={ <> {t('EnergyMeter.Energy')} <hr /> {t('EnergyMeter.Used')}: {energyUsed} <br /> {t('EnergyMeter.Unused')}: {energyUnused} {energyUsed > energyCapacity && ( <> <hr /> {t('EnergyMeter.UpgradeNeeded', energy)} </> )} {costs.length > 0 && ( <> <hr /> <div className={styles.costs}> <span>{t('Loadouts.ModPlacement.UpgradeCosts')}</span> {costs.map((cost) => ( <Cost key={cost.itemHash} cost={cost} className={styles.cost} /> ))} </div> </> )} </> } className={wrapperClass} > <EnergyIncrements energy={{ energyCapacity, energyUsed, }} /> {energyUsed > energyCapacity && <EnergySwap energy={energy} />} </PressTip> ); } ================================================ FILE: src/app/dim-ui/ErrorBoundary.tsx ================================================ import ErrorPanel from 'app/shell/ErrorPanel'; import { errorLog } from 'app/utils/log'; import React, { Component } from 'react'; import { reportException } from '../utils/sentry'; interface Props { name: string; children?: React.ReactNode; } interface State { error?: Error; } export default class ErrorBoundary extends Component<Props, State> { constructor(props: Props) { super(props); this.state = {}; } componentDidCatch(error: Error, errorInfo: { componentStack: string }) { const { name } = this.props; this.setState({ error }); errorLog(name, error, errorInfo); reportException(name, error, errorInfo); } componentDidUpdate(prevProps: Readonly<Props>): void { if (prevProps.name !== this.props.name) { this.setState({ error: undefined }); } } render() { const { error } = this.state; const { children } = this.props; if (error) { return <ErrorPanel error={error} />; } return children; } } ================================================ FILE: src/app/dim-ui/ExpandableTextBlock.m.scss ================================================ @use '../variables.scss' as *; .textBlockWrapper { position: relative; overflow: hidden; @include interactive($hover: true) { cursor: pointer; } &::after { content: ''; position: absolute; inset: 0; box-shadow: inset 0 -31px 20px -20px black; } &.open { @include interactive($hover: true) { cursor: default; } } &.open::after { display: none; } } ================================================ FILE: src/app/dim-ui/ExpandableTextBlock.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'open': string; 'textBlockWrapper': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/ExpandableTextBlock.tsx ================================================ import clsx from 'clsx'; import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import * as styles from './ExpandableTextBlock.m.scss'; /** * wrapped around some inline content, this crops to a specified number of lines * (with a fadeout) and allows the user to click it and show the rest * * @param linesWhenClosed an integer please. controls how many lines to collapse to * @param alreadyOpen allows a parent component to force it open */ export function ExpandableTextBlock({ children, linesWhenClosed, alreadyOpen, className, }: { children: React.ReactNode; linesWhenClosed: number; alreadyOpen?: boolean; className?: string; }) { const [isOpen, setOpen] = useState(alreadyOpen); const [closedHeight, setClosedHeight] = useState<number>(); const contentRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null); // measure the element height, with lines clamped, the first time this component exists useLayoutEffect(() => { setClosedHeight(contentRef.current!.clientHeight); }, []); // after the element has been measured, if the unclamped text still fits inside clamped height, // then clamping wasn't necessary. set isOpen to mark it as, effectively, already opened useEffect(() => { if (closedHeight && wrapperRef.current!.clientHeight >= contentRef.current!.clientHeight) { setOpen(true); } }, [closedHeight]); return ( <div className={clsx(className, styles.textBlockWrapper, { [styles.open]: isOpen })} ref={wrapperRef} onClick={() => setOpen(true)} style={{ height: isOpen ? 'max-content' : closedHeight, overflow: 'hidden' }} > <div ref={contentRef} style={ closedHeight ? undefined : { WebkitLineClamp: linesWhenClosed, WebkitBoxOrient: 'vertical', display: '-webkit-box', } } > {children} </div> </div> ); } ================================================ FILE: src/app/dim-ui/ExternalLink.tsx ================================================ import React from 'react'; export default function ExternalLink({ href, children, ...props }: { href: string; children: React.ReactNode; } & Partial< React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> >) { return ( <a target="_blank" rel="noopener noreferrer" href={href} {...props}> {children} </a> ); } ================================================ FILE: src/app/dim-ui/FileUpload.m.scss ================================================ .fileInput { composes: flexColumn from './common.m.scss'; align-items: center; text-align: center; padding: 8px; border: 1px dashed #999; width: 100%; box-sizing: border-box; gap: 4px; cursor: pointer; } .instructions { font-weight: bold; } .dragActive { border-style: solid; } ================================================ FILE: src/app/dim-ui/FileUpload.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'dragActive': string; 'fileInput': string; 'instructions': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/FileUpload.tsx ================================================ import { t } from 'app/i18next-t'; import { AppIcon, uploadIcon } from 'app/shell/icons'; import clsx from 'clsx'; import Dropzone, { DropzoneOptions } from 'react-dropzone'; import * as styles from './FileUpload.m.scss'; export default function FileUpload({ accept, title, onDrop, }: { accept?: DropzoneOptions['accept']; title: string; onDrop: DropzoneOptions['onDrop']; }) { return ( <Dropzone onDrop={onDrop} accept={accept} useFsAccessApi={false}> {({ getRootProps, getInputProps, isDragActive }) => ( <div {...getRootProps()} className={clsx(styles.fileInput, { [styles.dragActive]: isDragActive })} > <input {...getInputProps()} /> <div className="dim-button"> <AppIcon icon={uploadIcon} /> {title} </div> <div className={styles.instructions}>{t('FileUpload.Instructions')}</div> </div> )} </Dropzone> ); } ================================================ FILE: src/app/dim-ui/FilterPills.m.scss ================================================ @use '../variables.scss' as *; .guide { composes: flexRow from '../dim-ui/common.m.scss'; flex-wrap: wrap; margin-top: 8px; margin-bottom: 8px; gap: 4px; @include phone-portrait { margin: 8px 6px; } } .pill { composes: resetButton from '../dim-ui/common.m.scss'; composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; background: rgb(0, 0, 0, 0.4); border-radius: 12px; padding: 2px 8px; color: white; border: 1px solid transparent; transition: border-color 150ms; white-space: nowrap; gap: 4px; user-select: none; @include interactive($hover: true) { border-color: rgb(255, 255, 255, 0.3); } .darkBackground & { background-color: rgb(255, 255, 255, 0.2); } :global(.app-icon) { font-size: 10px; } svg { height: 1em; width: auto; } } .selected { border-color: var(--theme-accent-primary) !important; background: rgb(0, 0, 0, 0.6); } ================================================ FILE: src/app/dim-ui/FilterPills.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'darkBackground': string; 'guide': string; 'pill': string; 'selected': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/FilterPills.tsx ================================================ import clsx from 'clsx'; import React from 'react'; import * as styles from './FilterPills.m.scss'; export interface Option<T> { readonly key: string; readonly value: T; readonly content: React.ReactNode; } /** * A generic interface for showing a row of "pills" that can be used for filtering items. Like the bounty guide, but simpler. * This is a controlled component - the state of options should be managed externally. */ export default function FilterPills<T>({ options, selectedOptions, onOptionsSelected, className, darkBackground, extra, }: { options: readonly Option<T>[]; selectedOptions: readonly Option<T>[]; onOptionsSelected: (options: Option<T>[]) => void; className?: string; darkBackground?: boolean; extra?: React.ReactNode; }) { const onClickPill = (e: React.MouseEvent, option: Option<T>) => { e.stopPropagation(); const match = (o: Option<T>) => o.key === option.key; if (e.shiftKey) { const existing = selectedOptions.find(match); if (existing) { onOptionsSelected(selectedOptions.filter((o) => !match(o))); } else { onOptionsSelected([...selectedOptions, option]); } } else if (selectedOptions.length > 1 || !selectedOptions.some(match)) { onOptionsSelected([option]); } else { onOptionsSelected([]); } }; const clearSelection = (e: React.MouseEvent) => { e.stopPropagation(); onOptionsSelected([]); }; return ( <div className={clsx(styles.guide, className, { [styles.darkBackground]: darkBackground })} onClick={clearSelection} > {options.map((o) => ( <button type="button" key={o.key} className={clsx(styles.pill, { [styles.selected]: selectedOptions.some((other) => other.key === o.key), })} onClick={(e) => onClickPill(e, o)} > {o.content} </button> ))} {extra} </div> ); } ================================================ FILE: src/app/dim-ui/FractionalPowerLevel.m.scss ================================================ .fractionalPowerLevel { white-space: nowrap; .fraction { letter-spacing: 0.12em; sup { vertical-align: top; } sub { vertical-align: bottom; } sub, sup { font-size: 75%; line-height: 1; } } } ================================================ FILE: src/app/dim-ui/FractionalPowerLevel.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'fraction': string; 'fractionalPowerLevel': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/FractionalPowerLevel.tsx ================================================ import * as styles from './FractionalPowerLevel.m.scss'; export default function FractionalPowerLevel({ power }: { power: number }) { const numerator = (power * 8) % 8; return ( <span className={styles.fractionalPowerLevel}> {Math.floor(power)} {numerator !== 0 && ( <>   <span className={styles.fraction}> <sup>{Math.floor(numerator)}</sup>⁄<sub>8</sub> </span> </> )} </span> ); } ================================================ FILE: src/app/dim-ui/HelpLink.m.scss ================================================ .helpLink { text-decoration: none; } ================================================ FILE: src/app/dim-ui/HelpLink.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'helpLink': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/HelpLink.tsx ================================================ import { t } from 'app/i18next-t'; import { AppIcon, helpIcon } from '../shell/icons'; import ExternalLink from './ExternalLink'; import * as styles from './HelpLink.m.scss'; export default function HelpLink({ helpLink }: { helpLink?: string }) { if (!helpLink || helpLink.length === 0) { return null; } return ( <ExternalLink className={styles.helpLink} title={t('General.UserGuideLink')} href={helpLink}> <AppIcon icon={helpIcon} /> </ExternalLink> ); } ================================================ FILE: src/app/dim-ui/ItemCategoryIcon.m.scss ================================================ .itemCategoryIcon { width: auto; height: 100%; } ================================================ FILE: src/app/dim-ui/ItemCategoryIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'itemCategoryIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/ItemCategoryIcon.tsx ================================================ import { DimItem } from 'app/inventory/item-types'; import clsx from 'clsx'; import * as styles from './ItemCategoryIcon.m.scss'; import { PressTip } from './PressTip'; import { getArmorSlotSvgIcon, getWeaponSlotSvgIcon, getWeaponTypeSvgIcon, } from './svgs/itemCategory'; export function ArmorSlotIcon({ item, className }: { item: DimItem; className?: string }) { const icon = getArmorSlotSvgIcon(item); return icon ? ( <PressTip minimal elementType="span" tooltip={item.typeName} className={className}> <icon.svg className={clsx(styles.itemCategoryIcon, { dontInvert: icon.colorized })} /> </PressTip> ) : ( <>{item.typeName}</> ); } export function WeaponSlotIcon({ item, className }: { item: DimItem; className?: string }) { const icon = getWeaponSlotSvgIcon(item); return icon ? ( <PressTip minimal elementType="span" tooltip={item.bucket.name} className={className}> <icon.svg className={clsx(styles.itemCategoryIcon, { dontInvert: icon.colorized })} /> </PressTip> ) : ( <>{item.bucket.name}</> ); } export function WeaponTypeIcon({ item, className }: { item: DimItem; className?: string }) { const icon = getWeaponTypeSvgIcon(item); return icon ? ( <PressTip minimal elementType="span" tooltip={item.typeName} className={className}> <icon.svg className={clsx(styles.itemCategoryIcon, { dontInvert: icon.colorized })} /> </PressTip> ) : ( <>{item.typeName}</> ); } ================================================ FILE: src/app/dim-ui/ItemPop.m.scss ================================================ @keyframes pop { to { transform: scale(1.5); } } .itemPop { animation: 0.25s linear 2 alternate pop; z-index: 1; } ================================================ FILE: src/app/dim-ui/ItemPop.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'itemPop': string; 'pop': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/KeyHelp.m.scss ================================================ @use '../variables.scss' as *; .keyHelp { font-family: monospace; border-radius: 2px; display: inline-block; padding: 0 4px; margin-left: 4px; font-size: 10px; color: #ddd; border: 1px solid #ddd; white-space: nowrap; text-align: center; text-transform: uppercase; @include phone-portrait { display: none; margin: 0; } :global(.highlighted) & { color: #222; border-color: #222; } } ================================================ FILE: src/app/dim-ui/KeyHelp.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'keyHelp': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/KeyHelp.tsx ================================================ import { symbolize } from 'app/hotkeys/hotkeys'; import clsx from 'clsx'; import * as styles from './KeyHelp.m.scss'; /** * A keyboard shortcut tip */ export default function KeyHelp({ combo, className }: { combo: string; className?: string }) { return <span className={clsx(styles.keyHelp, className)}>{symbolize(combo)}</span>; } ================================================ FILE: src/app/dim-ui/Loading.m.scss ================================================ @use 'sass:list'; @use '../variables.scss' as *; .loading { display: flex; flex-direction: column; justify-content: center; align-items: center; position: fixed; inset: var(--header-height) 0 0 0; overflow: hidden; z-index: 100; pointer-events: none; } $square-size: 40px; $initialDelay: 1s; $sequenceDelay: 0.1s; $logoDuration: 4s; $logoDelay: 1s; $image: ( (0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 4), (2, 4), (3, 4), (4, 4), (4, 3), (4, 2), (4, 1), (4, 0), (3, 0), (2, 0), (2, 2) ); .container { width: 5 * $square-size; height: 5 * $square-size; position: relative; transform: rotate(45deg); margin: 60px 0; } .square { width: $square-size; height: $square-size; background: #fff; position: absolute; transform: scale(0) rotate(0); } @for $index from 1 through list.length($image) { $point: list.nth($image, $index); .square:nth-child(#{$index}) { top: list.nth($point, 1) * $square-size; left: list.nth($point, 2) * $square-size; animation: $logoDuration ease ($initialDelay + $sequenceDelay * ($index - 1)) infinite forwards logo-animation-pop; } } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes logo-animation-pop { 0% { transform: scale(0) rotate(-45deg); } 10% { transform: scale(1.2) rotate(0); } 15% { transform: scale(1.1) rotate(0); } 55% { transform: scale(1) rotate(0); } 65% { transform: scale(0) rotate(-45deg); } 100% { transform: scale(0) rotate(-45deg); } } .textContainer { width: 300px; height: 3em; position: relative; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; } .text { font-size: 16px; text-align: center; position: absolute; } ================================================ FILE: src/app/dim-ui/Loading.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'container': string; 'fadeIn': string; 'loading': string; 'logoAnimationPop': string; 'square': string; 'text': string; 'textContainer': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/Loading.tsx ================================================ import { AnimatePresence, Transition, Variants, motion } from 'motion/react'; import * as styles from './Loading.m.scss'; const containerAnimateVariants: Variants = { initial: { opacity: 0 }, open: { opacity: 1 }, }; const containerAnimateTransition: Transition<number> = { duration: 0.5, delay: 1, }; const messageAnimateVariants: Variants = { initial: { y: -16, opacity: 0 }, open: { y: 0, opacity: 1 }, leave: { y: 16, opacity: 0 }, }; const messageAnimateTransition: Transition<number> = { duration: 0.2, ease: 'easeOut', }; export function Loading({ message }: { message?: string }) { return ( <section className={styles.loading}> <div className={styles.container}> {Array.from({ length: 16 }, (_, n) => ( <div key={n} className={styles.square} /> ))} </div> <motion.div className={styles.textContainer} initial="initial" animate="open" variants={containerAnimateVariants} transition={containerAnimateTransition} > <AnimatePresence> {message && ( <motion.div key={message} className={styles.text} initial="initial" animate="open" exit="leave" variants={messageAnimateVariants} transition={messageAnimateTransition} > {message} </motion.div> )} </AnimatePresence> </motion.div> </section> ); } ================================================ FILE: src/app/dim-ui/PageLoading.m.scss ================================================ .pageLoading { position: relative; pointer-events: none; } ================================================ FILE: src/app/dim-ui/PageLoading.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'pageLoading': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/PageLoading.tsx ================================================ import { RootState } from 'app/store/types'; import clsx from 'clsx'; import { Transition, Variants, motion } from 'motion/react'; import { useRef } from 'react'; import { useSelector } from 'react-redux'; import { Loading } from './Loading'; import * as styles from './PageLoading.m.scss'; const messageSelector = (state: RootState) => state.shell.loadingMessages.at(-1); const animateVariants: Variants = { initial: { opacity: 0 }, open: { opacity: 1 }, }; const animateTransition: Transition<number> = { duration: 0.1, delay: 0.5, ease: 'easeIn', }; /** * This displays the page-level loading screen. */ export default function PageLoading() { const message = useSelector(messageSelector); const nodeRef = useRef<HTMLDivElement>(null); return ( Boolean(message) && ( <motion.div ref={nodeRef} className={clsx('dim-page', styles.pageLoading)} initial="initial" animate="open" variants={animateVariants} transition={animateTransition} > <Loading message={message} /> </motion.div> ) ); } ================================================ FILE: src/app/dim-ui/PageWithMenu.m.scss ================================================ @use '../variables.scss' as *; .page { max-width: 100%; display: flex; flex-direction: row; align-items: flex-start; padding: 0 14px 0 14px; @include phone-portrait { flex-direction: column; padding: 0; } h2 { text-transform: uppercase; letter-spacing: 2px; @include phone-portrait { margin-left: var(--inventory-column-padding); margin-right: calc(var(--inventory-column-padding) - var(--item-margin)); } } } .contents { flex: 1; width: 100%; box-sizing: border-box; @include desktop { margin-top: 12px; // 8px to match the menu, plus 4px to match the padding } h2:first-child { margin-top: 0; } :global(.issue-banner-shown) & { @include desktop { padding-bottom: $issue-banner-height; } } } .menu { --page-with-menu-menu-width: 230px; font-size: 14px; flex-shrink: 0; margin-right: 12px; margin-top: 8px; margin-left: -4px; // To undo the padding on the inner div position: sticky; top: calc(var(--header-height) + 8px); width: calc(var(--page-with-menu-menu-width) + 8px); overflow: hidden auto; max-height: calc(var(--viewport-height) - var(--header-height) - 8px); @include phone-portrait { position: static; margin: 0; width: 100%; padding: 0; max-height: none; // On mobile, the inner div can be full size and we just allow scrollbars to // appear over the content. > div { width: 100% !important; padding: 0 !important; } } // This inner container div exists to keep the contents from changing widths, // while the outer .menu container may change size as the scrollbars appear // and disappear. > div { width: var(--page-with-menu-menu-width); padding: 4px; // To allow for the outline of the character selector to show } // Add in some width when a scrollbar is present! // It'd be cooler if https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-gutter existed. &.menuScrollbars { // Windows scrollbars are var(--scrollbar-size), then padding p width: calc(var(--page-with-menu-menu-width) + var(--scrollbar-size) + 8px); } ul { margin: 0; padding: 0; } } .menuHeader { margin-bottom: 4px; padding-bottom: 1px; margin-top: 20px; letter-spacing: 1px; text-transform: uppercase; border-bottom: 0.5px solid #666; } .menuButton { display: flex; text-decoration: none; flex-direction: row; align-items: center; margin-bottom: 4px; min-height: 24px; gap: 4px; @include phone-portrait { padding: 6px 10px; font-size: 16px; } @include interactive($hover: true, $focus: true) { color: var(--theme-accent-primary); :global(.app-icon) { color: var(--theme-accent-primary); } } img { height: 24px; width: 24px; } > span:not(:global(.app-icon)) { flex: 1; display: block; text-transform: uppercase; letter-spacing: 1px; } } ================================================ FILE: src/app/dim-ui/PageWithMenu.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'contents': string; 'menu': string; 'menuButton': string; 'menuHeader': string; 'menuScrollbars': string; 'page': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/PageWithMenu.tsx ================================================ import useResizeObserver from '@react-hook/resize-observer'; import clsx from 'clsx'; import React, { useRef, useState } from 'react'; import ErrorBoundary from './ErrorBoundary'; import * as styles from './PageWithMenu.m.scss'; function PageWithMenu({ children, className }: { children: React.ReactNode; className?: string }) { return <div className={clsx(className, styles.page)}>{children}</div>; } /** Detect the presence of scrollbars that take up space. This may only work in this particular case! */ function useHasScrollbars(ref: React.RefObject<HTMLDivElement | null>) { const [hasScrollbars, setHasScrollbars] = useState(false); useResizeObserver(ref, () => { const elem = ref.current; if (!elem) { return; } setHasScrollbars(elem.clientWidth < elem.offsetWidth); }); return hasScrollbars; } /** A sidebar menu. This gets displayed inline on mobile. */ PageWithMenu.Menu = function Menu({ children, className, }: { children: React.ReactNode; className?: string; }) { const ref = useRef<HTMLDivElement>(null); const hasScrollbars = useHasScrollbars(ref); return ( <div ref={ref} className={clsx(className, styles.menu, { [styles.menuScrollbars]: hasScrollbars })} > <div>{children}</div> </div> ); }; /** The main contents of the page, displayed beside the menu on desktop and below the menu on mobile. */ PageWithMenu.Contents = function Contents({ children, className, }: { children: React.ReactNode; className?: string; }) { return ( <div className={clsx(className, styles.contents)}> <ErrorBoundary name="pageWithMenu-contents">{children}</ErrorBoundary> </div> ); }; /** A header for a section of links (MenuButtons) within a Menu. */ PageWithMenu.MenuHeader = function MenuHeader({ children, className, }: { children: React.ReactNode; className?: string; }) { return <div className={clsx(className, styles.menuHeader)}>{children}</div>; }; /** * A link into a section of the page, to be displayed in the menu. The page * will smoothly scroll to the given anchor name. */ PageWithMenu.MenuButton = function MenuButton({ children, className, anchor, ...otherProps }: { children: React.ReactNode; className?: string; /** An optional string ID of a section to scroll into view when this is clicked. */ anchor?: string; } & React.AnchorHTMLAttributes<HTMLAnchorElement>) { const classes = clsx(className, styles.menuButton); return anchor ? ( <a className={classes} href={`#${anchor}`} {...otherProps}> {children} </a> ) : ( <a className={classes} {...otherProps}> {children} </a> ); }; export default PageWithMenu; ================================================ FILE: src/app/dim-ui/PressTip.m.scss ================================================ @use '../variables.scss' as *; $horizontal-padding: 11px; .control { user-select: none; touch-action: none; -webkit-touch-callout: none; } .tooltip { position: absolute; background-color: var(--theme-tooltip-body-bg); color: #dadaea; width: fit-content; max-width: 306px; border: 1px solid var(--theme-tooltip-border); border-top-width: $theme-tooltip-corner-radius; border-radius: $theme-tooltip-corner-radius; box-shadow: var(--theme-drop-shadow); text-align: left; z-index: 99999; white-space: pre-wrap; box-sizing: border-box; user-select: none; pointer-events: none; // The top 'ribbon' in tooltips that's tinted in different contexts (exotic perks, sub-class details) // Note: We don't use border-top-color or 'box-shadow: inset' to color the ribbon // to avoid alignment artefacts along the miter joints (top left & right corners) since the top border is thicker &::after { content: ''; display: block; position: absolute; background-color: var(--tooltip-ribbon-color); height: $theme-tooltip-corner-radius; top: calc(-1 * $theme-tooltip-corner-radius); left: -1px; right: -1px; border-radius: $theme-tooltip-corner-radius $theme-tooltip-corner-radius 0 0; } &.wideTooltip { max-width: 100vw; } hr { border-color: var(--theme-tooltip-border); } p { margin-bottom: 0; &:first-of-type { margin: 0; } } .header { background-color: var(--theme-tooltip-header-bg); padding: 6px $horizontal-padding; h2 { font-size: 14px; margin: 0; color: var(--theme-text); @include destiny-header; } h3 { font-size: 12px; margin: 0; color: #8e8e9e; } } .content { padding: 8px $horizontal-padding; } .arrow { // Local variable used to re-size tooltip arrow when minimal size --arrow-size: #{$theme-tooltip-arrow-size}; border-style: solid; position: absolute; border-color: transparent; border-width: var(--arrow-size); } &[data-popper-placement='top'] .arrow { border-bottom-width: 0; border-top-color: var(--theme-tooltip-border); bottom: calc(var(--arrow-size) * -1); } &[data-popper-placement='bottom'] .arrow { border-top-width: 0; border-bottom-color: var(--theme-tooltip-border); // Include corner radius so the arrow doesn't overlap with the ribbon top: calc((var(--arrow-size) + $theme-tooltip-corner-radius) * -1); } &[data-popper-placement='right'] .arrow { border-left-width: 0; border-right-color: var(--theme-tooltip-border); left: calc(var(--arrow-size) * -1); } &[data-popper-placement='left'] .arrow { border-right-width: 0; border-left-color: var(--theme-tooltip-border); right: calc(var(--arrow-size) * -1); } &.minimalTooltip { border-radius: 3px; border: none; box-shadow: var(--theme-drop-shadow); background-color: var(--theme-tooltip-minimal-bg); .arrow { --arrow-size: #{$theme-tooltip-arrow-size-mini}; } // hide ribbon &::after { display: none; } .content { padding: 3px 6px; } &[data-popper-placement='left'] .arrow { border-left-color: var(--theme-tooltip-minimal-bg); } &[data-popper-placement='right'] .arrow { border-right-color: var(--theme-tooltip-minimal-bg); } &[data-popper-placement='top'] .arrow { border-top-color: var(--theme-tooltip-minimal-bg); } &[data-popper-placement='bottom'] .arrow { border-bottom-color: var(--theme-tooltip-minimal-bg); // Exclude corner radius because minimal tooltips don't have a ribbon top: calc(var(--arrow-size) * -1); } } .section:not(:empty) { margin: 8px #{-$horizontal-padding} -8px #{-$horizontal-padding}; padding: 5px $horizontal-padding 7px $horizontal-padding; border-top: 1px solid rgb(255, 255, 255, 0.2); > :first-child { margin-top: 0; } > :last-child { margin-bottom: 0; } &:first-child { margin-top: -7px; border-top: none; } } } ================================================ FILE: src/app/dim-ui/PressTip.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'arrow': string; 'content': string; 'control': string; 'header': string; 'minimalTooltip': string; 'section': string; 'tooltip': string; 'wideTooltip': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/PressTip.tsx ================================================ import { Placement } from '@popperjs/core'; import { tempContainer } from 'app/utils/temp-container'; import clsx from 'clsx'; import React, { RefObject, createContext, use, useCallback, useEffect, useRef, useState, } from 'react'; import { createPortal } from 'react-dom'; import * as styles from './PressTip.m.scss'; import { usePopper } from './usePopper'; /** * The element where the PressTip should be added to. By default it's the body, * but other elements (like Sheet) can use this to override the attachment point * for PressTips below them in the tree. */ // eslint-disable-next-line @eslint-react/naming-convention/context-name export const PressTipRoot = createContext<RefObject<HTMLElement | null>>({ current: null, }); interface Props { /** * The tooltip may be provided directly, or as a function which will defer * constructing the tree until the tooltip is shown. */ tooltip: React.ReactNode | (() => React.ReactNode); /** * The children of this component define the content that will trigger the tooltip. */ children?: React.ReactNode; /** By default everything gets wrapped in a div, but you can choose a different element type here. */ elementType?: React.ElementType; className?: string; /** Allow the tooltip to be wider than the normal size */ wide?: boolean; /** Reduce padding around the tooltip content. This is appropriate for single-line strings. */ minimal?: boolean; style?: React.CSSProperties; placement?: Placement; role?: string; } type ControlProps = Props & React.HTMLAttributes<HTMLDivElement> & { open: boolean; triggerRef: React.RefObject<HTMLDivElement | null>; }; interface TooltipCustomization { header?: React.ReactNode; subheader?: React.ReactNode; className?: string | null; } const TooltipContext = createContext<React.Dispatch< React.SetStateAction<TooltipCustomization> > | null>(null); /** * <PressTip.Control /> can be used to have a controlled version of the PressTip * * @example * * const ref = useRef<HTMLDivElement>(null); * <PressTip.Control * open={true} * triggerRef={ref} * tooltip={() => ( * <span> * PressTip Content * </span> * )}> * PressTip context element * </PressTip.Control> */ function Control({ tooltip, open, triggerRef, children, elementType: Component = 'div', className, placement, wide, minimal, ...rest }: ControlProps) { const tooltipContents = useRef<HTMLDivElement>(null); const pressTipRoot = use(PressTipRoot); const [customization, customizeTooltip] = useState<TooltipCustomization>({ className: null }); usePopper( { contents: tooltipContents, reference: triggerRef, arrowClassName: styles.arrow, placement, }, [open], ); if (!tooltip) { const { style } = rest; return ( <Component className={className} style={style} {...rest}> {children} </Component> ); } // TODO: if we reuse a stable tooltip container instance we could animate between them // TODO: or use framer motion layout animations? return ( <Component ref={triggerRef} className={clsx(styles.control, className)} {...rest}> {children} {open && createPortal( <div className={clsx(styles.tooltip, customization.className, { [styles.wideTooltip]: wide, [styles.minimalTooltip]: minimal, })} ref={tooltipContents} > {Boolean(customization.header || customization.subheader) && ( <div className={styles.header}> <h2>{customization.header}</h2> {Boolean(customization.subheader) && <h3>{customization.subheader}</h3>} </div> )} {containsContentStyle(tooltip) ? ( tooltip ) : ( <div className={styles.content}> <TooltipContext value={customizeTooltip}> {typeof tooltip === 'function' ? tooltip() : tooltip} </TooltipContext> </div> )} <div className={styles.arrow} /> </div>, pressTipRoot.current || tempContainer, )} </Component> ); } /** * This checks to see if a tooltip already contains a "content" classname element. * If so we can treat it as raw input rather than wrapping it in another copy of * the default tooltip content wrapper. */ function containsContentStyle(tooltip: unknown): tooltip is React.ReactNode { return Boolean( tooltip && typeof tooltip === 'object' && ((Array.isArray(tooltip) && tooltip.some(containsContentStyle)) || ('props' in tooltip && tooltip.props && typeof tooltip.props === 'object' && (('className' in tooltip.props && tooltip.props.className === styles.content) || ('children' in tooltip.props && Array.isArray(tooltip.props.children) && tooltip.props.children.some(containsContentStyle))))), ); } /** * This hook allows customization of the tooltip that the calling component is currently hosted within. * It has no effect if the calling component is not hosted within a tooltip. * * @returns Whether the calling component is currently being hosted in a tooltip. */ export function useTooltipCustomization({ getHeader, getSubheader, className, }: { /** * A function that returns the content to be rendered in the tooltip's header (bold uppercase text). This * **MUST** be memoized (e.g. wrapped in `useCallback`) to prevent an infinite loop. */ getHeader?: () => React.ReactNode; /** * A function that returns the content to be rendered in the tooltip's subheader (dimmed text below the * header). This **MUST** be memoized (e.g. wrapped in `useCallback`) to prevent an infinite loop. */ getSubheader?: () => React.ReactNode; /** The CSS class(es) to be applied to the tooltip's root element. */ className?: string | null; }) { const customizeTooltip = use(TooltipContext); useEffect(() => { if (customizeTooltip) { customizeTooltip((existing) => ({ ...existing, ...(getHeader && { header: getHeader() }), ...(getSubheader && { subheader: getSubheader() }), ...(className !== undefined && { className }), })); } }, [customizeTooltip, getHeader, getSubheader, className]); return customizeTooltip !== null; } export const Tooltip = { /** * A convenience component used to customise the tooltip's header (bold uppercase text) from within JSX. * This does not render anything and has no effect if the calling component is not currently hosted within * a tooltip. * * If you want to display more than a single string, use the `useTooltipCustomization` hook instead. */ Header: ({ text }: { text: string }) => { useTooltipCustomization({ getHeader: useCallback(() => text, [text]) }); return null; }, /** * A convenience component used to customise the tooltip's subheader (dimmed text below the header) from * within JSX. This does not render anything and has no effect if the calling component is not currently * hosted within a tooltip. * * If you want to display more than a single string, use the `useTooltipCustomization` hook instead. */ Subheader: ({ text }: { text: string }) => { useTooltipCustomization({ getSubheader: useCallback(() => text, [text]) }); return null; }, /** * A convenience component used to add a CSS class to the tooltip's root component from within JSX. * This does not render anything and has no effect if the calling component is not currently hosted within * a tooltip. */ Customize: ({ className }: { className: string | null }) => { useTooltipCustomization({ className }); return null; }, /** * If the calling component is hosted within a tooltip, this component wraps its children in a styled `div`. * If not, a fragment containing the children is returned instead. */ Section: ({ children, className }: { children: React.ReactNode; className?: string }) => { const tooltip = use(TooltipContext); if (!tooltip) { return <>{children}</>; } return <div className={clsx(styles.section, className)}>{children}</div>; }, }; const hoverTime = 100; // ms that the cursor can be over the target before the presstip shows const pressTime = 300; // ms that the element can be pressed before the presstip shows /** * A "press tip" is a tooltip that can be shown by pressing on an element, or via hover. * * Tooltip content can be any React element, and can be updated through React. * * Short taps on the element will fire a click event rather than showing the element. * * <PressTip /> wraps <PressTip.Control /> to give you a simpler API for rendering a basic tooltip. * * @example * * <PressTip * tooltip={() => ( * <span> * PressTip Content * </span> * )}> * PressTip context element * </PressTip> */ export function PressTip(props: Props) { // The timer before we show the presstip (different on hover and press) const timer = useRef<number>(0); const touchStartTime = useRef<number>(0); // The triggering element const ref = useRef<HTMLDivElement>(null); // Allow us to distinguish between press and hover gestures const startEvent = useRef<'pointerdown' | 'pointerenter'>(undefined); // Absolute timestamp within which we will suppress clicks const suppressClickUntil = useRef<number>(0); const [open, setOpen] = useState<boolean>(false); const closeToolTip = useCallback((e: React.PointerEvent | React.MouseEvent) => { // Ignore events that aren't paired up if ( !startEvent.current || (e.type === 'pointerup' && startEvent.current === 'pointerenter') || (e.type === 'pointerleave' && startEvent.current === 'pointerdown') ) { return; } setOpen(false); // click fires after pointerup, but we want to suppress click if we'd shown the presstip if ( startEvent.current === 'pointerdown' && performance.now() - touchStartTime.current > pressTime ) { suppressClickUntil.current = performance.now() + 100; } clearTimeout(timer.current); timer.current = 0; startEvent.current = undefined; }, []); // Fires on both pointerenter and pointerdown - does double duty for handling both hover tips and press tips const hover = useCallback((e: React.PointerEvent) => { if ( e.type === 'pointerenter' && // Ignore hover events when the mouse is down (e.buttons !== 0 || // Safari on iOS 26+ fires pointerenter with type 'touch' sometimes // when showing elements. This causes PressTips to be shown initially // and get stuck open, and pointereenter doesn't make much sense for // touch anyway. e.pointerType === 'touch') ) { return; } e.preventDefault(); // If we're already hovering, don't start hovering again if ( startEvent.current && // Safari, at least, fires both pointerenter and pointerdown at the same time. We want the pointerdown event. !(startEvent.current === 'pointerenter' && performance.now() - touchStartTime.current < 10) ) { return; } clearTimeout(timer.current); // Save the event type that initiated the hover startEvent.current = e.type as 'pointerenter' | 'pointerdown'; // Record the start timestamp of the gesture touchStartTime.current = performance.now(); // Hover over should wait for a shorter delay than a press const hoverDelay = e.type === 'pointerenter' ? hoverTime : pressTime; // Start a timer to show the pressTip timer.current = window.setTimeout(() => { setOpen(true); }, hoverDelay); }, []); // Stop the hover timer when the component unmounts useEffect(() => () => clearTimeout(timer.current), []); // When the tooltip was opened by pressing (pointerdown), prevent the click event when // we end the gesture. If the presstip was opened via hovering we want to allow clicks // through. const absorbClick = useCallback((e: React.MouseEvent) => { if (performance.now() < suppressClickUntil.current) { e.stopPropagation(); } }, []); return ( <Control open={open} triggerRef={ref} onPointerEnter={hover} onPointerDown={hover} onPointerLeave={closeToolTip} onPointerUp={closeToolTip} onPointerCancel={closeToolTip} /* onLostPointerCapture closes the tooltip when dragging within our SheetHorizontalScrollContainer which handles pointer events itself - without this, tooltips never close after the scroller steals the pointer capture. */ onLostPointerCapture={closeToolTip} onClick={absorbClick} {...props} /> ); } ================================================ FILE: src/app/dim-ui/README.md ================================================ # DIM UI This package is meant to be home to all the "generic" DIM UI pieces and helpers (some of which is Destiny-specific, much of which is just DIM-specific). Things like buttons, dialogs, headers, and helpers. For UI elements that are specific to particular DIM functionality, put them in their appropriate feature folder. ================================================ FILE: src/app/dim-ui/RadioButtons.m.scss ================================================ @use '../variables' as *; .buttons { composes: flexRow from '../dim-ui/common.m.scss'; gap: 4px; } .button { composes: dim-button from global; flex-grow: 1; } .selected { composes: selected from global; cursor: default; } ================================================ FILE: src/app/dim-ui/RadioButtons.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'button': string; 'buttons': string; 'selected': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/RadioButtons.tsx ================================================ import clsx from 'clsx'; import { memo, useMemo } from 'react'; import * as styles from './RadioButtons.m.scss'; export interface Option<T extends string | number> { label: React.ReactNode; tooltip?: React.ReactNode; value: T; } let nameCounter = 1; /** * A controlled component for a horizontal radio button strip, where one button can be selected at a time. */ function RadioButtons<T extends string | number>({ className, value, onChange, options, }: { className?: string; options: Option<T>[]; value: T; onChange: (value: T) => void; }) { const name = useMemo(() => `radio-${nameCounter++}`, []); return ( <div className={clsx(styles.buttons, className)}> {options.map((option) => ( <RadioButton key={option.value} option={option} selected={option.value === value} onChange={onChange} name={name} /> ))} </div> ); } function RadioButton<T extends string | number>({ option: { label, value }, name, selected, onChange, }: { option: Option<T>; name: string; selected: boolean; onChange: (value: T) => void; }) { return ( <label className={clsx(styles.button, { [styles.selected]: selected, })} > <input type="radio" name={name} checked={selected} onChange={() => onChange(value)} /> {label} </label> ); } export default memo(RadioButtons) as typeof RadioButtons; ================================================ FILE: src/app/dim-ui/Select.m.scss ================================================ @use '../variables.scss' as *; .button { composes: dim-button from global; display: flex; flex-direction: row; align-items: center; } .menu { background: var(--theme-dropdown-menu-bg); color: var(--theme-text); font-size: 12px; z-index: 100; position: absolute; &.open { box-shadow: 0 0 0 1px var(--theme-item-popup-border), var(--theme-drop-shadow); } } .menuItem { display: flex; flex-direction: row; align-items: center; min-width: 10em; padding: 6px 9px; @include phone-portrait { padding: 10px 16px; font-size: 14px; } :global(.app-icon) { color: var(--theme-text) !important; width: 16px; text-align: center; margin-right: 4px; font-size: 12px !important; height: 12px; } } .highlighted { background-color: var(--theme-accent-primary); color: var(--theme-text-invert); cursor: pointer; & :global(.app-icon) { color: var(--theme-text-inverted) !important; } // Maintain legibility of keyHelp & span { color: var(--theme-text-invert); border-color: var(--theme-text-invert); } } .disabled { color: #999 !important; :global(.app-icon) { color: #999 !important; } &.highlighted { background-color: transparent; } } .arrow { font-size: 10px !important; width: 10px !important; height: 10px !important; margin-left: 10px; } ================================================ FILE: src/app/dim-ui/Select.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'arrow': string; 'button': string; 'disabled': string; 'highlighted': string; 'menu': string; 'menuItem': string; 'open': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/Select.tsx ================================================ import { expandDownIcon, expandUpIcon } from 'app/shell/icons'; import AppIcon from 'app/shell/icons/AppIcon'; import clsx from 'clsx'; import { useSelect } from 'downshift'; import { useHeightFromViewportBottom } from 'app/utils/hooks'; import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'; import * as styles from './Select.m.scss'; import { usePopper } from './usePopper'; export interface Option<T> { key: string; content: ReactNode; disabled?: boolean; value?: T; } interface Props<T> { className?: string; /** Hide the selected option from the dropdown */ hideSelected?: boolean; disabled?: boolean; /** Sets the max width for the button. */ maxButtonWidth?: number; /** * Sets the max width for the dropdown. * * If 'button' is used the two things can happen: * 1. If maxButtonWidth is set it will use that as the max width. * 2. If maxButtonWidth is undefined it will calculate the width * of the button dynamically and use that to set the max width. */ maxDropdownWidth?: number | 'button'; value?: T; options: Option<T>[]; /** Optional override for the button content */ children?: ReactNode; onChange: (value?: T) => void; } /** * A Select menu, which maintains a current value and a dropdown to choose * another value. A replacement for HTML's <select> element. This is a * controlled component. * * @see Dropdown for a menu of commands * @see MultiSelect for multiple-item selector */ export default function Select<T>({ className, disabled, maxButtonWidth, maxDropdownWidth, options: items, onChange, value, hideSelected, children, }: Props<T>) { const { isOpen, getToggleButtonProps, getMenuProps, highlightedIndex, getItemProps, selectedItem, } = useSelect({ items, selectedItem: items.find((o) => o.value === value), itemToString: (i) => i?.key || 'none', onSelectedItemChange: ({ selectedItem }) => onChange(selectedItem?.value), isItemDisabled: (item) => Boolean(item.disabled), }); const buttonRef = useRef<HTMLButtonElement>(null); const menuRef = useRef<HTMLDivElement>(null); const [dropdownWidth, setDropdownWidth] = useState<number | undefined>(() => typeof maxDropdownWidth === 'number' ? maxDropdownWidth : undefined, ); const [dropdownHeight, setDropdownHeight] = useState<number | undefined>(); usePopper( { contents: menuRef, reference: buttonRef, placement: 'bottom-start', offset: 2, }, [isOpen, items], ); if (!selectedItem) { throw new Error('value must correspond to one of the provided options'); } useEffect(() => { if (maxDropdownWidth === 'button' && dropdownWidth === undefined && buttonRef.current) { // Minus 2 because the menu has a thicker outline than the button border (2px vs 1px) const width = maxButtonWidth !== undefined ? maxButtonWidth : buttonRef.current.getBoundingClientRect().width - 2; setDropdownWidth(width); } }, [dropdownWidth, maxButtonWidth, maxDropdownWidth]); useHeightFromViewportBottom(buttonRef, setDropdownHeight, 28, true); let buttonStyle: CSSProperties | undefined; const dropdownStyle: CSSProperties = { overflowY: 'auto', overscrollBehaviorY: 'contain', maxHeight: dropdownHeight, }; if (maxButtonWidth !== undefined) { buttonStyle = { maxWidth: maxButtonWidth, }; } return ( <div className={className}> <button type="button" style={buttonStyle} className={styles.button} {...getToggleButtonProps({ ref: buttonRef, disabled, })} > {children ?? ( <> {selectedItem.content}{' '} <AppIcon icon={isOpen ? expandUpIcon : expandDownIcon} className={styles.arrow} /> </> )} </button> <div {...getMenuProps({ ref: menuRef, className: clsx(styles.menu, { [styles.open]: isOpen }) })} > <div style={dropdownStyle}> {isOpen && items.map( (item, index) => !(hideSelected && item.value === value) && ( <div className={clsx(styles.menuItem, { [styles.highlighted]: highlightedIndex === index, [styles.disabled]: item.disabled, })} key={item.key} {...getItemProps({ item, index, })} > {item.content} </div> ), )} </div> </div> </div> ); } ================================================ FILE: src/app/dim-ui/SetFilterButton.m.scss ================================================ @use '../variables.scss' as *; .setFilterButton { display: inline-block; cursor: pointer; color: #888; background-color: #222; padding: 0 6px; border-radius: 4px; text-align: center; text-decoration: none; @include interactive($hover: true) { background-color: #68a0b7; color: #222; } } ================================================ FILE: src/app/dim-ui/SetFilterButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'setFilterButton': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/SetFilterButton.tsx ================================================ import { setSearchQuery } from 'app/shell/actions'; import { AppIcon, searchIcon } from 'app/shell/icons'; import { useDispatch } from 'react-redux'; import * as styles from './SetFilterButton.m.scss'; /** a simple low-profile button that changes the header search field to the provided string */ export function SetFilterButton({ filter }: { filter: string }) { const dispatch = useDispatch(); return ( <a onClick={() => { dispatch(setSearchQuery(filter)); }} title={filter} className={styles.setFilterButton} > <AppIcon icon={searchIcon} /> </a> ); } ================================================ FILE: src/app/dim-ui/Sheet.m.scss ================================================ @use '../variables' as *; // These styles need to be early in the stylesheet so that they can be easily // overridden by components. // TODO: When we stop supporting Chrome <99 and Safari <15.4, we can use @layer // here to fix these cascade issues once and for all. $control-color: rgb(255, 255, 255, 0.5); .sheet { max-height: calc(var(--viewport-height) - var(--header-height) - 8px); left: 0; right: 0; position: fixed; backface-visibility: hidden; bottom: 0; // Pin the sheet to just over the keyboard bottom: var(--viewport-bottom-offset); background-color: var(--theme-item-sheet-bg); color: #e0e0e0; box-shadow: 0 -1px 24px 0 #222; user-select: none; } .header { box-sizing: border-box; padding: 11px 48px 10px 10px; border-bottom: 1px solid #333; border-top: 5px solid $control-color; cursor: grab; // without a min-height, the border-bottom sticks through the close sheet button min-height: 56px; flex-shrink: 0; display: flex; flex-direction: row; align-items: center; > *:first-child { flex: 1; } :where(h1) { font-size: 16px; margin: 0 0 8px 0; display: block; @include destiny-header; } } .footer { border-top: 1px solid #333; padding: 8px 10px; padding-bottom: Max(8px, env(safe-area-inset-bottom)); flex-shrink: 0; } .container { display: flex; flex-direction: column; position: relative; max-height: calc(var(--viewport-height) - var(--header-height) - 8px); touch-action: none; } .contents { flex: 1; -webkit-overflow-scrolling: touch; box-sizing: border-box; // This gets overridden to overflow-y: auto by a resize observer in the // sheets code if the content actually overflows, as part of an elaborate // workaround for browser bugs concerning overscroll-behavior overflow: hidden; overscroll-behavior: none; &:last-child { padding-bottom: env(safe-area-inset-bottom); } } .close { all: initial; z-index: 1; position: absolute; right: 0; top: 0; padding: 18px 12px 12px 12px; color: $control-color; cursor: pointer; @include interactive($hover: true, $active: true, $focus: true) { color: var(--theme-accent-primary); } > :global(.app-icon) { height: 24px; width: 24px; font-size: 24px; } } .sheetDisabled { transform-origin: center bottom; // TODO: would be better to do with Framer Motion, once we switch over to it transform: scale(0.98) !important; transition: transform 300ms linear; } .disabledScreen { background-color: black; position: absolute; inset: 0; display: none; transition: opacity 300ms linear; z-index: 1; .sheetDisabled & { display: block; opacity: 0.6; } } .noHeader { right: 16px !important; } ================================================ FILE: src/app/dim-ui/Sheet.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'close': string; 'container': string; 'contents': string; 'disabledScreen': string; 'footer': string; 'header': string; 'noHeader': string; 'sheet': string; 'sheetDisabled': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/Sheet.tsx ================================================ import { useHotkey } from 'app/hotkeys/useHotkey'; import { t } from 'app/i18next-t'; import ItemPickerContainer from 'app/item-picker/ItemPickerContainer'; import { Portal } from 'app/utils/temp-container'; import SingleVendorSheetContainer from 'app/vendors/single-vendor/SingleVendorSheetContainer'; import clsx from 'clsx'; import { PanInfo, Transition, motion, useAnimation, useDragControls, useReducedMotion, } from 'motion/react'; import React, { createContext, use, useCallback, useEffect, useLayoutEffect, useRef, useState, } from 'react'; import { AppIcon, disabledIcon } from '../shell/icons'; import ErrorBoundary from './ErrorBoundary'; import { PressTipRoot } from './PressTip'; import * as styles from './Sheet.m.scss'; import { sheetsOpen } from './sheets-open'; import { useFixOverscrollBehavior } from './useFixOverscrollBehavior'; /** * Propagates a function for setting a sheet to disabled. This forms a chain as * sheets are shown, where each sheet is wired to its parent so that each child * disables and re-enables its parent automatically. */ const SheetDisabledContext = createContext<(shown: boolean) => void>(() => { // No-op }); /** * The contents of the header, footer, and body can be regular elements, or a function that * takes an "onClose" function that can be used to close the sheet. Using onClose to close * the sheet ensures that it will animate away rather than simply disappearing. */ export type SheetContent = React.ReactNode | ((args: { onClose: () => void }) => React.ReactNode); // The sheet is dismissed if it's flicked at a velocity above dismissVelocity, // or dragged down more than dismissAmount times the height of the sheet. const dismissVelocity = 120; // px/ms const dismissByVelocityMinOffset = 16; // px const dismissAmount = 0.5; const spring: Transition<number> = { type: 'spring', stiffness: 280, damping: 20, mass: 0.2, } as const; const reducedMotionTween = { type: 'tween', duration: 0.01 } as const; const animationVariants = { close: { y: window.innerHeight }, open: { y: 0 }, } as const; const dragConstraints = { top: 0, bottom: window.innerHeight } as const; const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); const handleKeyDown = (e: React.KeyboardEvent) => { // Allow "esc" to propagate which lets you escape focus on inputs. if (e.key !== 'Escape') { e.stopPropagation(); } }; /** * Automatically disable the parent sheet while this sheet is shown. You must * pass `setParentDisabled` to SheetDisabledContext.Provider. */ function useDisableParent( forceDisabled?: boolean, ): [disabled: boolean, setParentDisabled: React.Dispatch<React.SetStateAction<boolean>>] { const [disabledByChildSheet, setDisabledByChildSheet] = useState(false); const setParentDisabled = use(SheetDisabledContext); const effectivelyDisabled = forceDisabled || disabledByChildSheet; useEffect(() => { setParentDisabled(true); return () => setParentDisabled(false); }, [setParentDisabled]); return [effectivelyDisabled, setDisabledByChildSheet]; } /** * A Sheet is a UI element that comes up from the bottom of the screen, * and can be dragged downward to dismiss */ export default function Sheet({ header, footer, children, sheetClassName, closeButtonClassName, headerClassName, disabled: forceDisabled, zIndex, freezeInitialHeight, allowClickThrough, onClose, }: { /** A static, non-scrollable header shown in line with the close button. */ header?: SheetContent; /** A static, non-scrollable footer shown at the bottom of the sheet. Good for buttons. */ footer?: SheetContent; /** Scrollable contents for the sheet. */ children?: SheetContent; /** * Disable the sheet (no clicking, dragging, or close-on-esc). The sheet will * automatically disable itself if another sheet is shown as a child, so no * need to set this explicitly most of the time - pretty much just if you need * to communicate that some "global" sheet like the item picker is up. */ disabled?: boolean; // TODO: remove /** Override the z-index of the sheet. Useful when stacking sheets on top of other sheets or on top of the item popup. */ zIndex?: number; /** A custom class name to add to the sheet container. */ sheetClassName?: string; /** A custom class name to add to the sheet close button. */ closeButtonClassName?: string; /** A custom class name to add to the sheet header. */ headerClassName?: string; // TODO: remove /** If set, the sheet will always be whatever height it was when first rendered, even if the contents change size. */ freezeInitialHeight?: boolean; // TODO: remove by getting a recursive item popup host /** * Allow clicks to escape this sheet. This allows for things like the popups * in the Compare sheet being closed by clicking in the Compare sheet. By * default we block clicks so that clicks in sheets spawned from within an * item popup don't close the popup they were spawned from! */ allowClickThrough?: boolean; onClose: () => void; // TODO: "skinny" sheet option }) { const sheet = useRef<HTMLDivElement>(null); const sheetContents = useRef<HTMLDivElement | null>(null); const [frozenHeight, setFrozenHeight] = useState<number | undefined>(undefined); const frozenHeightIntervalRef = useRef<NodeJS.Timeout | undefined>(undefined); const [disabled, setParentDisabled] = useDisableParent(forceDisabled); const reducedMotion = Boolean(useReducedMotion()); const animationControls = useAnimation(); const dragControls = useDragControls(); /** * Triggering close starts the animation. The onClose prop is called by the callback * passed to the onAnimationComplete motion prop. */ const triggerClose = useCallback( (e?: React.MouseEvent | KeyboardEvent) => { e?.preventDefault(); // Animate offscreen animationControls.start('close'); }, [animationControls], ); // Handle global escape key useHotkey('esc', t('Hotkey.ClearDialog'), triggerClose); // We need to call the onClose callback when then close animation is complete so that // the calling component can unmount the sheet // TODO (ryan/ben) move to using a container component and AnimatePresence const handleAnimationComplete = useCallback( (animationDefinition: 'close' | 'open') => { if (animationDefinition === 'close') { onClose(); } }, [onClose], ); // Determine when to drag. Drags if the touch falls in the header, or if the contents // are scrolled all the way to the top. const dragHandleDown = useCallback( (e: React.PointerEvent<HTMLDivElement>) => { if ( !sheetContents.current!.contains(e.target as Node) || sheetContents.current!.scrollTop === 0 ) { dragControls.start(e); } }, [dragControls], ); useFixOverscrollBehavior(sheetContents); // When drag ends we determine if the sheet should be closed either via the final // drag velocity or if the sheet has been dragged halfway the down from its height. const handleDragEnd = useCallback( (_event: TouchEvent | MouseEvent | PointerEvent, info: PanInfo) => { const velocity = info.velocity.y / window.devicePixelRatio; const offset = info.offset.y; if ( (velocity > dismissVelocity && offset > dismissByVelocityMinOffset) || (sheet.current && offset > dismissAmount * sheet.current.clientHeight) ) { triggerClose(); return; } animationControls.start('open'); }, [animationControls, triggerClose], ); useLayoutEffect(() => { clearInterval(frozenHeightIntervalRef.current); if (freezeInitialHeight && sheetContents.current && !frozenHeight) { if (sheetContents.current.clientHeight > 0) { setFrozenHeight(sheetContents.current.clientHeight); } else { const setHeight = () => { if (!sheetContents.current || sheetContents.current.clientHeight === 0) { return false; } setFrozenHeight(sheetContents.current.clientHeight); frozenHeightIntervalRef.current = undefined; return true; }; frozenHeightIntervalRef.current = tryRepeatedlyWithLimit(setHeight); } } }, [freezeInitialHeight, frozenHeight]); useEffect(() => { animationControls.start('open'); }, [animationControls]); // Track the total number of sheets that are open (to help prevent reloads while users are doing things) useEffect(() => { sheetsOpen.open++; return () => { sheetsOpen.open--; }; }, []); const sheetBody = ( <motion.div // motion props initial="close" transition={reducedMotion ? reducedMotionTween : spring} animate={animationControls} variants={animationVariants} onAnimationComplete={handleAnimationComplete} drag="y" dragControls={dragControls} dragListener={false} dragConstraints={dragConstraints} dragElastic={0} onDragEnd={disabled ? undefined : handleDragEnd} // regular props style={{ zIndex }} className={clsx(styles.sheet, sheetClassName, { [styles.sheetDisabled]: disabled })} ref={sheet} role="dialog" aria-modal="false" onKeyDown={handleKeyDown} onKeyUp={stopPropagation} onKeyPress={stopPropagation} onClick={allowClickThrough ? undefined : stopPropagation} > <button type="button" className={clsx(styles.close, closeButtonClassName, { [styles.noHeader]: !header })} onClick={triggerClose} aria-keyshortcuts="esc" aria-label={t('General.Close')} > <AppIcon icon={disabledIcon} /> </button> <div className={styles.container} onPointerDown={disabled ? undefined : dragHandleDown}> {Boolean(header) && ( <div className={clsx(styles.header, headerClassName)}> {typeof header === 'function' ? header({ onClose: triggerClose }) : header} </div> )} <div className={styles.contents} style={frozenHeight ? { flexBasis: frozenHeight } : undefined} ref={sheetContents} > <ErrorBoundary name="sheet-contents"> {typeof children === 'function' ? children({ onClose: triggerClose }) : children} </ErrorBoundary> </div> {Boolean(footer) && ( <div className={styles.footer}> {typeof footer === 'function' ? footer({ onClose: triggerClose }) : footer} </div> )} </div> <div className={styles.disabledScreen} onClick={stopPropagation} onPointerDown={stopPropagation} /> </motion.div> ); return ( <Portal> <SheetDisabledContext value={setParentDisabled}> <PressTipRoot value={sheet}> <ItemPickerContainer> <SingleVendorSheetContainer>{sheetBody}</SingleVendorSheetContainer> </ItemPickerContainer> </PressTipRoot> </SheetDisabledContext> </Portal> ); } function tryRepeatedlyWithLimit(callback: () => boolean, timeout = 500, limit = 5_000) { let totalTime = 0; return setInterval(() => { if (totalTime > limit) { return; } const res = callback(); totalTime += timeout; if (res) { return; } }, timeout); } ================================================ FILE: src/app/dim-ui/SheetHorizontalScrollContainer.m.scss ================================================ .horizontalScrollContainer { flex: 1; white-space: nowrap; overflow: auto hidden; display: flex; flex-direction: row; justify-content: flex-start; contain: content; touch-action: none; } ================================================ FILE: src/app/dim-ui/SheetHorizontalScrollContainer.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'horizontalScrollContainer': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/SheetHorizontalScrollContainer.tsx ================================================ import clsx from 'clsx'; import React, { useCallback, useRef } from 'react'; import * as styles from './SheetHorizontalScrollContainer.m.scss'; // After this many pixels of dragging in either direction, we consider ourselves to be part of a scrolling gesture. const HORIZ_SCROLL_DRAG_THRESHOLD = 20; /** * We have issues on mobile where horizontal scrolling of flex components doesn't work for some unknown reason. This * component is a workaround that captures pointer events when the pointer has been triggered via the down state and * has also been moved by HORIZ_SCROLL_DRAG_THRESHOLD pixels. This ensures that button clicks in the component don't * get interupted and work as expected. */ export function SheetHorizontalScrollContainer({ className, children, }: { className?: string; children: React.ReactNode; }) { // This uses pointer events to directly set the scroll position based on dragging the items. This works around an // iOS bug around nested draggables, but also is kinda nice on desktop. I wasn't able to get it to do an inertial // animation after releasing. const ref = useRef<HTMLDivElement>(null); const dragStateRef = useRef<{ scrollPosition: number; pointerDownPosition: number; scrolling: boolean; }>(undefined); const handlePointerDown = useCallback((e: React.PointerEvent) => { // Don't do any of this if the view isn't scrollable in the first place if (ref.current!.scrollWidth <= ref.current!.clientWidth) { return; } dragStateRef.current = { pointerDownPosition: e.clientX, scrollPosition: ref.current!.scrollLeft, scrolling: false, }; }, []); const handlePointerUp = useCallback((e: React.PointerEvent) => { dragStateRef.current = undefined; ref.current!.releasePointerCapture(e.pointerId); }, []); const handlePointerMove = useCallback((e: React.PointerEvent) => { if (dragStateRef.current !== undefined) { const { scrollPosition, pointerDownPosition } = dragStateRef.current; // Once we've moved HORIZ_SCROLL_DRAG_THRESHOLD in either direction, // constrain to horizontal scrolling only dragStateRef.current.scrolling ||= Math.abs(e.clientX - pointerDownPosition) > HORIZ_SCROLL_DRAG_THRESHOLD; if (dragStateRef.current.scrolling) { // Only set the pointer capture once we've moved enough. This allows you // to still keep scrolling even if the pointer leaves the scrollable // area (which feels nice) but buttons still work. If we always capture // in handlePointerDown, buttons won't work because all events get // retargeted to the scroll area. ref.current!.setPointerCapture(e.pointerId); e.stopPropagation(); } ref.current!.scrollLeft = scrollPosition - (e.clientX - pointerDownPosition); } }, []); return ( <div ref={ref} className={clsx(styles.horizontalScrollContainer, className)} onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerCancel={handlePointerUp} > {children} </div> ); } ================================================ FILE: src/app/dim-ui/ShowPageLoading.tsx ================================================ import { loadingEnd, loadingStart } from 'app/shell/actions'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { useEffect } from 'react'; /** * This component can be used to show a page-level loading screen with an optional message. */ export default function ShowPageLoading({ message }: { message: string }) { const dispatch = useThunkDispatch(); useEffect(() => { dispatch(loadingStart(message)); return () => { dispatch(loadingEnd(message)); }; }, [dispatch, message]); return null; } ================================================ FILE: src/app/dim-ui/SpecialtyModSlotIcon.m.scss ================================================ .specialtyModIcon { display: inline-block; background-size: contain; background-position: center; background-repeat: no-repeat; } ================================================ FILE: src/app/dim-ui/SpecialtyModSlotIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'specialtyModIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/SpecialtyModSlotIcon.tsx ================================================ import { bungieBackgroundStyle } from 'app/dim-ui/BungieImage'; import { DimItem } from 'app/inventory/item-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { artificeDisplayStub } from 'app/search/specialty-modslots'; import { getSpecialtySocketMetadata, isArtifice } from 'app/utils/item-utils'; import clsx from 'clsx'; import { PressTip } from './PressTip'; import * as styles from './SpecialtyModSlotIcon.m.scss'; /** * if an item has specialty modslots, this returns one or * more elements wrapped in a fragment. they'll probably * need a flex/something wrapper to display right */ export function SpecialtyModSlotIcon({ item, className }: { item: DimItem; className?: string }) { const defs = useD2Definitions()!; const modMetadata = isArtifice(item) ? artificeDisplayStub : getSpecialtySocketMetadata(item); if (!modMetadata) { return null; } const emptySlotItem = defs.InventoryItem.get(modMetadata.emptyModSocketHash); let background: string; if (modMetadata.milestoneHash) { const milestone = defs.Milestone.get(modMetadata.milestoneHash); background = milestone.displayProperties.icon; } else if (modMetadata.activityModeHash) { const activityMode = defs.ActivityMode.get(modMetadata.activityModeHash); background = activityMode.displayProperties.icon; } else if (modMetadata.iconHash) { const icon = defs.Icon.get(modMetadata.iconHash); background = icon.foreground; } else { background = emptySlotItem.displayProperties.icon; } return ( <PressTip minimal tooltip={emptySlotItem.itemTypeDisplayName} key={emptySlotItem.hash} className={clsx(className, styles.specialtyModIcon)} style={bungieBackgroundStyle(background)} /> ); } ================================================ FILE: src/app/dim-ui/StaticPage.m.scss ================================================ @use '../variables.scss' as *; .page { composes: dim-page from global; padding: 16px 1em; font-size: 14px; user-select: auto; form { padding: 0; } h1 { flex-shrink: 0; margin: 0; font-weight: 200; display: flex; flex-direction: row; align-items: center; @media (max-width: 800px) { font-size: 20px; } img { margin-right: 8px; } } h2 { a { color: var(--theme-accent-primary); text-decoration: underline; text-underline-offset: 6px; :global(.app-icon) { margin-right: 6px; } } } > p { margin: 5px 0; } } ================================================ FILE: src/app/dim-ui/StaticPage.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'page': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/StaticPage.tsx ================================================ import clsx from 'clsx'; import React from 'react'; import * as styles from './StaticPage.m.scss'; /** * Styled wrapper for "static pages" - e.g. informational pages like About. */ export default function StaticPage({ children, className, }: { children?: React.ReactNode; className?: string; }) { return <div className={clsx(styles.page, className)}>{children}</div>; } ================================================ FILE: src/app/dim-ui/Switch.m.scss ================================================ @use 'sass:math'; @use '../variables.scss' as *; $dotSize: 16px; $barHeight: 10px; $barWidth: 28px; $duration: 150ms; $touchPadding: 2px; // A bit of extra hit area .switch { display: inline-block; position: relative; overflow: hidden; padding: 0 !important; height: $dotSize + $touchPadding * 2; box-sizing: border-box; vertical-align: middle; label { cursor: pointer; display: inline-block; position: relative; padding: $touchPadding !important; height: $dotSize; margin: 0 !important; &::before { content: ''; display: inline-block; width: $barWidth; height: $barHeight; margin: math.div($dotSize - $barHeight, 2) !important; padding: 0 !important; background-color: rgb(255, 255, 255, 0.2); border-radius: 20px; transition: background-color $duration ease-out; } // The dot &::after { content: ''; position: absolute; width: $dotSize - 4px; height: $dotSize - 4px; border-radius: 50%; background-color: white; transition: left $duration ease-out, background-color $duration ease-out; top: $touchPadding; left: $touchPadding; border: 2px solid transparent; box-shadow: 0 0 3px rgb(0, 0, 0, 0.5); } } input { position: absolute; top: 0; left: -999px; // move it away but keep it focusable &:checked { & + label { &::before { background-color: var(--theme-accent-primary); opacity: 0.5; } // The dot &::after { left: $touchPadding + $barWidth - $barHeight; background-color: var(--theme-accent-primary); } } } &:disabled { & + label { &::before { cursor: not-allowed; background-color: rgb(255, 255, 255, 0.2) !important; } // The dot &::after { background-color: #666; cursor: not-allowed; } } } // Set focus styles &:focus + label::after { border-color: #0175ff; } // For browsers that support :focus-visible, remove focus styles when focus-visible would be unset &:focus:not(:focus-visible) + label::after { border-color: transparent; } } } ================================================ FILE: src/app/dim-ui/Switch.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'switch': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/Switch.tsx ================================================ import clsx from 'clsx'; import React from 'react'; import * as styles from './Switch.m.scss'; export default function Switch<K extends string>({ checked, onChange, name, className, disabled = false, }: { checked: boolean; name: K; className?: string; disabled?: boolean; onChange: (checked: boolean, name: K) => void; }) { const change = (e: React.ChangeEvent<HTMLInputElement>) => { onChange(e.target.checked, name); }; return ( <div className={clsx(styles.switch, className)}> <input type="checkbox" id={name} role="switch" className="onoffswitch-checkbox" checked={checked} onChange={change} disabled={disabled} /> {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} <label htmlFor={name} /> </div> ); } ================================================ FILE: src/app/dim-ui/TileGrid.m.scss ================================================ @use '../variables' as *; .header { font-weight: 600; font-size: 16px; text-transform: uppercase; padding-top: 8px; padding-bottom: 8px; background-color: var(--theme-item-sheet-bg); position: sticky; top: 0; z-index: 1; } .items { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--tile-grid-width, 250px), 1fr)); gap: 4px; } .tile { // Don't scale up items even if users have item tiles set to large --item-size: 50px; display: grid; grid-template-columns: min-content auto min-content; grid-template-rows: max-content auto; grid-template-areas: 'icon title corner' 'details details details'; border: 1px solid transparent; cursor: pointer; padding: 8px; gap: 8px; box-sizing: border-box; position: relative; @include interactive($hover: true) { background-color: rgb(255, 255, 255, 0.1); } &:focus-visible { border-color: var(--theme-accent-secondary); outline: none; } &.compact { grid-template-areas: 'icon title corner' 'icon details corner'; row-gap: 4px; } > :global(.item) { grid-area: icon; } } .details { display: flex; flex-direction: column; gap: 4px; grid-area: details; text-wrap: pretty; } .selected { border-color: var(--theme-accent-primary); } .disabled { opacity: 0.5; cursor: default; } .title { align-self: center; color: var(--theme-text); font-weight: bold; font-size: 16px; grid-area: title; text-wrap: balance; } ================================================ FILE: src/app/dim-ui/TileGrid.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'compact': string; 'details': string; 'disabled': string; 'header': string; 'items': string; 'selected': string; 'tile': string; 'title': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/TileGrid.tsx ================================================ import clsx from 'clsx'; import * as styles from './TileGrid.m.scss'; /** * A grid of tiles with an optional header, e.g. the mod picker, exotic picker, * or subclass picker tiles. * * Tile size can be tweaked using the --tile-grid-width CSS variable. */ export function TileGrid({ className, header, children, }: { className?: string; header?: React.ReactNode; children: React.ReactNode; }) { return ( <div className={className}> {Boolean(header) && <div className={styles.header}>{header}</div>} <div className={styles.items}>{children}</div> </div> ); } /** * An individual tile in the tile grid. Has an icon on the left, and content on the right. */ export function TileGridTile({ className, children, icon, title, corner, selected, disabled, onClick, compact, }: { className?: string; children: React.ReactNode; icon: React.ReactNode; title: React.ReactNode; corner?: React.ReactElement; selected?: boolean; disabled?: boolean; onClick: React.MouseEventHandler<HTMLElement>; compact?: boolean; }) { return ( <div className={clsx(className, styles.tile, { [styles.selected]: selected, [styles.disabled]: disabled, [styles.compact]: compact, })} onClick={disabled ? undefined : onClick} role="button" aria-disabled={disabled} aria-pressed={selected} tabIndex={0} > <> {icon} <div className={styles.title}>{title}</div> {corner} <div className={styles.details}>{children}</div> </> </div> ); } ================================================ FILE: src/app/dim-ui/UserGuideLink.tsx ================================================ import { t } from 'app/i18next-t'; import { userGuideUrl } from 'app/shell/links'; import clsx from 'clsx'; import { AppIcon, helpIcon } from '../shell/icons'; import ExternalLink from './ExternalLink'; /** * Link to a specific topic in the DIM User Guide wiki. */ export default function UserGuideLink({ topic, title, className, }: { topic?: string; title?: string; className?: string; }) { if (!topic || topic.length === 0) { return null; } const link = userGuideUrl(topic); return ( <ExternalLink href={link} className={clsx('dim-button', className)}> <AppIcon icon={helpIcon} /> {title || t('General.UserGuideLink')} </ExternalLink> ); } ================================================ FILE: src/app/dim-ui/VirtualList.m.scss ================================================ .scrollContainer { height: 100%; width: 100%; overflow: hidden auto; contain: strict; box-sizing: border-box; } .contentsPlaceholder { width: 100%; position: relative; overflow: hidden; } .virtualArea { position: absolute; top: 0; left: 0; width: 100%; > * { // Prevent the margins of elements inside the item containers from escaping // this container. Otherwise the height measurements will be wrong for the // virtual list. overflow: hidden; } } ================================================ FILE: src/app/dim-ui/VirtualList.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'contentsPlaceholder': string; 'scrollContainer': string; 'virtualArea': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/VirtualList.tsx ================================================ import { Virtualizer, useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual'; import clsx from 'clsx'; import { useImperativeHandle, useLayoutEffect, useRef } from 'react'; import * as styles from './VirtualList.m.scss'; interface VirtualListProps { numElements: number; estimatedSize: number | ((index: number) => number); className?: string; itemContainerClassName?: string; /** * The number of items to render above and below the visible area. Increasing * this number will increase the amount of time it takes to render the * virtualizer, but might decrease the likelihood of seeing slow-rendering * blank items at the top and bottom of the virtualizer when scrolling. */ overscan?: number; children: (index: number) => React.ReactNode; getItemKey: (index: number) => string | number; // React.Key, but they added bigint while @tanstack/react-virtual used their own Key type ref?: React.Ref<VirtualListRef>; } export interface VirtualListRef { scrollToIndex: Virtualizer<HTMLDivElement, Element>['scrollToIndex']; } /** * A virtual scrolling list linked to a scrollable element. e.g. Item Feed. * * @see WindowVirtualList for a window-linked virtual scroller. */ export function VirtualList({ numElements, estimatedSize, className, itemContainerClassName, overscan, children, getItemKey, ref, }: VirtualListProps) { // Dynamic-height element-based virtual list code based on https://tanstack.com/virtual/v3/docs/examples/react/dynamic const parentRef = useRef<HTMLDivElement>(null); const virtualizer = useVirtualizer({ count: numElements, getScrollElement: () => parentRef.current, estimateSize: typeof estimatedSize === 'function' ? estimatedSize : () => estimatedSize, getItemKey, overscan, }); useImperativeHandle(ref, () => ({ scrollToIndex: virtualizer.scrollToIndex }), [ virtualizer.scrollToIndex, ]); if (numElements === 0) { return null; } const items = virtualizer.getVirtualItems(); return ( <div ref={parentRef} className={clsx(className, styles.scrollContainer)}> <div className={styles.contentsPlaceholder} style={{ height: virtualizer.getTotalSize(), }} > <div className={styles.virtualArea} style={{ transform: `translateY(${items.length > 0 ? items[0].start : 0}px)`, }} > {items.map((virtualItem) => ( <div key={virtualItem.key} ref={virtualizer.measureElement} className={itemContainerClassName} data-index={virtualItem.index} > {children(virtualItem.index)} </div> ))} </div> </div> </div> ); } /** * A virtual scrolling list linked to window scroll. e.g. Optimizer sets or Loadouts. * * @see VirtualList for an element-linked virtual scroller */ export function WindowVirtualList({ numElements, estimatedSize, className, itemContainerClassName, children, overscan, getItemKey, ref, }: VirtualListProps) { // Dynamic-height window-based virtual list code based on https://tanstack.com/virtual/v3/docs/examples/react/dynamic const parentRef = useRef<HTMLDivElement>(null); const parentOffsetRef = useRef(0); useLayoutEffect(() => { parentOffsetRef.current = parentRef.current?.offsetTop ?? 0; }, []); const headerHeightRef = useRef(0); useLayoutEffect(() => { headerHeightRef.current = parseInt( document.querySelector('html')!.style.getPropertyValue('--header-height'), 10, ); }, []); const virtualizer = useWindowVirtualizer({ count: numElements, estimateSize: typeof estimatedSize === 'function' ? estimatedSize : () => estimatedSize, scrollMargin: parentOffsetRef.current, scrollPaddingStart: headerHeightRef.current, getItemKey, overscan, }); useImperativeHandle(ref, () => ({ scrollToIndex: virtualizer.scrollToIndex }), [ virtualizer.scrollToIndex, ]); if (numElements === 0) { return null; } const items = virtualizer.getVirtualItems(); return ( <div className={clsx(className, styles.contentsPlaceholder)} ref={parentRef} style={{ height: `${virtualizer.getTotalSize()}px`, }} > <div className={styles.virtualArea} style={{ transform: `translateY(${ items.length > 0 ? items[0].start - virtualizer.options.scrollMargin : 0 }px)`, }} > {items.map((virtualItem) => ( <div key={virtualItem.key} ref={virtualizer.measureElement} className={itemContainerClassName} data-index={virtualItem.index} > {children(virtualItem.index)} </div> ))} </div> </div> ); } ================================================ FILE: src/app/dim-ui/WeaponGroupingIcon.m.scss ================================================ .ammoIcon { width: 28px; height: 20px; } .elementIcon { width: 50%; height: 0; padding-bottom: 50%; margin: 0; } .weaponTypeIcon { width: 100%; height: auto; } ================================================ FILE: src/app/dim-ui/WeaponGroupingIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'ammoIcon': string; 'elementIcon': string; 'weaponTypeIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/WeaponGroupingIcon.tsx ================================================ import TagIcon from 'app/inventory/TagIcon'; import { AmmoIcon } from 'app/item-popup/AmmoIcon'; import { VaultGroupIcon } from 'app/shell/item-comparators'; import ElementIcon from './ElementIcon'; import { getWeaponTypeSvgIconFromCategoryHashes } from './svgs/itemCategory'; import * as styles from './WeaponGroupingIcon.m.scss'; export default function WeaponGroupingIcon({ icon, className, }: { icon: VaultGroupIcon; className?: string; }) { switch (icon.type) { case 'typeName': { const typeIcon = getWeaponTypeSvgIconFromCategoryHashes(icon.itemCategoryHashes); return ( typeIcon && ( <div className={className}> <typeIcon.svg className={styles.weaponTypeIcon} /> </div> ) ); } case 'ammoType': { return ( <div className={className}> <AmmoIcon type={icon.ammoType} className={styles.ammoIcon} /> </div> ); } case 'tag': { return ( icon.tag && ( <div className={className}> <TagIcon tag={icon.tag} /> </div> ) ); } case 'elementWeapon': { return ( <div className={className}> <ElementIcon className={styles.elementIcon} element={icon.element} /> </div> ); } case 'none': return null; } } ================================================ FILE: src/app/dim-ui/_tooltip-mixins.scss ================================================ @use '../variables' as *; @use 'sass:color'; // These mixins are used to customize tooltips hosted within a PressTip (see PressTip.m.scss) @mixin tooltip-background-color($color) { --tooltip-background-color: #{$color}; } @mixin tooltip-border-color($color) { --tooltip-border-color-rgb: #{dim-hex-to-rgb-values($color)}; } @mixin tooltip-ribbon-color($color) { --tooltip-ribbon-color: #{$color}; } @mixin tooltip-section-color($color) { &:not(:empty) { color: color.mix(white, $color, 80%); background-color: rgb($color, 0.175); border-top-color: transparent !important; } } ================================================ FILE: src/app/dim-ui/common.m.scss ================================================ @layer common { .flexWrap { display: flex; flex-flow: row wrap; } .flexRow { display: flex; flex-direction: row; } .flexColumn { display: flex; flex-direction: column; } .resetButton { text-align: center; color: inherit; appearance: none; background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer; font-size: inherit; font-family: inherit; user-select: none; &:disabled { cursor: default; } } // Show visible scrollbars when scrollable, even if the OS (e.g. macOS) prefers hidden scrollbars. .visibleScrollbars { &::-webkit-scrollbar-track { background: #555; } &::-webkit-scrollbar-thumb { background: #e4e4e4; border-radius: 3px; } &::-webkit-scrollbar { width: 10px; &:horizontal { height: 10px; } } } } ================================================ FILE: src/app/dim-ui/destiny-symbols/ColorDestinySymbols.m.scss ================================================ @use '../../variables.scss' as *; .thermal { color: $solar; } .stasis { color: $stasis; } .arc { color: $arc; } .void { color: $void; } .strand { color: $strand; } .prismatic { color: $prismatic; } ================================================ FILE: src/app/dim-ui/destiny-symbols/ColorDestinySymbols.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'arc': string; 'prismatic': string; 'stasis': string; 'strand': string; 'thermal': string; 'void': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/destiny-symbols/ColorDestinySymbols.tsx ================================================ import { FontGlyphs } from 'data/font/d2-font-glyphs'; import { DimCustomSymbols } from 'data/font/dim-custom-symbols'; import * as styles from './ColorDestinySymbols.m.scss'; const iconPlaceholder = /([\uE000-\uF8FF\u{F0000}-\u{F1000}])/u; const styleTable = { [String.fromCodePoint(FontGlyphs.thermal)]: styles.thermal, [String.fromCodePoint(FontGlyphs.arc)]: styles.arc, [String.fromCodePoint(FontGlyphs.void)]: styles.void, [String.fromCodePoint(FontGlyphs.stasis)]: styles.stasis, [String.fromCodePoint(FontGlyphs.strand_kill)]: styles.strand, [String.fromCodePoint(DimCustomSymbols.prismatic)]: styles.prismatic, }; export default function ColorDestinySymbols({ text, className, }: { text?: string; className?: string; }): React.ReactElement { // split into segments, filter out empty, try replacing each piece with an icon if one matches const richTextSegments = (text ?? '') .split(iconPlaceholder) .filter(Boolean) .map((t, index) => replaceWithIcon(t, index)); return <span className={className}>{richTextSegments}</span>; } function replaceWithIcon(textSegment: string, index: number) { const className = styleTable[textSegment]; return className ? ( <span key={textSegment + index} className={className}> {textSegment} </span> ) : ( textSegment ); } ================================================ FILE: src/app/dim-ui/destiny-symbols/RichDestinyText.tsx ================================================ import { dynamicStringsSelector } from 'app/inventory/selectors'; import React from 'react'; import { useSelector } from 'react-redux'; import { conversionTableSelector, iconPlaceholder, RichTextConversionTable, } from './rich-destiny-text'; const dynamicTextFinder = /\{var:\d+\}/g; /** * converts an objective description or other string to html nodes. this identifies: * * • bungie's localized placeholder strings * * • special unicode characters representing weapon/etc icons in the game's font * * and ensures they are replaced with the unicode characters * * this also performs new dynamic string replacement * (certain per-character customized strings) * so please include the characterId of the item's owner if possible */ export default function RichDestinyText({ text, ownerId = '', // normalize for cleaner indexing later className, }: { text?: string; ownerId?: string; className?: string; }): React.ReactElement { const replacer = useDynamicStringReplacer(ownerId); const conversionTable = useSelector(conversionTableSelector); // perform dynamic string replacement text = replacer(text); // split into segments, filter out empty, try replacing each piece with an icon if one matches const richTextSegments = text .split(iconPlaceholder) .filter(Boolean) .map((t, index) => replaceWithIcon(t, index, conversionTable)); return <span className={className}>{richTextSegments}</span>; } function replaceWithIcon( textSegment: string, index: number, conversionTable: RichTextConversionTable, ) { const replacementInfo = conversionTable[textSegment]; return replacementInfo ? ( <span key={textSegment + index} title={replacementInfo.plaintext}> {replacementInfo.unicode} </span> ) : ( textSegment ); } export function useDynamicStringReplacer(ownerId = '') { const dynamicStrings = useSelector(dynamicStringsSelector); return function (text = '') { return text.replace(dynamicTextFinder, (segment) => { const hash = segment.match(/\d+/)![0]; const dynamicValue = dynamicStrings?.byCharacter[ownerId]?.[hash] ?? dynamicStrings?.allProfile[hash] ?? (dynamicStrings && Object.values(dynamicStrings.byCharacter)[0][hash]); return dynamicValue?.toString() ?? segment; }); }; } ================================================ FILE: src/app/dim-ui/destiny-symbols/SymbolsPicker.m.scss ================================================ @use '../../variables.scss' as *; @use '../tooltip-mixins' as *; .symbolsWindow { width: 300px; height: 350px; background-color: var(--theme-dropdown-menu-bg); display: flex; flex-flow: column; box-shadow: var(--theme-drop-shadow); pointer-events: auto; } .symbolsButton { composes: resetButton from '../common.m.scss'; font-family: unset; margin: 0 2px; @include interactive($hover: true) { color: black; background-color: var(--theme-accent-primary); } span { font-size: 18px; } } .wrapperDiv { position: relative; } .buttonDiv { position: absolute; top: 0; right: 0; } .symbolsSearch { padding: 10px 10px; flex: 0; > div { min-width: 0; } } .symbolsBody { overflow: hidden auto; flex: 1; } .symbolsContainer { padding: 4px 10px; display: flex; flex-flow: row wrap; justify-content: space-between; align-items: baseline; } // Ensure the last row is left-aligned .symbolsContainer::after { content: ''; flex: auto; } .emojiButton { composes: resetButton from '../common.m.scss'; font-family: unset; text-align: center; font-size: 18px; color: var(--theme-text); padding: 2px; @include interactive($hover: true) { color: var(--theme-text-invert); background-color: var(--theme-accent-primary); } } .symbolsFooter { padding: 10px 10px; font-size: 20px; min-height: 40px; flex: 0; border-top: 1px grey solid; display: flex; flex-direction: row; > span { flex: 0; align-self: center; } > div { padding-left: 10px; flex: 1; display: flex; flex-direction: column; > span { font-size: 12px; &:first-child { font-size: 16px; } } } } ================================================ FILE: src/app/dim-ui/destiny-symbols/SymbolsPicker.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttonDiv': string; 'emojiButton': string; 'symbolsBody': string; 'symbolsButton': string; 'symbolsContainer': string; 'symbolsFooter': string; 'symbolsSearch': string; 'symbolsWindow': string; 'wrapperDiv': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/destiny-symbols/SymbolsPicker.tsx ================================================ import { t } from 'app/i18next-t'; import { SearchInput } from 'app/search/SearchInput'; import { tempContainer } from 'app/utils/temp-container'; import clsx from 'clsx'; import { FontGlyphs } from 'data/font/d2-font-glyphs'; import React, { HTMLProps, memo, use, useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useSelector } from 'react-redux'; import ClickOutside from '../ClickOutside'; import { PressTipRoot } from '../PressTip'; import { usePopper } from '../usePopper'; import ColorDestinySymbols from './ColorDestinySymbols'; import * as styles from './SymbolsPicker.m.scss'; import { symbolsSelector } from './destiny-symbols'; const symbolsIcon = String.fromCodePoint(FontGlyphs.gilded_title); /** * Decorate an <input> or <textarea> with an emoji picker button to pick Destiny symbols */ export function WithSymbolsPicker<T extends HTMLTextAreaElement | HTMLInputElement>({ input, setValue, className, children, }: { input: React.RefObject<T | null>; setValue: (val: string) => void; // NB no matter the type here TS/JSX cannot enforce that only a T is used here... children: React.ReactElement<HTMLProps<T>>; className?: string; }) { return ( <div className={clsx(className, styles.wrapperDiv)}> <> {children} <div className={styles.buttonDiv}> <SymbolsPickerButton input={input} setValue={setValue} /> </div> </> </div> ); } const SymbolsWindow = memo(function ({ onChooseGlyph, }: { onChooseGlyph: (unicode: string) => void; }) { const allSymbols = useSelector(symbolsSelector); const [query, setQuery] = useState(''); const [preview, setPreview] = useState<(typeof allSymbols)[number] | undefined>(undefined); return ( <> {/* explicitly eat all click events so that clicking in the window doesn't dismiss the item popup */} <div className={styles.symbolsWindow} onClick={(e) => e.stopPropagation()}> <div className={styles.symbolsSearch}> <SearchInput query={query} onQueryChanged={setQuery} placeholder={t('Glyphs.SearchSymbols')} /> </div> <div className={styles.symbolsBody}> <div className={styles.symbolsContainer}> {allSymbols.map( (emoji) => (emoji.fullName.includes(query) || emoji.name.includes(query)) && ( <button className={styles.emojiButton} type="button" key={emoji.glyph} onClick={(e) => { e.stopPropagation(); onChooseGlyph(emoji.glyph); }} onPointerEnter={() => setPreview(emoji)} > {emoji.glyph} </button> ), )} </div> </div> <div className={styles.symbolsFooter}> <ColorDestinySymbols text={preview?.glyph ?? symbolsIcon} /> {preview && ( <div> <span>{preview.fullName}</span> <span>:{preview.name}:</span> </div> )} </div> </div> </> ); }); function SymbolsPickerButton<T extends HTMLTextAreaElement | HTMLInputElement>({ input, setValue, }: { input?: React.RefObject<T | null>; setValue: (val: string) => void; }) { const controlRef = useRef<HTMLButtonElement>(null); const tooltipContents = useRef<HTMLDivElement>(null); const [open, setOpen] = useState(false); const pressTipRoot = use(PressTipRoot); usePopper( { contents: tooltipContents, reference: controlRef, arrowClassName: '', placement: 'top', }, [open, pressTipRoot], ); // A user should be able to click multiple symbols to insert multiple symbols sequentially, // so we need to internally maintain where the cursor is (even when the element isn't actually focused) const [insertionIndex, setInsertionIndex] = useState<number | null>(null); const updateInsertionIndex = useCallback(() => { setInsertionIndex(input?.current?.selectionStart ?? null); }, [input]); useEffect(() => { const i = input?.current; const listener = updateInsertionIndex; i?.addEventListener('blur', listener); return () => i?.removeEventListener('blur', listener); }); const onChooseGlyph = useCallback( (symbol: string) => { const i = input?.current; if (i) { const inputText = i.value; const insIndex = insertionIndex ?? inputText.length; setValue(inputText.slice(0, insIndex) + symbol + inputText.slice(insIndex)); setInsertionIndex(insIndex + symbol.length); } }, [input, insertionIndex, setValue], ); return ( <> <button type="button" ref={controlRef} className={styles.symbolsButton} onClick={() => setOpen(!open)} title={t('Glyphs.OpenSymbolsPicker')} > <span>{symbolsIcon}</span> </button> {open && createPortal( <div ref={tooltipContents}> <ClickOutside onClickOutside={() => setOpen(false)}> <SymbolsWindow onChooseGlyph={onChooseGlyph} /> </ClickOutside> </div>, pressTipRoot.current ?? tempContainer, )} </> ); } ================================================ FILE: src/app/dim-ui/destiny-symbols/destiny-symbols.test.ts ================================================ import { RootState } from 'app/store/types'; import { symbolData } from 'data/font/symbol-name-sources'; import { getTestDefinitions, setupi18n } from 'testing/test-utils'; import { symbolsSelector } from './destiny-symbols'; import { conversionTableSelector } from './rich-destiny-text'; test('dim-custom-symbols symbols map', async () => { setupi18n(); const defs = await getTestDefinitions(); for (const { codepoint, source } of symbolData) { if (!source?.fromRichText) { continue; } const { tableName, hash } = source; const def = tableName === 'Objective' ? defs.Objective.get(hash) : tableName === 'SandboxPerk' ? defs.SandboxPerk.get(hash) : undefined; if (!def) { throw new Error( `No definition exists for ${codepoint}: ${JSON.stringify(source)}. You may need to update the dim-custom-symbols repo.`, ); } } const conversionTable = conversionTableSelector({ manifest: { d2Manifest: defs } } as RootState); for (const [key, value] of Object.entries(conversionTable)) { if (!value) { throw new Error(`Conversion table entry doesn't exist for ${key}`); } if (!value.plaintext || value.plaintext.length < 3) { throw new Error( `Conversion table entry ${key} has no plaintext. You may need to update the dim-custom-symbols repo.`, ); } if (!value.unicode) { throw new Error( `Conversion table entry ${key} has no unicode. You may need to update the dim-custom-symbols repo.`, ); } } const symbolsMap = symbolsSelector({ manifest: { d2Manifest: defs } } as RootState); for (const value of symbolsMap) { if (!value.name) { throw new Error( `Symbol ${JSON.stringify(value)} has no name. You may need to update the dim-custom-symbols repo.`, ); } if (!value.fullName) { throw new Error( `Symbol ${JSON.stringify(value)} has no fullName. You may need to update the dim-custom-symbols repo.`, ); } } }); ================================================ FILE: src/app/dim-ui/destiny-symbols/destiny-symbols.ts ================================================ import { I18nKey, t, tl } from 'app/i18next-t'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { StringLookup } from 'app/utils/util-types'; import { FontGlyphs } from 'data/font/d2-font-glyphs'; import { DimCustomSymbols } from 'data/font/dim-custom-symbols'; import { TranslateManually, symbolData } from 'data/font/symbol-name-sources'; import { createSelector } from 'reselect'; import { conversionTableSelector } from './rich-destiny-text'; const manualTranslations: { [key in TranslateManually]: I18nKey } = { // t('Glyphs.Smoke') Let's keep this for a bit [FontGlyphs.gilded_title]: tl('Glyphs.Gilded'), [FontGlyphs.environment_hazard]: tl('Glyphs.Misadventure'), [FontGlyphs.void_quickfall]: tl('Glyphs.Quickfall'), [FontGlyphs.spear_launcher]: tl('Glyphs.ScorchCannon'), [DimCustomSymbols.hive_relic]: tl('Glyphs.HiveSword'), [FontGlyphs.light]: tl('Glyphs.LightLevel'), [DimCustomSymbols.harmonic]: tl('Glyphs.Harmonic'), [DimCustomSymbols.respawn_restricted]: tl('Glyphs.RespawnRestricted'), [DimCustomSymbols.prismatic]: tl('Glyphs.Prismatic'), [FontGlyphs.void_titan_axe_throw_relic]: tl('Glyphs.Axe'), [FontGlyphs.light_ability]: tl('Glyphs.LightAbility'), [FontGlyphs.darkness_ability]: tl('Glyphs.DarkAbility'), }; export type SymbolsMap = { glyph: string; name: string; fullName: string }[]; const simplifyName = (name: string) => name .toLowerCase() .replace(/[\s-]+/g, '_') .replace(/[^\p{L}_]/gu, ''); export const symbolsSelector = createSelector( d2ManifestSelector, conversionTableSelector, (defs, richTextReplacements) => { const list: SymbolsMap = []; if (!defs) { return list; } for (const { codepoint, glyph, source } of symbolData) { const manualTranslation = (manualTranslations as StringLookup<I18nKey>)[codepoint]; if (manualTranslation) { const hardCodedName = t(manualTranslation); list.push({ glyph, fullName: hardCodedName, name: simplifyName(hardCodedName), }); continue; } const richTextRepl = richTextReplacements[glyph]; if (richTextRepl) { const fullName = richTextRepl.plaintext.slice(1, -1).trim(); list.push({ glyph, fullName, name: simplifyName(fullName) }); continue; } if (source) { const defName = source.tableName === 'Objective' ? defs[source.tableName].get(source.hash)?.progressDescription : defs[source.tableName].get(source.hash)?.displayProperties?.name; if (defName) { list.push({ glyph, fullName: defName, name: simplifyName(defName) }); continue; } } list.push({ glyph, fullName: t('Glyphs.Missing'), name: simplifyName(t('Glyphs.Missing')), }); } return list; }, ); ================================================ FILE: src/app/dim-ui/destiny-symbols/rich-destiny-text.ts ================================================ import { d2ManifestSelector } from 'app/manifest/selectors'; import { symbolData } from 'data/font/symbol-name-sources'; import { createSelector } from 'reselect'; import { D2ManifestDefinitions } from '../../destiny2/d2-definitions'; // matches a bracketed thing in the string export const iconPlaceholder = /(\[[^\]]+\])/g; // this table converts manifest strings to their appropriate special unicode characters // (and special unicode characters to themselves, but formatted) // for example, if we were only setting this up for rocket launchers, with DIM set to english, // generateConversionTable would end up outputting this: // { // "[Rocket Launcher]": { plaintext:"[Rocket Launcher]", unicode:"" }, // "": { plaintext:"[Rocket Launcher]", unicode:"" }, // } export type RichTextConversionTable = NodeJS.Dict<{ unicode: string; plaintext: string; }>; /** * given defs, uses known examples from the manifest * and returns a localized string-to-font glyph conversion table * "[Rocket Launcher]" -> "" */ export const conversionTableSelector = createSelector( d2ManifestSelector, (defs: D2ManifestDefinitions | undefined) => { const conversionTable: RichTextConversionTable = {}; if (!defs) { return conversionTable; } for (const { glyph, source } of symbolData) { if (!source?.fromRichText) { continue; } const unicode = glyph; const { tableName, hash } = source; const localizedString = tableName === 'Objective' ? defs.Objective.get(hash)?.progressDescription : tableName === 'SandboxPerk' ? defs.SandboxPerk.get(hash)?.displayProperties.description : undefined; // find just the text segment that says "[Rocket Launcher]" in current language const progressDescriptionMatch = localizedString?.match(iconPlaceholder)?.[0]; // data we'll need later to render a glyph and give it a title attribute const thisReplacementInfo = { unicode, plaintext: progressDescriptionMatch ?? unicode }; // insert it into the lookup table, keyed by the unicode character conversionTable[unicode] = thisReplacementInfo; // and also keyed by the matching string, if we found one if (progressDescriptionMatch) { conversionTable[progressDescriptionMatch] = thisReplacementInfo; } } return conversionTable; }, ); ================================================ FILE: src/app/dim-ui/dim-button.scss ================================================ @use '../variables.scss' as *; /* Button */ @mixin drop-shadow { filter: drop-shadow(0 0 1px rgb(0, 0, 0, 0.25)); filter: drop-shadow( 0 0 1px color-mix(in srgb, var(--theme-accent-primary), rgb(0, 0, 0, 0.5) 70%) ); } @layer base { .dim-button { cursor: pointer; padding: 4px 10px; min-height: 26px; display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 3px; background-color: var(--theme-button-bg); color: var(--theme-button-txt); font-size: 12px; line-height: calc(16 / 12); font-family: 'Open Sans', sans-serif, 'Destiny Symbols'; text-shadow: 1px 1px 3px rgb(0, 0, 0, 0.25); border: 1px solid transparent; box-sizing: border-box; text-align: center; // Slightly bigger hit targets on mobile @include phone-portrait { font-size: 14px; line-height: calc(18 / 14); padding: 8px 16px; } @include interactive($hover: true, $active: true, $selectedClass: '.selected') { background-color: var(--theme-accent-primary) !important; color: var(--theme-text-invert) !important; text-shadow: none; // Invert images on buttons to match text color, unless they have the dontInvert class // (e.g. colored icons in organizer/compare) img:not(.dontInvert) { filter: invert(1); } img, svg { &.dontInvert { @include drop-shadow; } } } img, svg { display: block; height: 1.3em; width: auto; vertical-align: bottom; &:only-child { margin: 0; } // Don't invert images/icons on buttons when they're not monochrome // e.g. kinetic slot filters in compare/organizer, or material cost for mod preview &.dontInvert { @include drop-shadow; } } // Organizer embeds radio buttons in here input { display: none; } &[disabled] { opacity: 0.5; cursor: not-allowed; @include interactive($hover: true, $active: true, $selectedClass: '.selected') { background-color: var(--theme-button-bg); color: var(--theme-text); img, svg { @include drop-shadow; } } } &.danger { @include interactive($hover: true) { background-color: $red !important; } } &.left { justify-content: flex-start; } // Set focus styles &:focus-visible { border-color: var(--theme-accent-primary); outline: none; } } a.dim-button { text-decoration: none; } .dim-button-primary { background-color: var(--theme-button-bg-primary); font-weight: bold; } } ================================================ FILE: src/app/dim-ui/scroll.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import * as styles from './ItemPop.m.scss'; /** * Cross browser safe scrollTo implementation. */ export function scrollToPosition(options: ScrollToOptions) { const isSmoothScrollSupported = 'scrollBehavior' in document.documentElement.style; if (isSmoothScrollSupported) { window.scroll(options); } else { document.scrollingElement!.scrollTop = options.top!; } } /** * Scroll to an item tile and make it briefly zoom/wobble for attention */ export const itemPop = (item: DimItem) => { // TODO: this is tough to do with an ID since we'll have multiple const element = document.getElementById(item.index)?.parentNode as HTMLElement; if (!element) { throw new Error(`No element with id ${item.index}`); } const elementRect = element.getBoundingClientRect(); const html = document.querySelector('html')!; const headerHeight = parseInt(html.style.getPropertyValue('--header-height'), 10); const storeHeaderHeight = parseInt(html.style.getPropertyValue('--store-header-height'), 10); const absoluteElementTop = elementRect.top + window.pageYOffset; scrollToPosition({ left: 0, top: absoluteElementTop - (headerHeight + storeHeaderHeight + 12) }); element.classList.add(styles.itemPop); const removePop = () => { element.classList.remove(styles.itemPop); for (const event of ['webkitAnimationEnd', 'oanimationend', 'msAnimationEnd', 'animationend']) { element.removeEventListener(event, removePop); } }; for (const event of ['webkitAnimationEnd', 'oanimationend', 'msAnimationEnd', 'animationend']) { element.addEventListener(event, removePop); } }; ================================================ FILE: src/app/dim-ui/sheets-open.ts ================================================ /** * The total number of sheets that are open. Used by the sneaky updates code to * determine if the user is in the middle of something. * * This is in a separate file from Sheet.tsx to avoid pulling in dependencies to * tests. */ export const sheetsOpen = { open: 0, }; ================================================ FILE: src/app/dim-ui/svgs/BucketIcon.tsx ================================================ import { d2MissingIcon } from 'app/search/d2-known-values'; import clsx from 'clsx'; import { BucketHashes, ItemCategoryHashes } from 'data/d2/generated-enums'; import React from 'react'; import BungieImage from '../BungieImage'; import { ItemCategoryIcon, getBucketSvgIcon, itemCategoryIcons } from './itemCategory'; type BucketIconProps = React.SVGProps<SVGSVGElement> & React.ImgHTMLAttributes<HTMLImageElement> & ( | { icon: ItemCategoryIcon; } | { bucketHash: BucketHashes; } | { itemCategoryHash: ItemCategoryHashes; } ); function resolveIcon(props: BucketIconProps) { if ('icon' in props) { const { icon, ...otherProps } = props; return { icon, otherProps, }; } else if ('bucketHash' in props) { const { bucketHash, ...otherProps } = props; return { icon: getBucketSvgIcon(bucketHash), otherProps, }; } else { const { itemCategoryHash, ...otherProps } = props; return { icon: itemCategoryIcons[itemCategoryHash], otherProps, }; } } /** returns an img corresponding to the specified bucket or item category */ export default function BucketIcon(props: BucketIconProps) { const resolved = resolveIcon(props); return resolved.icon ? ( <resolved.icon.svg {...resolved.otherProps} className={clsx(props.className, { dontInvert: resolved.icon.colorized, })} /> ) : ( <BungieImage src={d2MissingIcon} {...resolved.otherProps} /> ); } ================================================ FILE: src/app/dim-ui/svgs/itemCategory.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import { LookupTable } from 'app/utils/util-types'; import { BucketHashes, ItemCategoryHashes } from 'data/d2/generated-enums'; import legs from 'destiny-icons/armor_types/boots.svg?react'; import chest from 'destiny-icons/armor_types/chest.svg?react'; import classItem from 'destiny-icons/armor_types/class.svg?react'; import gauntlets from 'destiny-icons/armor_types/gloves.svg?react'; import helmet from 'destiny-icons/armor_types/helmet.svg?react'; import heavyAmmo from 'destiny-icons/general/ammo-heavy.svg?react'; import hunter from 'destiny-icons/general/class_hunter.svg?react'; import titan from 'destiny-icons/general/class_titan.svg?react'; import warlock from 'destiny-icons/general/class_warlock.svg?react'; import emblem from 'destiny-icons/general/emblem.svg?react'; import ghost from 'destiny-icons/general/ghost.svg?react'; import ship from 'destiny-icons/general/ship.svg?react'; import sparrow from 'destiny-icons/general/sparrow.svg?react'; import autoRifle from 'destiny-icons/weapons/auto_rifle.svg?react'; import traceRifle from 'destiny-icons/weapons/beam_weapon.svg?react'; import bow from 'destiny-icons/weapons/bow.svg?react'; import fusionRifle from 'destiny-icons/weapons/fusion_rifle.svg?react'; import glaive from 'destiny-icons/weapons/glaive.svg?react'; import gLauncher_special from 'destiny-icons/weapons/grenade_launcher-field_forged.svg?react'; import gLauncher from 'destiny-icons/weapons/grenade_launcher.svg?react'; import handCannon from 'destiny-icons/weapons/hand_cannon.svg?react'; import machinegun from 'destiny-icons/weapons/machinegun.svg?react'; import pulseRifle from 'destiny-icons/weapons/pulse_rifle.svg?react'; import rLauncher from 'destiny-icons/weapons/rocket_launcher.svg?react'; import scoutRifle from 'destiny-icons/weapons/scout_rifle.svg?react'; import shotgun from 'destiny-icons/weapons/shotgun.svg?react'; import sidearm from 'destiny-icons/weapons/sidearm.svg?react'; import smg from 'destiny-icons/weapons/smg.svg?react'; import sniperRifle from 'destiny-icons/weapons/sniper_rifle.svg?react'; import sword from 'destiny-icons/weapons/sword_heavy.svg?react'; import lFusionRifle from 'destiny-icons/weapons/wire_rifle.svg?react'; import energyWeaponSlot from 'images/weapon-slot-energy.svg?react'; import kineticWeaponSlot from 'images/weapon-slot-kinetic.svg?react'; import React from 'react'; export interface ItemCategoryIcon { svg: React.FC<React.SVGProps<SVGSVGElement>>; colorized: boolean; } function monochrome(svg: React.FC<React.SVGProps<SVGSVGElement>>): ItemCategoryIcon { return { svg, colorized: false }; } function colorized(svg: React.FC<React.SVGProps<SVGSVGElement>>): ItemCategoryIcon { return { svg, colorized: true }; } const weaponTypeSvgByCategoryHash: LookupTable<ItemCategoryHashes, ItemCategoryIcon> = { [ItemCategoryHashes.AutoRifle]: monochrome(autoRifle), [ItemCategoryHashes.HandCannon]: monochrome(handCannon), [ItemCategoryHashes.PulseRifle]: monochrome(pulseRifle), [ItemCategoryHashes.ScoutRifle]: monochrome(scoutRifle), [ItemCategoryHashes.FusionRifle]: monochrome(fusionRifle), [ItemCategoryHashes.SniperRifle]: monochrome(sniperRifle), [ItemCategoryHashes.Shotgun]: monochrome(shotgun), [ItemCategoryHashes.MachineGun]: monochrome(machinegun), [ItemCategoryHashes.RocketLauncher]: monochrome(rLauncher), [ItemCategoryHashes.Sidearm]: monochrome(sidearm), [ItemCategoryHashes.Sword]: monochrome(sword), [ItemCategoryHashes.GrenadeLaunchers]: monochrome(gLauncher), [-ItemCategoryHashes.GrenadeLaunchers]: monochrome(gLauncher_special), [ItemCategoryHashes.TraceRifles]: monochrome(traceRifle), [ItemCategoryHashes.LinearFusionRifles]: monochrome(lFusionRifle), [ItemCategoryHashes.SubmachineGuns]: monochrome(smg), [ItemCategoryHashes.Bows]: monochrome(bow), [ItemCategoryHashes.Glaives]: monochrome(glaive), }; const weaponSlotSvgByCategoryHash: LookupTable<ItemCategoryHashes, ItemCategoryIcon> = { [ItemCategoryHashes.KineticWeapon]: colorized(kineticWeaponSlot), [ItemCategoryHashes.EnergyWeapon]: colorized(energyWeaponSlot), [ItemCategoryHashes.PowerWeapon]: colorized(heavyAmmo), }; const armorSlotSvgByCategoryHash: LookupTable<ItemCategoryHashes, ItemCategoryIcon> = { [ItemCategoryHashes.Helmets]: monochrome(helmet), [ItemCategoryHashes.Arms]: monochrome(gauntlets), [ItemCategoryHashes.Chest]: monochrome(chest), [ItemCategoryHashes.Legs]: monochrome(legs), [ItemCategoryHashes.ClassItems]: monochrome(classItem), }; /** * A mapping from known item category hashes to an appropriate icon */ export const itemCategoryIcons: LookupTable<ItemCategoryHashes, ItemCategoryIcon> = { ...armorSlotSvgByCategoryHash, ...weaponSlotSvgByCategoryHash, ...weaponTypeSvgByCategoryHash, [ItemCategoryHashes.Weapon]: monochrome(handCannon), [ItemCategoryHashes.Ghost]: monochrome(ghost), [ItemCategoryHashes.Sparrows]: monochrome(sparrow), [ItemCategoryHashes.Ships]: monochrome(ship), [ItemCategoryHashes.Emblems]: monochrome(emblem), [ItemCategoryHashes.Hunter]: monochrome(hunter), [ItemCategoryHashes.Titan]: monochrome(titan), [ItemCategoryHashes.Warlock]: monochrome(warlock), } as const; /** A mapping from bucket hash to item category */ const bucketHashToItemCategoryHash: LookupTable<BucketHashes, ItemCategoryHashes> = { [BucketHashes.KineticWeapons]: ItemCategoryHashes.KineticWeapon, [BucketHashes.EnergyWeapons]: ItemCategoryHashes.EnergyWeapon, [BucketHashes.PowerWeapons]: ItemCategoryHashes.PowerWeapon, [BucketHashes.Helmet]: ItemCategoryHashes.Helmets, [BucketHashes.Gauntlets]: ItemCategoryHashes.Arms, [BucketHashes.ChestArmor]: ItemCategoryHashes.Chest, [BucketHashes.LegArmor]: ItemCategoryHashes.Legs, [BucketHashes.ClassArmor]: ItemCategoryHashes.ClassItems, [BucketHashes.Ghost]: ItemCategoryHashes.Ghost, [BucketHashes.Vehicle]: ItemCategoryHashes.Sparrows, [BucketHashes.Ships]: ItemCategoryHashes.Ships, [BucketHashes.Emblems]: ItemCategoryHashes.Emblems, } as const; /** an SVG of the weapon's type, if determinable */ export function getWeaponTypeSvgIconFromCategoryHashes(itemCategoryHashes: ItemCategoryHashes[]) { // reverse through the ICHs because most specific is last, // i.e. Weapon, Fusion Rifle, Linear Fusion Rifle for (const ich of itemCategoryHashes.toReversed()) { const svg = weaponTypeSvgByCategoryHash[ich]; if (svg) { return svg; } } } /** an SVG of the weapon's type, if determinable */ export function getWeaponTypeSvgIcon(item: DimItem) { return getWeaponTypeSvgIconFromCategoryHashes(item.itemCategoryHashes); } /** an SVG of the weapon's slot, if possible */ export function getWeaponSlotSvgIcon(item: DimItem) { for (const ich of item.itemCategoryHashes.toReversed()) { const svg = weaponSlotSvgByCategoryHash[ich]; if (svg) { return svg; } } } /** an SVG of the armor's slot, if determinable */ export function getArmorSlotSvgIcon(item: DimItem) { for (const ich of item.itemCategoryHashes.toReversed()) { const svg = armorSlotSvgByCategoryHash[ich]; if (svg) { return svg; } } } /** an SVG of the bucket's icon, if determinable */ export function getBucketSvgIcon(bucketHash: BucketHashes) { const ich = bucketHashToItemCategoryHash[bucketHash]; if (ich) { return itemCategoryIcons[ich]; } } ================================================ FILE: src/app/dim-ui/table-columns.test.ts ================================================ import { act, renderHook } from '@testing-library/react'; import { SortDirection, useTableColumnSorts } from './table-columns'; describe('useTableColumnSorts', () => { it('use cases', () => { const { result } = renderHook(() => useTableColumnSorts([])); // Toggle on one column act(() => { const [_columnSorts, toggleColumnSort] = result.current; toggleColumnSort('one', false, SortDirection.ASC)(); }); let [columnSorts] = result.current; expect(columnSorts).toEqual([{ columnId: 'one', sort: SortDirection.ASC }]); // Now toggle that same column act(() => { const [_columnSorts, toggleColumnSort] = result.current; toggleColumnSort('one', false, SortDirection.ASC)(); }); // Now we should see the sort inverted [columnSorts] = result.current; expect(columnSorts).toEqual([{ columnId: 'one', sort: SortDirection.DESC }]); // Now toggle another column act(() => { const [_columnSorts, toggleColumnSort] = result.current; toggleColumnSort('two', false, SortDirection.ASC)(); }); // Now we should see only the second column [columnSorts] = result.current; expect(columnSorts).toEqual([{ columnId: 'two', sort: SortDirection.ASC }]); // Toggle the first column, but with additive=true act(() => { const [_columnSorts, toggleColumnSort] = result.current; toggleColumnSort('one', true, SortDirection.ASC)(); }); // Now we should see both columns [columnSorts] = result.current; expect(columnSorts).toEqual([ { columnId: 'two', sort: SortDirection.ASC }, { columnId: 'one', sort: SortDirection.ASC }, ]); // Toggle a third column with additive=false, but this one has a different default sort act(() => { const [_columnSorts, toggleColumnSort] = result.current; toggleColumnSort('three', false, SortDirection.DESC)(); }); // Now we should see only the third column, but with its default sort [columnSorts] = result.current; expect(columnSorts).toEqual([{ columnId: 'three', sort: SortDirection.DESC }]); }); }); ================================================ FILE: src/app/dim-ui/table-columns.ts ================================================ import { useCallback, useState } from 'react'; export interface ColumnSort { readonly columnId: string; readonly sort: SortDirection; } export const enum SortDirection { ASC, DESC, } export function useTableColumnSorts(defaultSorts: ColumnSort[]) { const [columnSorts, setColumnSorts] = useState<ColumnSort[]>(defaultSorts); // Toggle sorting of columns. If shift is held (the additive param), adds this column to the sort. const toggleColumnSort = useCallback( (columnId: string, additive: boolean, defaultDirection: SortDirection = SortDirection.ASC) => () => setColumnSorts((sorts) => { const newColumnSorts = additive ? Array.from(sorts) // start with a copy of the existing sorts : sorts.filter((s) => s.columnId === columnId); // otherwise just this column const index = newColumnSorts.findIndex((s) => s.columnId === columnId); if (index >= 0) { // This column is already in the sort, flip the direction const columnSort = newColumnSorts[index]; if (columnSort.sort === defaultDirection || !additive) { newColumnSorts[index] = { ...columnSort, sort: columnSort.sort === SortDirection.ASC ? SortDirection.DESC : SortDirection.ASC, }; } else { newColumnSorts.splice(index, 1); // remove the column from the sort } } else { // Add the column to the sort newColumnSorts.push({ columnId: columnId, sort: defaultDirection, }); } return newColumnSorts; }), [], ); return [columnSorts, toggleColumnSort] as const; } ================================================ FILE: src/app/dim-ui/text-complete/text-complete.m.scss ================================================ @use '../../variables.scss' as *; .dropdownMenu { color: black; background-color: white; border-radius: 4px; overflow: hidden; list-style: none; padding: 0; margin: 0; @include phone-portrait { a { font-size: 16px; } li { padding: 8px 10px; } } :global(.textcomplete-header), :global(.textcomplete-footer) { display: none; } li { padding: 4px 8px; @include interactive($hover: true) { background-color: #e8a534; } &:nth-child(2) { border-top: none; } } :global(.active) { background-color: #e8a534; } a { color: black; font-size: 12px; text-decoration-line: none; @include interactive($hover: true) { cursor: pointer; } } } ================================================ FILE: src/app/dim-ui/text-complete/text-complete.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'dropdownMenu': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/text-complete/text-complete.ts ================================================ import { StrategyProps, Textcomplete } from '@textcomplete/core'; import { TextareaEditor } from '@textcomplete/textarea'; import { getHashtagsFromString } from 'app/inventory/note-hashtags'; import clsx from 'clsx'; import { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { SymbolsMap, symbolsSelector } from '../destiny-symbols/destiny-symbols'; import { tempContainer } from 'app/utils/temp-container'; import * as styles from './text-complete.m.scss'; function createTagsCompleter( textArea: React.RefObject<HTMLTextAreaElement | HTMLInputElement | null>, tags: string[], ): StrategyProps { return { match: /#(\w*)$/, search: (term, callback) => { const termLower = term.toLowerCase(); // need to build this list from the element ref, because relying // on liveNotes state would re-instantiate Textcomplete every keystroke const existingTags = getHashtagsFromString(textArea.current!.value).map((t) => t.toLowerCase(), ); const possibleTags: string[] = []; for (const t of tags) { const tagLower = t.toLowerCase(); // don't suggest duplicate tags if (existingTags.includes(tagLower)) { continue; } // favor startswith if (tagLower.startsWith(`#${termLower}`)) { possibleTags.unshift(t); // over full text search } else if (tagLower.includes(termLower)) { possibleTags.push(t); } } callback(possibleTags); }, replace: (key) => `${key} `, // to-do: for major tags, gonna use this to show what the notes icon will change to // template: (key) => `<img src="${url}"/> <small>:${key}:</small>`, }; } function createSymbolsAutocompleter(symbols: SymbolsMap): StrategyProps { return { match: /\B:(\p{L}*)$/u, search: (term, callback) => { const termLower = term.toLowerCase(); const possibleTags: [string, string][] = []; for (const t of symbols) { const tagLower = t.name; // favor startswith if (tagLower.startsWith(termLower)) { possibleTags.unshift([t.glyph, tagLower]); // over full text search } else if (tagLower.includes(termLower)) { possibleTags.push([t.glyph, tagLower]); } } callback(possibleTags); }, template: ([glyph, name]) => `${glyph} :${name}:`, replace: ([glyph]) => `${glyph} `, }; } /** * Autocomplete a list of hashtags in this <textarea /> or <input type="text" />. * `tags` must have a stable object identity when using this hook (unless the set of tags changes). * selectors should ensure this, useMemo doesn't guarantee it per contract but works now. * * When using an input, set an appropriate line-height so that textcomplete doesn't fall back to a slow path... */ export function useAutocomplete( textArea: React.RefObject<HTMLTextAreaElement | HTMLInputElement | null>, tags: string[], parent?: React.RefObject<HTMLElement | null>, ) { const symbols = useSelector(symbolsSelector); useEffect(() => { if (textArea.current) { // commit a type crime here because textcomplete says it only works with // TextArea but happens to also work entirely fine with Input[type=text] // https://github.com/yuku/textcomplete/issues/355 const editor = new TextareaEditor(textArea.current as unknown as HTMLTextAreaElement); const textcomplete = new Textcomplete( editor, [createTagsCompleter(textArea, tags), createSymbolsAutocompleter(symbols)], { dropdown: { className: clsx(styles.dropdownMenu, 'textcomplete-dropdown'), parent: parent?.current ?? tempContainer, }, }, ); return () => { textcomplete.destroy(); }; } }, [parent, symbols, tags, textArea]); } ================================================ FILE: src/app/dim-ui/useBulkNote.m.scss ================================================ @use '../variables' as *; .form { position: relative; } .body { display: flex; flex-direction: column; gap: 8px; } .radios { display: flex; flex-direction: column; gap: 8px; } .preview { margin-top: 8px; display: grid; grid-template-columns: min-content 1fr; gap: 4px 8px; > :nth-child(2n + 1) { font-weight: bold; } } ================================================ FILE: src/app/dim-ui/useBulkNote.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'body': string; 'form': string; 'preview': string; 'radios': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/useBulkNote.tsx ================================================ import { calculateElementOffset } from '@textcomplete/utils'; import { Option } from 'app/dim-ui/RadioButtons'; import { t } from 'app/i18next-t'; import { appendNote, removeFromNote, setNote } from 'app/inventory/actions'; import { DimItem } from 'app/inventory/item-types'; import { appendedToNote, removedFromNote } from 'app/inventory/note-hashtags'; import { allNotesHashtagsSelector, getNotesSelector } from 'app/inventory/selectors'; import { maxLength } from 'app/item-popup/NotesArea'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { isWindows, isiOSBrowser } from 'app/utils/browsers'; import { useCallback, useLayoutEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import TextareaAutosize from 'react-textarea-autosize'; import { PressTipRoot } from './PressTip'; import { WithSymbolsPicker } from './destiny-symbols/SymbolsPicker'; import { useAutocomplete } from './text-complete/text-complete'; import * as styles from './useBulkNote.m.scss'; import useDialog, { Body, Buttons, Title } from './useDialog'; export interface BulkNoteResult { note: string; /** * 'replace' replaces the entire note with the new note. * 'append' adds the new note to the end of the existing note. * 'remove' removes any part of the note that matches the new note. */ appendMode: 'replace' | 'append' | 'remove'; } export default function useBulkNote(): [ element: React.ReactNode, bulkNote: (items: DimItem[]) => Promise<void>, ] { const [dialog, showDialog] = useDialog<DimItem[], BulkNoteResult | null>((args, close) => ( <BulkNoteDialog close={close} items={args} /> )); const dispatch = useThunkDispatch(); const bulkNote = useCallback( async (items: DimItem[]) => { const note = await showDialog(items); if (note !== null && items?.length) { for (const item of items) { switch (note.appendMode) { case 'replace': dispatch(setNote(item, note.note)); break; case 'append': { dispatch(appendNote(item, note.note)); break; } case 'remove': dispatch(removeFromNote(item, note.note)); break; } } } }, [dispatch, showDialog], ); return [dialog, bulkNote]; } function BulkNoteDialog({ close, items, }: { items: DimItem[]; close: (result: BulkNoteResult | null) => void; }) { const [note, setNote] = useState(''); const [appendMode, setAppendMods] = useState<BulkNoteResult['appendMode']>('replace'); const cancel = useCallback(() => close(null), [close]); const ok = useCallback(() => close({ note, appendMode }), [appendMode, close, note]); const okButton = ( <button className="dim-button dim-button-primary" type="button" onClick={ok}> {t('BulkNote.Confirm')} </button> ); const cancelButton = ( <button className="dim-button" type="button" onClick={cancel}> {t('Dialog.Cancel')} </button> ); const radioOptions: Option<BulkNoteResult['appendMode']>[] = [ { value: 'replace', label: t('BulkNote.Replace'), }, { value: 'append', label: t('BulkNote.Append'), }, { value: 'remove', label: t('BulkNote.Remove'), }, ]; const getNote = useSelector(getNotesSelector); const exemplar = items.find((i) => i.taggable && getNote(i)) ?? items.find((i) => i.taggable); const originalNote = exemplar && getNote(exemplar); const updatedNote = appendMode === 'replace' ? note : appendMode === 'append' ? appendedToNote(originalNote, note) : removedFromNote(originalNote, note); return ( <> <Title> <h2>{t('BulkNote.Title', { count: items.length })}</h2>
{radioOptions.map((o) => ( ))}
{exemplar && (
Before:
{originalNote}
After:
{updatedNote}
)} {isWindows() ? ( <> {cancelButton} {okButton} ) : ( <> {okButton} {cancelButton} )} ); } // TODO: Recombine with the one in NotesArea which has a lot of extra weird stuff function NotesEditor({ notes, onNotesChanged, }: { notes: string; onNotesChanged: (notes: string) => void; }) { const textArea = useRef(null); const form = useRef(null); const isPhonePortrait = useIsPhonePortrait(); const tags = useSelector(allNotesHashtagsSelector); useAutocomplete(textArea, tags, form); // On iOS at least, focusing the keyboard pushes the content off the screen const nativeAutoFocus = !isPhonePortrait && !isiOSBrowser(); const handleChange = (e: React.ChangeEvent) => onNotesChanged(e.target.value); // This effect brute-force compensates for the fact that the textcomplete // library doesn't set its offset relative to its closest positioned parent, // but rather always from the document. It may be time to fork or replace the // textcomplete lib. useLayoutEffect(() => { if (!form.current) { return; } const offsets = calculateElementOffset(form.current); const dropdown = form.current.querySelector('.textcomplete-dropdown'); if (dropdown && dropdown instanceof HTMLElement && dropdown.style.left) { dropdown.style.left = `${parseInt(dropdown.style.left, 10) - offsets.left}px`; dropdown.style.top = `${parseInt(dropdown.style.top, 10) - offsets.top}px`; } }, [notes]); return (
onNotesChanged(val)}>
); } ================================================ FILE: src/app/dim-ui/useConfirm.tsx ================================================ import { t } from 'app/i18next-t'; import { isWindows } from 'app/utils/browsers'; import { useCallback } from 'react'; import useDialog, { Buttons, Title } from './useDialog'; export interface ConfirmOpts { okLabel?: React.ReactNode; cancelLabel?: React.ReactNode; } /** * Replacement for window.confirm, returns an element you need to render, and a * confirm function you can use to show confirm dialogs. */ export default function useConfirm(): [ element: React.ReactNode, confirm: (message: React.ReactNode, opts?: ConfirmOpts) => Promise, ] { const [dialog, showDialog] = useDialog< ConfirmOpts & { message: React.ReactNode; }, boolean >((args, close) => ( )); const confirm = (message: React.ReactNode, opts?: ConfirmOpts) => showDialog({ message, ...opts }); return [dialog, confirm]; } function ConfirmDialog({ message, okLabel, cancelLabel, close, }: { message: React.ReactNode; okLabel?: React.ReactNode; cancelLabel?: React.ReactNode; close: (result: boolean) => void; }) { const cancel = useCallback(() => close(false), [close]); const ok = useCallback(() => close(true), [close]); const okButton = ( ); const cancelButton = ( ); return ( <> {message} {isWindows() ? ( <> {cancelButton} {okButton} ) : ( <> {okButton} {cancelButton} )} ); } ================================================ FILE: src/app/dim-ui/useDialog.m.scss ================================================ @use '../variables.scss' as *; .dialog { position: fixed; background-color: #08080f; color: var(--theme-text); top: 0; top: env(safe-area-inset-top, 0); margin-top: 0; border: 1px solid #707070; border-top-width: 6px; box-shadow: 0 0 24px 6px #080811; padding: 0; width: 40em; box-sizing: border-box; overflow: visible; cursor: default; @include phone-portrait { min-width: 100%; max-width: 100%; border-left: none; border-right: none; } &::backdrop, & + :global(.backdrop) { background: rgb(0, 0, 0, 0.6); } } .title { margin: 16px; h2 { font-size: 14px; margin: 0; font-weight: 600; } } .buttons { display: flex; flex-direction: row-reverse; gap: 8px; margin: 16px; align-items: center; } .body { margin: 16px; } ================================================ FILE: src/app/dim-ui/useDialog.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'body': string; 'buttons': string; 'dialog': string; 'title': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/useDialog.tsx ================================================ import * as styles from './useDialog.m.scss'; import { Portal } from 'app/utils/temp-container'; import clsx from 'clsx'; import { useCallback, useImperativeHandle, useRef, useState } from 'react'; import ClickOutsideRoot from './ClickOutsideRoot'; export class DialogError extends Error { constructor(reason: string) { super(reason); this.name = 'DialogError'; } } export interface DialogRef { showDialog: (args: Args) => Promise; } /** * A generic dialog component that uses the system native dialog component. */ function Dialog({ children, ref, }: { children: (args: Args, close: (result: Result) => void) => React.ReactNode; ref?: React.Ref>; }) { const dialogRef = useRef(null); const [dialogState, setDialogState] = useState<{ args: Args; promise: Promise; resolve: (value: Result) => void; reject: (err: Error) => void; }>(); const handleCloseEvent = () => { if (dialogState) { dialogState.reject(new DialogError('canceled')); setDialogState(undefined); } }; const close = (result: Result) => { if (dialogState) { dialogState.resolve(result); setDialogState(undefined); dialogRef.current?.close(); } }; const showDialog = useCallback( (args: Args) => { if (dialogState) { dialogState.reject(new DialogError('another dialog shown while this one is open')); } let resolve: (value: Result) => void | undefined; let reject: (err: Error) => void | undefined; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); setDialogState({ args, promise, resolve: resolve!, reject: reject!, }); dialogRef.current!.showModal(); return promise; }, [dialogState], ); useImperativeHandle(ref, () => ({ showDialog }), [showDialog]); // We block click event propagation or else it'll trigger click handlers of the parent. return ( {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} e.stopPropagation()} > {dialogState && children(dialogState.args, close)} ); } /** * A generic dialog component that uses the system native dialog component. * * Use this hook to get both an element that you should render, and a * `showDialog` function that can be used to invoke the dialog. The render * function provided to the hook gets called when the dialog is shown, with the * args from `showDialog`. Call `close` with the results to resolve the promise * from `showDialog`. */ export default function useDialog( children: (args: Args, close: (result: Result) => void) => React.ReactNode, ): [element: React.ReactNode, showDialog: (args: Args) => Promise] { const dialogRef = useRef>(null); const showDialog = useCallback((args: Args) => dialogRef.current!.showDialog(args), []); // eslint-disable-next-line @eslint-react/no-missing-key return [{children}, showDialog]; } /** * A standardized title area for the dialog. Use H2 for title string. */ export function Title({ children }: { children: React.ReactNode }) { return
{children}
; } /** * A standardized buttons area for the dialog. */ export function Buttons({ children }: { children: React.ReactNode }) { return
{children}
; } /** * A standardized body for the dialog. */ export function Body({ children, className }: { children: React.ReactNode; className?: string }) { return
{children}
; } ================================================ FILE: src/app/dim-ui/useFixOverscrollBehavior.ts ================================================ import useResizeObserver from '@react-hook/resize-observer'; /* * There is a bug in all browsers where overflow-behavior does not apply when an * item that has overflow: auto is not tall enough to actually need to scroll. * In those cases, scrolling "chains" to the main viewport, leading to an effect * of touching the sheet but scrolling what's behind it. In the past we used * libraries like body-scroll-lock to fix this, but a more targeted fix is here. * We watch the size of the element, and if it's big enough to scroll we turn on * overflow: auto. If they're not, we have to turn them to overflow: hidden so * they no longer count as a user-scrollable item. For this to work, the initial * styles must also start with overflow: hidden or overflow: auto, or else we * can't tell if we are overflowing the container initially. Also - if we have * -webkit-overflow-scrolling: touch, we get unwanted scroll chaining. But the * original reason to have that (native-style scrolling) is the default now. See * https://github.com/w3c/csswg-drafts/issues/3349#issuecomment-492721871 and * https://bugs.chromium.org/p/chromium/issues/detail?id=813094 */ export function useFixOverscrollBehavior(ref: React.RefObject) { useResizeObserver(ref, (entry) => { const elem = entry.target as HTMLElement; if (elem.scrollHeight > elem.clientHeight) { // Scrollable contents elem.style.overflowY = 'auto'; elem.style.touchAction = ''; } else { // Non-scrollable contents elem.style.overflowY = 'hidden'; elem.style.touchAction = 'none'; } }); } ================================================ FILE: src/app/dim-ui/usePopper.ts ================================================ import { applyStyles, arrow, computeStyles, flip, Instance, offset, Options, Padding, Placement, popperGenerator, popperOffsets, preventOverflow, } from '@popperjs/core'; import computeSidecarPosition from 'app/item-popup/sidecar-popper-modifier'; import { compact } from 'app/utils/collections'; import React, { useLayoutEffect, useRef } from 'react'; // ensure this stays in sync with '$theme-tooltip-arrow-size' in '_variables.scss' const popperArrowSize = 8; /** Makes a custom popper that doesn't have the event listeners modifier */ const createPopper = popperGenerator({ defaultModifiers: [ popperOffsets, offset, computeStyles, applyStyles, flip, preventOverflow, arrow, computeSidecarPosition, ], }); const popperOptions = ( placement: Options['placement'] = 'auto', arrowClassName?: string, menuClassName?: string, boundarySelector?: string, offset = arrowClassName ? popperArrowSize : 0, fixed = false, padding?: Padding, ): Partial => { const headerHeight = parseInt( document.querySelector('html')!.style.getPropertyValue('--header-height'), 10, ); const boundaryElement = boundarySelector && document.querySelector(boundarySelector); padding ??= { left: 10, top: headerHeight + (boundaryElement ? boundaryElement.clientHeight : 0) + 5, right: 10, bottom: 10, }; const hasArrow = Boolean(arrowClassName); const hasMenu = Boolean(menuClassName); return { strategy: fixed ? 'fixed' : 'absolute', placement, modifiers: compact([ { name: 'preventOverflow', options: { priority: ['bottom', 'top', 'right', 'left'], boundariesElement: 'viewport', padding, }, }, { name: 'flip', options: { behavior: ['top', 'bottom', 'right', 'left'], boundariesElement: 'viewport', padding, }, }, { name: 'offset', options: { offset: [0, offset], }, }, hasArrow && { name: 'arrow', options: { element: `.${arrowClassName}`, }, }, hasMenu && { name: 'computeSidecarPosition', options: { element: `.${menuClassName}`, }, }, ]), }; }; export function usePopper( { contents, reference, arrowClassName, menuClassName, boundarySelector, placement, offset, fixed, padding, }: { /** A ref to the rendered contents of a popper-positioned item */ contents: React.RefObject; /** An ref to the item that triggered the popper, which anchors it */ reference: React.RefObject; /** A class used to identify the arrow */ arrowClassName?: string; /** A class used to identify the sidecar menu */ menuClassName?: string; /** An optional additional selector for a "boundary area" */ boundarySelector?: string; /** Placement preference of the popper. Defaults to "auto" */ placement?: Placement; /** Offset of how far from the element to shift the popper. */ offset?: number; /** Is this placed on a fixed item? Workaround for https://github.com/popperjs/popper-core/issues/1156. TODO: make a "positioning context" context value for this */ fixed?: boolean; padding?: Padding; }, deps: React.DependencyList = [], ) { const popper = useRef(undefined); const destroy = () => { if (popper.current) { try { // Work around a popper issue with our custom modifier until we can switch to floating-ui popper.current.destroy(); } catch {} popper.current = undefined; } }; useLayoutEffect(() => { // log('Effect', name, contents.current, reference.current); // Reposition the popup as it is shown or if its size changes if (!contents.current || !reference.current) { return destroy(); } else if (popper.current) { popper.current.update(); } else { const options = popperOptions( placement, arrowClassName, menuClassName, boundarySelector, offset, fixed, padding, ); popper.current = createPopper(reference.current, contents.current, options); popper.current.update(); } return destroy; }, [ contents, reference, arrowClassName, menuClassName, boundarySelector, placement, offset, fixed, padding, /** * Doing ...deps allows us to pass dependencies from the components that rely on * usePopper. Certain popovers are only shown when specific conditions are met, * so by making those conditions dependencies we can position the popover * correctly once the popover is actually shown. */ // eslint-disable-next-line react-hooks/exhaustive-deps ...deps, ]); } ================================================ FILE: src/app/dim-ui/usePrompt.m.scss ================================================ .input { width: 100%; box-sizing: border-box; } ================================================ FILE: src/app/dim-ui/usePrompt.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'input': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/dim-ui/usePrompt.tsx ================================================ import { t } from 'app/i18next-t'; import { isWindows } from 'app/utils/browsers'; import { useCallback, useState } from 'react'; import useDialog, { Body, Buttons, Title } from './useDialog'; import * as styles from './usePrompt.m.scss'; export interface PromptOpts { defaultValue?: string; okLabel?: React.ReactNode; cancelLabel?: React.ReactNode; } /** * Replacement for window.prompt, returns an element you need to render, and a * confirm function you can use to show prompt dialogs. */ export default function usePrompt(): [ element: React.ReactNode, prompt: (message: string, opts?: PromptOpts) => Promise, ] { const [dialog, showDialog] = useDialog( (args, close) => ( ), ); const prompt = (message: string, opts?: PromptOpts) => showDialog({ message, ...opts }); return [dialog, prompt]; } function PromptDialog({ message, okLabel, cancelLabel, defaultValue, close, }: { message: React.ReactNode; defaultValue?: string; okLabel?: React.ReactNode; cancelLabel?: React.ReactNode; close: (result: string | null) => void; }) { const [value, setValue] = useState(defaultValue ?? ''); const cancel = useCallback(() => close(null), [close]); const ok = useCallback(() => close(value), [close, value]); const okButton = ( ); const cancelButton = ( ); return ( <> {message} setValue(e.target.value)} /> {isWindows() ? ( <> {cancelButton} {okButton} ) : ( <> {okButton} {cancelButton} )} ); } ================================================ FILE: src/app/farming/Farming.m.scss ================================================ @use '../variables.scss' as *; .farming { position: fixed; backface-visibility: hidden; bottom: 0; background-color: black; width: 43rem; padding: 0.5rem 1rem; color: #e0e0e0; align-items: center; text-align: left; display: flex; flex-direction: row; max-width: 100%; left: 50%; transform: translateX(-50%); box-sizing: border-box; z-index: 6; padding-bottom: Max(0.5rem, env(safe-area-inset-bottom)); @include phone-portrait { width: auto; right: 0; bottom: 50px; left: inherit; transform: none; button { margin: 0; } p { display: none; } } button { color: var(--theme-text); background-color: rgb(128, 128, 128, 0.4); border: none; padding: 0.5rem 1rem; text-align: center; font-size: 1.5em; margin-left: 1rem; transition: 0.3s background-color; @include interactive($hover: true, $active: true) { background-color: rgb(232, 165, 52, 0.8); } } p { margin: 0.25em 0; } } ================================================ FILE: src/app/farming/Farming.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'farming': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/farming/Farming.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { useSetting } from 'app/settings/hooks'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { AnimatePresence, Transition, Variants, motion } from 'motion/react'; import React from 'react'; import { useSelector } from 'react-redux'; import { stopFarming } from './actions'; import * as styles from './Farming.m.scss'; import { farmingStoreSelector } from './selectors'; const animateVariants: Variants = { shown: { y: 0, x: '-50%' }, hidden: { y: 60, x: '-50%' }, }; const animateTransition: Transition = { type: 'spring', duration: 0.3, bounce: 0 }; export default function Farming() { const dispatch = useThunkDispatch(); const store = useSelector(farmingStoreSelector); const [makeRoomForItems, setMakeRoomForItems] = useSetting('farmingMakeRoomForItems'); const inventoryClearSpaces = useSelector(settingSelector('inventoryClearSpaces')); const makeRoomForItemsChanged = (e: React.ChangeEvent) => setMakeRoomForItems(e.currentTarget.checked); const onStopClicked = () => dispatch(stopFarming()); return ( {store && ( {store.destinyVersion === 2 ? (

{t('FarmingMode.D2Desc', { store: store.name, context: store.genderName, count: inventoryClearSpaces, })}{' '} {t('FarmingMode.Vault')}

) : (

{makeRoomForItems ? t('FarmingMode.Desc', { store: store.name, context: store.genderName, count: inventoryClearSpaces, }) : t('FarmingMode.MakeRoom.Desc', { store: store.name, context: store.genderName, })}

)}
)}
); } ================================================ FILE: src/app/farming/actions.ts ================================================ import { settingSelector, settingsSelector } from 'app/dim-api/selectors'; import { bucketsSelector, getTagSelector, storesSelector } from 'app/inventory/selectors'; import { capacityForItem, findItemsByBucket, getVault, isD1Store, } from 'app/inventory/stores-helpers'; import { isInInGameLoadoutForSelector } from 'app/loadout/selectors'; import { D1BucketHashes, supplies } from 'app/search/d1-known-values'; import { refresh } from 'app/shell/refresh-events'; import { observe, unobserve } from 'app/store/observerMiddleware'; import { ThunkResult } from 'app/store/types'; import { CancelToken, withCancel } from 'app/utils/cancel'; import { infoLog } from 'app/utils/log'; import { dedupePromise } from 'app/utils/promises'; import { BucketCategory } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { InventoryBucket } from '../inventory/inventory-buckets'; import { MoveReservations, createMoveSession, sortMoveAsideCandidatesForStore, } from '../inventory/item-move-service'; import { DimItem } from '../inventory/item-types'; import { D1Store, DimStore } from '../inventory/store-types'; import { clearItemsOffCharacter } from '../loadout-drawer/loadout-apply'; import * as actions from './basic-actions'; import { farmingInterruptedSelector, farmingStoreSelector } from './selectors'; const FARMING_OBSERVER_ID = 'farming-observer'; // These are things you may pick up frequently out in the wild const makeRoomTypes = [ BucketHashes.KineticWeapons, // Primary BucketHashes.EnergyWeapons, // Special BucketHashes.PowerWeapons, // Heavy BucketHashes.Helmet, BucketHashes.Gauntlets, BucketHashes.ChestArmor, BucketHashes.LegArmor, BucketHashes.ClassArmor, D1BucketHashes.Artifact, BucketHashes.Consumables, BucketHashes.Materials, ]; const FARMING_REFRESH_RATE = 30_000; // Bungie.net caches results for 30 seconds - this may be too fast let intervalId = 0; /** * Start the farming process for a given store. This causes stores to auto refresh * and makes space(s) for new drops. In D1 it can also move some other items around. */ export function startFarming(storeId: string): ThunkResult { return async (dispatch, getState) => { dispatch(actions.start(storeId)); const farmingStore = farmingStoreSelector(getState()); if (farmingStore?.id !== storeId) { return; } infoLog('farming', 'Started farming', farmingStore.name); // Use a deduped promise for actually performing the farming operations. Since the store // observer will fire every time an item is moved, we'd schedule duplicate moves if we didn't // do this deduping. const doFarm = dedupePromise(async (farmingStore: DimStore, cancelToken: CancelToken) => { if (farmingInterruptedSelector(getState())) { infoLog('farming', 'Farming interrupted, will resume when tasks are complete'); } else if (isD1Store(farmingStore)) { return dispatch(farmD1(farmingStore, cancelToken)); } else { // In D2 we just make room return dispatch(makeRoomForItems(farmingStore, cancelToken)); } }); dispatch( observe({ id: FARMING_OBSERVER_ID, runInitially: true, getObserved: (rootState) => farmingStoreSelector(rootState), sideEffect: ({ current: farmingStore }) => { const [cancelToken, cancel] = withCancel(); if (farmingStore?.id !== storeId) { dispatch(unobserve(FARMING_OBSERVER_ID)); cancel(); return; } doFarm(farmingStore, cancelToken); }, }), ); window.clearInterval(intervalId); intervalId = window.setInterval(refresh, FARMING_REFRESH_RATE); }; } /** * Stop farming. This cancels the faster refresh */ export function stopFarming(): ThunkResult { return async (dispatch) => { dispatch(actions.stop()); window.clearInterval(intervalId); }; } // Ensure that there's {{inventoryClearSpaces}} number open space(s) in each category that could // hold an item, so they don't go to the postmaster. function makeRoomForItems(store: DimStore, cancelToken: CancelToken): ThunkResult { return (dispatch, getState) => { const buckets = bucketsSelector(getState())!; const makeRoomBuckets = Object.values(buckets.byHash).filter( (b) => b.category === BucketCategory.Equippable && b.vaultBucket, ); return dispatch(makeRoomForItemsInBuckets(store, makeRoomBuckets, cancelToken)); }; } // D1 Stuff function farmD1(store: D1Store, cancelToken: CancelToken): ThunkResult { return async (dispatch, getState) => { await dispatch(farmItems(store, cancelToken)); if (settingsSelector(getState()).farmingMakeRoomForItems) { await dispatch(makeRoomForD1Items(store, cancelToken)); } }; } function farmItems(store: D1Store, cancelToken: CancelToken): ThunkResult { const toMove = store.items.filter( (i) => !i.equipped && !i.notransfer && (i.isEngram || (i.equipment && i.bucket.hash !== BucketHashes.Emblems && i.rarity === 'Uncommon') || supplies.includes(i.hash)), ); if (toMove.length === 0) { return () => Promise.resolve(); } return moveItemsToVault(store, toMove, [], cancelToken); } // Ensure that there's {{inventoryClearSpaces}} number open space(s) in each category that could // hold an item, so they don't go to the postmaster. function makeRoomForD1Items(store: D1Store, cancelToken: CancelToken): ThunkResult { return async (dispatch, getState) => { const buckets = bucketsSelector(getState())!; const makeRoomBuckets = makeRoomTypes.map((type) => buckets.byHash[type]); return dispatch(makeRoomForItemsInBuckets(store, makeRoomBuckets, cancelToken)); }; } // Ensure that there's {{inventoryClearSpaces}} number of open space(s) in each category that could // hold an item, so they don't go to the postmaster. function makeRoomForItemsInBuckets( store: DimStore, makeRoomBuckets: InventoryBucket[], cancelToken: CancelToken, ): ThunkResult { return async (dispatch, getState) => { const stores = storesSelector(getState()); // If any category is full, we'll move one aside const itemsToMove: DimItem[] = []; const getTag = getTagSelector(getState()); const isInInGameLoadoutFor = isInInGameLoadoutForSelector(getState()); const inventoryClearSpaces = settingSelector('inventoryClearSpaces')(getState()); for (const bucket of makeRoomBuckets) { const items = findItemsByBucket(store, bucket.hash); if (items.length > 0) { const capacityIncludingClearSpacesSetting = capacityForItem(store, items[0]) - inventoryClearSpaces + 1; if (items.length >= capacityIncludingClearSpacesSetting) { const moveAsideCandidates = items.filter((i) => !i.equipped && !i.notransfer); const prioritizedMoveAsideCandidates = sortMoveAsideCandidatesForStore( moveAsideCandidates, store, getVault(stores)!, getTag, isInInGameLoadoutFor, ); // We'll move the first one to the vault const itemToMove = prioritizedMoveAsideCandidates[0]; if (itemToMove) { itemsToMove.push(itemToMove); } } } } if (itemsToMove.length === 0) { return; } return dispatch(moveItemsToVault(store, itemsToMove, makeRoomBuckets, cancelToken)); }; } function moveItemsToVault( store: DimStore, items: DimItem[], makeRoomBuckets: InventoryBucket[], cancelToken: CancelToken, ): ThunkResult { const reservations: MoveReservations = {}; // reserve one space in the active character reservations[store.id] = {}; for (const bucket of makeRoomBuckets) { reservations[store.id][bucket.hash] = 1; } return clearItemsOffCharacter(store, items, createMoveSession(cancelToken, items), reservations); } ================================================ FILE: src/app/farming/basic-actions.ts ================================================ import { createAction } from 'typesafe-actions'; /** Started farming a particular store */ export const start = createAction('farming/START')(); /** Stopped farming */ export const stop = createAction('farming/STOP')(); /** Temporarily interrupt farming */ export const interruptFarming = createAction('farming/INTERRUPT')(); /** Resume an interruption */ export const resumeFarming = createAction('farming/RESUME')(); ================================================ FILE: src/app/farming/reducer.ts ================================================ import { Reducer } from 'redux'; import { ActionType, getType } from 'typesafe-actions'; import * as actions from './basic-actions'; export interface FarmingState { // The actively farming store, if any readonly storeId?: string; // A counter for pending tasks that interrupt farming readonly numInterruptions: number; } export type FarmingAction = ActionType; const initialState: FarmingState = { numInterruptions: 0, }; export const farming: Reducer = ( state: FarmingState = initialState, action: FarmingAction, ): FarmingState => { switch (action.type) { case getType(actions.start): return { ...state, storeId: action.payload, numInterruptions: 0, }; case getType(actions.stop): return { ...state, storeId: undefined, numInterruptions: 0, }; case getType(actions.interruptFarming): return { ...state, numInterruptions: state.numInterruptions + 1, }; case getType(actions.resumeFarming): return { ...state, numInterruptions: state.numInterruptions - 1, }; default: return state; } }; ================================================ FILE: src/app/farming/selectors.ts ================================================ import { storesSelector } from 'app/inventory/selectors'; import { RootState } from 'app/store/types'; import { createSelector } from 'reselect'; export const farmingStoreSelector = createSelector( storesSelector, (state: RootState) => state.farming.storeId, (stores, storeId) => stores.find((s) => s.id === storeId), ); export const farmingInterruptedSelector = (state: RootState) => state.farming.numInterruptions > 0; ================================================ FILE: src/app/gear-power/GearPower.m.scss ================================================ @use '../variables.scss' as *; .sheetContents { composes: flexColumn from '../dim-ui/common.m.scss'; align-items: center; } .gearPowerSheet { width: 100%; max-width: 360px; margin: 0 auto; @include desktop { max-width: fit-content; } } .gearPowerSheetContent { margin: 0 20px 20px; white-space: pre-wrap; } .gearPowerHeader { display: flex; margin-right: 10px; align-items: center; > img { margin-right: 10px; height: 48px; width: 48px; } h1:first-child { margin-bottom: 4px; } h1:last-child { margin-bottom: 0; } } .powerLevel { color: $power; font-size: 1.4em; :global(.app-icon) { font-size: 60%; vertical-align: 45%; margin-right: 2px; } img { height: 1.2em; width: auto; margin: 0; vertical-align: text-bottom; filter: none !important; display: inline-block; } :global(.selected) & { color: black; } } .toggle { padding: 10px 10px 15px; box-sizing: border-box; :global(.dim-button) { padding: 4px 20px; } } .powerToggleButton { display: flex; flex-direction: column; align-items: center; } .gearGrid { width: max-content; margin: 10px auto; display: grid; gap: 4px 10px; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(10, 1fr); } .kinetic, .energy, .power { grid-column-start: 1; } .helmet, .gauntlets, .chest, .leg, .classItem { grid-column-start: 2; &.gearItem, .gearItemInfo > div { flex-direction: row-reverse; } } .kinetic { grid-row-start: 3; } .energy { grid-row-start: 5; } .power { grid-row-start: 7; } .helmet { grid-row-start: 1; } .gauntlets { grid-row-start: 3; } .chest { grid-row-start: 5; } .leg { grid-row-start: 7; } .classItem { grid-row-start: 9; } .gearItem { grid-row-end: span 2; display: flex; } .itemImage { height: 40px; width: 40px; margin: 0 5px; border: 1px solid #ddd; cursor: pointer; pointer-events: auto; } .gearItemInfo { display: flex; flex-direction: column; .power { font-size: 16px; line-height: 1; } .statMeta { font-size: 24px; line-height: 26px; } /* stylelint-disable-next-line no-descending-specificity */ & > div { display: flex; } } .bucketImage { height: 22px; width: 22px; margin-top: 2px; opacity: 0.6; } .neutral { opacity: 0.6; } .positive { color: $green; } .negative { color: $red; } .neutral, .positive, .negative { margin: 0 3px; } .footNote { margin: 10px auto 0; color: #999; max-width: 400px; } ================================================ FILE: src/app/gear-power/GearPower.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bucketImage': string; 'chest': string; 'classItem': string; 'energy': string; 'footNote': string; 'gauntlets': string; 'gearGrid': string; 'gearItem': string; 'gearItemInfo': string; 'gearPowerHeader': string; 'gearPowerSheet': string; 'gearPowerSheetContent': string; 'helmet': string; 'itemImage': string; 'kinetic': string; 'leg': string; 'negative': string; 'neutral': string; 'positive': string; 'power': string; 'powerLevel': string; 'powerToggleButton': string; 'sheetContents': string; 'statMeta': string; 'toggle': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/gear-power/GearPower.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import FractionalPowerLevel from 'app/dim-ui/FractionalPowerLevel'; import RadioButtons from 'app/dim-ui/RadioButtons'; import BucketIcon from 'app/dim-ui/svgs/BucketIcon'; import { t } from 'app/i18next-t'; import { locateItem } from 'app/inventory/locate-item'; import { powerLevelSelector } from 'app/inventory/store/selectors'; import { AppIcon, powerActionIcon } from 'app/shell/icons'; import { RootState } from 'app/store/types'; import { LookupTable } from 'app/utils/util-types'; import clsx from 'clsx'; import rarityIcons from 'data/d2/engram-rarity-icons.json'; import { BucketHashes } from 'data/d2/generated-enums'; import { useState } from 'react'; import { useSelector } from 'react-redux'; import { useSubscription } from 'use-subscription'; import Sheet from '../dim-ui/Sheet'; import { storesSelector } from '../inventory/selectors'; import * as styles from './GearPower.m.scss'; import { showGearPower$ } from './gear-power'; const bucketClassNames: LookupTable = { [BucketHashes.KineticWeapons]: styles.kinetic, [BucketHashes.EnergyWeapons]: styles.energy, [BucketHashes.PowerWeapons]: styles.power, [BucketHashes.Helmet]: styles.helmet, [BucketHashes.Gauntlets]: styles.gauntlets, [BucketHashes.ChestArmor]: styles.chest, [BucketHashes.LegArmor]: styles.leg, [BucketHashes.ClassArmor]: styles.classItem, }; export default function GearPower() { const stores = useSelector(storesSelector); const reset = () => { showGearPower$.next(undefined); }; const selectedStoreId = useSubscription(showGearPower$); const selectedStore = stores.find((s) => s.id === selectedStoreId); const powerLevel = useSelector((state: RootState) => powerLevelSelector(state, selectedStoreId)); const [whichGear, setWhichGear] = useState<'drop' | 'equip'>('drop'); if (!selectedStore || !powerLevel) { return null; } const header = (

{selectedStore.name}

); const powerFloor = Math.floor( whichGear === 'drop' ? powerLevel.dropPower : powerLevel.maxEquippableGearPower, ); const items = whichGear === 'drop' ? powerLevel.dropCalcItems : powerLevel.maxEquippablePowerItems; return (
{t('Stats.EquippableGear')}
), tooltip: t('Stats.MaxGearPowerOneExoticRule'), value: 'equip', }, { label: (
{t('Stats.DropLevel')}
), tooltip: t('Stats.DropLevelExplanation1'), value: 'drop', }, ]} />
{items.map((i) => { const powerDiff = (powerFloor - i.power) * -1; const diffSymbol = powerDiff >= 0 ? '+' : ''; const diffClass = powerDiff > 0 ? styles.positive : powerDiff < 0 ? styles.negative : styles.neutral; return (
locateItem(i)}>
{i.power}
{diffSymbol} {powerDiff}
); })}
{whichGear === 'equip' ? ( t('Stats.MaxGearPowerOneExoticRule') ) : ( <>

{t('Stats.DropLevelExplanation1')}

{t('Stats.DropLevelExplanation2')}

)}
); } // implement this once item popup & sheet coexist more peacefully // // import ItemPopupTrigger from 'app/inventory/ItemPopupTrigger'; // // {(ref, onClick) => ( // // // // )} // // t('Loadouts.OnWrongCharacterWarning') and t('Loadouts.OnWrongCharacterAdvice') and t('Loadouts.EquippableDifferent1') and t('Loadouts.EquippableDifferent2') // used to live in this file ================================================ FILE: src/app/gear-power/gear-power.ts ================================================ import { Observable } from 'app/utils/observable'; /** * The currently selected store for showing gear power. */ export const showGearPower$ = new Observable(undefined); /** * Show the gear power sheet */ export function showGearPower(selectedStoreId: string) { showGearPower$.next(selectedStoreId); } ================================================ FILE: src/app/google.ts ================================================ import { getToken } from 'app/bungie-api/oauth-tokens'; import { browserName, browserVersion } from './utils/system-info'; declare global { interface Window { dataLayer: any[]; } } window.dataLayer ||= []; export function ga(..._args: any[]) { // Google Analytics actually requires that we push arguments here, not _args! // eslint-disable-next-line prefer-rest-params window.dataLayer.push(arguments); } export function gaPageView(path: string, title?: string) { ga('event', 'page_view', { page_title: title, page_location: window.location.origin + path, page_path: path, }); } export function gaEvent(type: string, params: Record) { ga('event', type, params); } export function initGoogleAnalytics() { ga('js', new Date()); const token = getToken(); ga('config', $ANALYTICS_PROPERTY, { allow_ad_personalization_signals: false, allow_google_signals: false, send_page_view: false, user_id: token?.bungieMembershipId, dim_version: $DIM_VERSION, dim_flavor: $DIM_FLAVOR, browser_name: browserName, browser_version: browserVersion, }); const script = document.createElement('script'); script.type = 'text/javascript'; script.async = true; script.src = `https://www.googletagmanager.com/gtag/js?id=${$ANALYTICS_PROPERTY}`; document.head.appendChild(script); } ================================================ FILE: src/app/hotkeys/GlobalHotkeys.tsx ================================================ import { Hotkey } from './hotkeys'; import { useHotkeys } from './useHotkey'; /** * Used to install global hotkeys that do not require focus and are included in the hotkey cheat sheet. * Prefer useHotkey or useHotkeys hooks. */ export default function GlobalHotkeys({ hotkeys: hotkeyDefs }: { hotkeys: Hotkey[] }) { useHotkeys(hotkeyDefs); return null; } ================================================ FILE: src/app/hotkeys/HotkeysCheatSheet.m.scss ================================================ /*! * angular-hotkeys v1.7.0 * https://chieffancypants.github.io/angular-hotkeys * Copyright (c) 2016 Wes Cruver * License: MIT */ .container { display: table !important; position: fixed; width: 100%; height: 100%; top: 0; left: 0; color: var(--theme-text); font-size: 1em; background-color: rgb(0, 0, 0, 0.9); z-index: 2000; backdrop-filter: blur(5px); } .title { font-weight: bold; text-align: center; font-size: 1.2em; } .hotkeys { width: 100%; height: 100%; display: table-cell; vertical-align: middle; } .list { display: grid; grid-template-columns: repeat(auto-fill, 75px minmax(200px, 1fr)); margin: 0 auto; max-width: 800px; color: var(--theme-text); } .keys { padding: 5px; text-align: right; } .key { font-size: 1em !important; padding: 4px 6px !important; } .text { padding-left: 10px; font-size: 1em; align-self: center; } @media all and (max-width: 500px) { .hotkeys { font-size: 0.8em; } } @media all and (min-width: 750px) { .hotkeys { font-size: 1.2em; } } ================================================ FILE: src/app/hotkeys/HotkeysCheatSheet.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'container': string; 'hotkeys': string; 'key': string; 'keys': string; 'list': string; 'text': string; 'title': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/hotkeys/HotkeysCheatSheet.tsx ================================================ import KeyHelp from 'app/dim-ui/KeyHelp'; import { t } from 'app/i18next-t'; import { compareBy } from 'app/utils/comparators'; import { Observable } from 'app/utils/observable'; import React, { memo } from 'react'; import { useSubscription } from 'use-subscription'; import GlobalHotkeys from './GlobalHotkeys'; import * as styles from './HotkeysCheatSheet.m.scss'; import { getAllHotkeys } from './hotkeys'; import { useHotkey } from './useHotkey'; export const showCheatSheet$ = new Observable(false); export default memo(function HotkeysCheatSheet() { const visible = useSubscription(showCheatSheet$); const toggle = () => showCheatSheet$.next(!visible); const hide = () => showCheatSheet$.next(false); useHotkey('?', t('Hotkey.ShowHotkeys'), toggle); if (!visible) { return null; } const appKeyMap = getAllHotkeys(); return (

{t('Hotkey.CheatSheetTitle')}

{Object.entries(appKeyMap) .sort(compareBy(([combo]) => combo)) .map( ([combo, description]) => description.length > 0 && (
{description}
), )}
); }); ================================================ FILE: src/app/hotkeys/hotkeys.test.ts ================================================ import { clearAllHotkeysForTest, registerHotkeys, removeHotkeysById } from './hotkeys'; beforeEach(clearAllHotkeysForTest); it('stacks hotkeys', () => { let p1 = 0; const handleP1 = () => { p1++; }; let p2 = 0; const handleP2 = () => { p2++; }; let p3 = 0; const handleP3 = () => { p3++; }; registerHotkeys('1', [{ combo: 'p', callback: handleP1, description: 'p' }]); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'p' })); expect(p1).toBe(1); registerHotkeys('2', [{ combo: 'p', callback: handleP2, description: 'p' }]); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'p' })); expect(p1).toBe(1); expect(p2).toBe(1); // Now, re-register 1 with a different handler registerHotkeys('1', [{ combo: 'p', callback: handleP3, description: 'p' }]); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'p' })); // It should still trigger p2! expect(p1).toBe(1); expect(p2).toBe(2); expect(p3).toBe(0); removeHotkeysById('2', 'p'); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'p' })); expect(p1).toBe(1); expect(p2).toBe(2); expect(p3).toBe(1); }); // Fixes https://github.com/DestinyItemManager/DIM/issues/6246 it('allows Escape hotkey when an input is focused', () => { const cb = jest.fn(); registerHotkeys('esc', [{ combo: 'Escape', callback: cb, description: 'p' }]); const input = document.createElement('input'); document.body.appendChild(input); input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); expect(cb).toHaveBeenCalled(); }); ================================================ FILE: src/app/hotkeys/hotkeys.ts ================================================ import { I18nKey, t, tl } from 'app/i18next-t'; import { isMac } from 'app/utils/browsers'; import { compareByIndex } from 'app/utils/comparators'; import { noop } from 'app/utils/functions'; import { StringLookup } from 'app/utils/util-types'; /** Mapping from key name to fun symbols */ const map: StringLookup = { command: '\u2318', // ⌘ shift: '\u21E7', // ⇧ left: '\u2190', // ← right: '\u2192', // → up: '\u2191', // ↑ down: '\u2193', // ↓ return: '\u23CE', // ⏎ backspace: '\u232B', // ⌫ }; /** We translate the keys that don't have fun symbols. */ const keyi18n: StringLookup = { tab: tl('Hotkey.Tab'), enter: tl('Hotkey.Enter'), }; /** * Convert strings like cmd into symbols like ⌘ */ export function symbolize(combo: string) { const parts = combo.split('+'); return parts .map((part) => { // try to resolve command / ctrl based on OS: if (part === 'mod') { part = isMac() ? 'command' : 'ctrl'; } return keyi18n[part] ? t(keyi18n[part]!) : map[part] || part.toUpperCase(); }) .join(' '); } export interface Hotkey { /** The actual hotkey combo, like "shift+p" */ combo: string; /** A description that'll be shown on the hotkey help screen. */ description: string; /** What to do when the hotkey is triggered. */ callback: (event: KeyboardEvent) => void; } // Each key combo can have many hotkey implementations bound to it, but only the // last one in the array gets triggered. const keyMap: { [combo: string]: undefined | (Hotkey & { id: string })[] } = {}; export function clearAllHotkeysForTest() { for (const key of Object.keys(keyMap)) { delete keyMap[key]; } } /** * Add a new set of hotkeys. The id parameter allows us to preserve this hotkey * in the stack of bindings for the hotkey even when repeatedly registered - use * the useId hook to generate a stable ID for a component to use for this. Call * removeHotkeysById when a component is unmounted or the hotkey is disabled. */ export function registerHotkeys(id: string, hotkeys: Hotkey[]) { if (!hotkeys?.length) { return noop; } for (const hotkey of hotkeys) { bind(id, hotkey); } } /** * Remove bound hotkeys from the stack by id. This should be the same ID the * hotkey was registered under. Pass combo if you know it to speed up removal. */ export function removeHotkeysById(id: string, combo?: string) { if (combo) { unbind(id, combo); } else { // Look for the ID in every combo for (const combo of Object.keys(keyMap)) { unbind(id, combo); } } } export function getAllHotkeys() { const combos: { [combo: string]: string } = {}; for (const k in keyMap) { const hotkeyList = keyMap[k]!; const hotkey = hotkeyList.at(-1)!; const combo = symbolize(hotkey.combo); combos[combo] = hotkey.description; } return combos; } const modifiers = ['ctrl', 'alt', 'shift', 'meta']; function normalizeCombo(combo: string) { return combo .split('+') .map((c) => (c === 'mod' ? (isMac() ? 'meta' : 'ctrl') : c)) .sort(compareByIndex(modifiers, (c) => c)) .join('+'); } function bind(id: string, hotkey: Hotkey) { const keys = (keyMap[normalizeCombo(hotkey.combo)] ??= []); // Replace existing hotkeys in the same place in the stack, so re-renders // don't pop the hotkey to the top. const existingIndex = keys.findIndex((h) => h.id === id); if (existingIndex >= 0) { keys[existingIndex] = { ...hotkey, id }; } else { keys.push({ ...hotkey, id }); } } function unbind(id: string, combo: string) { const normalizedCombo = normalizeCombo(combo); const hotkeysForCombo = keyMap[normalizedCombo]; const existingIndex = hotkeysForCombo?.findIndex((h) => h.id === id) ?? -1; if (existingIndex >= 0) { hotkeysForCombo!.splice(existingIndex, 1); } if (!hotkeysForCombo?.length) { delete keyMap[normalizedCombo]; } } const _MAP: { [code: number]: string } = { 8: 'backspace', 9: 'tab', 13: 'enter', 16: 'shift', 17: 'ctrl', 18: 'alt', 20: 'capslock', 27: 'esc', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down', 45: 'ins', 46: 'del', 91: 'meta', 93: 'meta', 224: 'meta', 106: '*', 107: '+', 109: '-', 110: '.', 111: '/', 186: ';', 187: '=', 188: ',', 189: '-', 190: '.', 191: '/', 192: '`', 219: '[', 220: '\\', 221: ']', 222: "'", }; /** * A list of hotkeys that should not be blocked even when a * form control is focused. */ const allowedKeysInFormControls = ['Escape']; // Add in the number keys for (let i = 0; i <= 9; ++i) { // This needs to use a string cause otherwise since 0 is falsey // mousetrap will never fire for numpad 0 pressed as part of a keydown // event. // // @see https://github.com/ccampbell/mousetrap/pull/258 _MAP[i + 96] = i.toString(); } function handleKeyEvent(e: KeyboardEvent) { /** * By default, we block custom hotkeys to prevent overriding built-in input * hotkeys. However, certain custom hotkeys should be allowed even when a * form control is focused. For example, pressing Escape inside of a sheet * should always close the sheet, even if an input in the sheet is focused. */ const blockHotKeyInFormControl = e.target instanceof HTMLElement && (e.target.isContentEditable || (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName) && (e.target as HTMLInputElement).type !== 'checkbox')) && !allowedKeysInFormControls.includes(e.key); if (e.isComposing || e.repeat || blockHotKeyInFormControl) { return; } const combo = new Set(); if (e.ctrlKey && e.key !== 'ctrl') { combo.add('ctrl'); } if (e.altKey && e.key !== 'alt') { combo.add('alt'); } if (e.shiftKey && e.key !== 'shift') { combo.add('shift'); } if (e.metaKey && e.key !== 'meta') { combo.add('meta'); } // This works for stuff like Shift+1 const character = _MAP[e.which] ?? String.fromCharCode(e.which).toLowerCase(); combo.add(character); const comboStr = [...combo].join('+'); if (trigger(comboStr, e)) { return; } // Then try the resolved key which works for stuff like ?. We don't need modifiers for that one. combo.delete('shift'); combo.delete(character); combo.add(e.key); const comboStr2 = [...combo].join('+'); trigger(comboStr2, e); } function trigger(comboStr: string, e: KeyboardEvent) { const callbacks = keyMap[comboStr]; if (callbacks) { // Only call the last callback registered for this combo. callbacks.at(-1)?.callback(e); e.preventDefault(); return true; } return false; } document.addEventListener('keydown', handleKeyEvent); ================================================ FILE: src/app/hotkeys/useHotkey.ts ================================================ import { useEffect, useId } from 'react'; import { Hotkey, registerHotkeys, removeHotkeysById } from './hotkeys'; /** * A hook for registering a single global hotkey that will appear in the hotkey * help screen. Make sure to memoize the callback. Hotkeys registered in this * way take precedence over any previously registered hotkeys for the same * combos, and can likewise be overridden by a later registration. However, once * the later registration is unregistered, these will become active again. For * example, if you have a sequence of sheets that open one after another, each * one can register an "esc" callback, and they'll dismiss one by one in order * as the user hits "esc." * * @example * useHotkey("ctrl+alt+1", "Does a thing", useCallback(() => setThing(1), [setThing])); */ export function useHotkey( combo: string, description: string, callback: (event: KeyboardEvent) => void, disabled?: boolean, ) { const id = useId(); useEffect(() => { if (disabled) { removeHotkeysById(id, combo); return; } registerHotkeys(id, [ { combo, description, callback, }, ]); }, [id, combo, description, callback, disabled]); // Remove the hotkey only once the component unmounts useEffect(() => () => removeHotkeysById(id, combo), [combo, id]); } /** * A hook for registering a dynamic list of global hotkeys that will appear in the hotkey help screen. Prefer useHotkey if you can. * * You should memoize the list of hotkeys with `useMemo`. * * @see {@link useHotkey} */ export function useHotkeys(hotkeyDefs: Hotkey[]) { const id = useId(); useEffect(() => registerHotkeys(id, hotkeyDefs), [hotkeyDefs, id]); // Remove the hotkeys only once the component unmounts useEffect(() => () => removeHotkeysById(id), [id]); } ================================================ FILE: src/app/i18n.ts ================================================ import { setTag } from '@sentry/browser'; import i18next from 'i18next'; import HttpApi, { HttpBackendOptions } from 'i18next-http-backend'; import de from 'locale/de.json'; import en from 'locale/en.json'; import es from 'locale/es.json'; import esMX from 'locale/esMX.json'; import fr from 'locale/fr.json'; import it from 'locale/it.json'; import ja from 'locale/ja.json'; import ko from 'locale/ko.json'; import pl from 'locale/pl.json'; import ptBR from 'locale/ptBR.json'; import ru from 'locale/ru.json'; import zhCHS from 'locale/zhCHS.json'; import zhCHT from 'locale/zhCHT.json'; import enSrc from '../../config/i18n.json'; import { languageSelector } from './dim-api/selectors'; import { humanBytes } from './storage/human-bytes'; import { StoreObserver } from './store/observerMiddleware'; import { invert } from './utils/collections'; import { infoLog } from './utils/log'; export const DIM_LANG_INFOS = { de: { latinBased: true }, en: { latinBased: true }, es: { latinBased: true }, 'es-mx': { latinBased: true }, fr: { latinBased: true }, it: { latinBased: true }, ja: { latinBased: false }, ko: { latinBased: false }, pl: { latinBased: true }, 'pt-br': { latinBased: true }, ru: { latinBased: false }, 'zh-chs': { latinBased: false }, 'zh-cht': { latinBased: false }, }; export type DimLanguage = keyof typeof DIM_LANG_INFOS; export const DIM_LANGS = Object.keys(DIM_LANG_INFOS) as DimLanguage[]; // Our locale names don't line up with the BCP 47 tags for Chinese export const browserLangToDimLang: Record = { 'zh-Hans': 'zh-chs', 'zh-Hant': 'zh-cht', }; const dimLangToBrowserLang = invert(browserLangToDimLang); // Hot-reload translations in dev. You'll still need to get things to re-render when // translations change (unless we someday switch to react-i18next) if (module.hot) { module.hot.accept('../../config/i18n.json', () => { i18next.reloadResources('en', undefined, () => { infoLog('i18n', 'Reloaded translations'); }); }); } function browserLanguage(): DimLanguage { const currentBrowserLang = window.navigator.language || 'en'; const overriddenLang = Object.entries(browserLangToDimLang).find(([browserLang]) => currentBrowserLang.startsWith(browserLang), ); if (overriddenLang) { return overriddenLang[1]; } return DIM_LANGS.find((lang) => currentBrowserLang.toLowerCase().startsWith(lang)) || 'en'; } // Try to pick a nice default language export function defaultLanguage(): DimLanguage { const storedLanguage = localStorage.getItem('dimLanguage') as DimLanguage; if (storedLanguage && DIM_LANGS.includes(storedLanguage)) { return storedLanguage; } return browserLanguage(); } export function initi18n(): Promise { const lang = defaultLanguage(); return new Promise((resolve, reject) => { // See https://github.com/i18next/i18next i18next.use(HttpApi).init( { debug: false, lng: lang, fallbackLng: 'en', lowerCaseLng: true, supportedLngs: DIM_LANGS, load: 'currentOnly', interpolation: { escapeValue: false, format(val: string, format) { switch (format) { case 'pct': return `${Math.min(100, Math.floor(100 * parseFloat(val)))}%`; case 'humanBytes': return humanBytes(parseInt(val, 10)); case 'number': return parseInt(val, 10).toLocaleString(); default: return val; } }, }, backend: { loadPath([lng]: string[]) { const path = { de, // In development, directly use the source English translations. // In production we use a version that's gone through i18n-scanner // to remove unused keys. en: $DIM_FLAVOR === 'dev' ? enSrc : en, es, 'es-mx': esMX, fr, it, ja, ko, pl, 'pt-br': ptBR, ru, 'zh-chs': zhCHS, 'zh-cht': zhCHT, }[lng] as unknown as string; if (!path) { throw new Error(`unsupported language ${lng}`); } return path; }, }, returnObjects: true, }, (error) => { if (error) { // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors reject(error); } else { resolve(undefined); } }, ); }); } // Reflect the setting changes in stored values and in the DOM export function createLanguageObserver(): StoreObserver { return { id: 'i18n-observer', getObserved: languageSelector, runInitially: true, sideEffect: ({ current }) => { if (current === browserLanguage()) { localStorage.removeItem('dimLanguage'); } else { localStorage.setItem('dimLanguage', current); } if (current !== i18next.language) { i18next.changeLanguage(current); } setTag('lang', current); document .querySelector('html')! .setAttribute('lang', dimLangToBrowserLang[current] ?? current); }, }; } ================================================ FILE: src/app/i18next-t.ts ================================================ import type { ParseKeys } from 'i18next'; // eslint-disable-next-line no-restricted-imports import { t as originalT } from 'i18next'; export type I18nKey = ParseKeys; export const t = ( key: I18nKey, opts?: | { count?: number; context?: string; metadata?: { context?: string[]; keys?: string } } | { [arg: string]: number | string; }, ): string => originalT(key, opts); /** * This is a "marker function" that tells our i18next-scanner that you will translate this string later (tl = translate later). * This way you don't need to pre-translate everything or include redundant comments. This function is inlined and * has no runtime presence. */ /*@__INLINE__*/ export function tl(key: T): T { return key; } ================================================ FILE: src/app/infuse/InfusionFinder.m.scss ================================================ @use '../variables.scss' as *; .infuseDialog { width: 100%; display: flex; flex-direction: column; overflow: auto; margin: 0 auto; } .infuseHeader { @include phone-portrait { flex-direction: column; } :global(.item) { margin-bottom: 0; } } .infuseActions { display: flex; flex-direction: column; justify-content: space-around; margin-left: 8px; button { margin-bottom: 4px; &:last-child { margin: 0; } } } .infuseSearch { flex: 1; margin-left: 16px; margin-top: auto; @include phone-portrait { margin-bottom: 8px; margin-left: 3px; } } .infuseSelected :global(.item) { outline: 2px solid var(--theme-accent-primary) !important; } .infusionEquation { display: flex; } .icon { align-self: center; margin: 0 8px; } .infuseSources { flex: 1; padding: 10px; min-height: 150px; } .infusionControls { display: flex; flex-direction: row; @include phone-portrait { flex-direction: column; } } .infuseTopRow { display: flex; flex-direction: row; @include phone-portrait { margin-bottom: 8px; } } .missingItem { :global(.item-img) { display: flex; align-items: center; justify-content: center; font-size: 24px; } } ================================================ FILE: src/app/infuse/InfusionFinder.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'icon': string; 'infuseActions': string; 'infuseDialog': string; 'infuseHeader': string; 'infuseSearch': string; 'infuseSelected': string; 'infuseSources': string; 'infuseTopRow': string; 'infusionControls': string; 'infusionEquation': string; 'missingItem': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/infuse/InfusionFinder.tsx ================================================ import { InfuseDirection } from '@destinyitemmanager/dim-api-types'; import { gaPageView } from 'app/google'; import { t } from 'app/i18next-t'; import { applyLoadout } from 'app/loadout-drawer/loadout-apply'; import { LoadoutItem } from 'app/loadout/loadout-types'; import SearchBar from 'app/search/SearchBar'; import { filterFactorySelector } from 'app/search/items/item-search-filter'; import { useSetting } from 'app/settings/hooks'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { DimThunkDispatch } from 'app/store/types'; import { useEventBusListener } from 'app/utils/hooks'; import { isD1Item } from 'app/utils/item-utils'; import clsx from 'clsx'; import { useCallback, useDeferredValue, useEffect, useReducer } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import Sheet from '../dim-ui/Sheet'; import '../inventory-page/StoreBucket.scss'; import ConnectedInventoryItem from '../inventory/ConnectedInventoryItem'; import { DimItem } from '../inventory/item-types'; import { allItemsSelector, currentStoreSelector, getTagSelector } from '../inventory/selectors'; import { DimStore } from '../inventory/store-types'; import { convertToLoadoutItem, newLoadout } from '../loadout-drawer/loadout-utils'; import { showNotification } from '../notifications/notifications'; import { AppIcon, faArrowCircleDown, faEquals, faRandom, helpIcon, plusIcon } from '../shell/icons'; import { chainComparator, compareBy, reverseComparator } from '../utils/comparators'; import * as styles from './InfusionFinder.m.scss'; import { showInfuse$ } from './infuse'; const itemComparator = chainComparator( reverseComparator(compareBy((item: DimItem) => item.power)), compareBy((item: DimItem) => isD1Item(item) && item.talentGrid ? (item.talentGrid.totalXP / item.talentGrid.totalXPRequired) * 0.5 : 0, ), ); interface State { direction: InfuseDirection; /** The item we're focused on */ query?: DimItem; /** The item that will be consumed by infusion */ source?: DimItem; /** The item that will have its power increased by infusion */ target?: DimItem; /** Search filter string */ filter: string; } type Action = /** Reset the tool (for when the sheet is closed) */ | { type: 'reset' } /** Set up the tool with a new focus item */ | { type: 'init'; item: DimItem; hasInfusables: boolean; hasFuel: boolean } /** Swap infusion direction */ | { type: 'swapDirection' } /** Select one of the items in the list */ | { type: 'selectItem'; item: DimItem } | { type: 'setFilter'; filter: string }; /** * All state for this component is managed through this reducer and the Actions above. */ function stateReducer(state: State, action: Action): State { switch (action.type) { case 'reset': return { ...state, query: undefined, source: undefined, target: undefined, filter: '', }; case 'init': { const direction = state.direction === InfuseDirection.INFUSE ? action.hasInfusables ? InfuseDirection.INFUSE : InfuseDirection.FUEL : action.hasFuel ? InfuseDirection.FUEL : InfuseDirection.INFUSE; return { ...state, direction, query: action.item, target: direction === InfuseDirection.INFUSE ? action.item : undefined, source: direction === InfuseDirection.INFUSE ? undefined : action.item, }; } case 'swapDirection': { const direction = state.direction === InfuseDirection.INFUSE ? InfuseDirection.FUEL : InfuseDirection.INFUSE; return { ...state, direction, target: direction === InfuseDirection.INFUSE ? state.query : undefined, source: direction === InfuseDirection.FUEL ? state.query : undefined, }; } case 'selectItem': { if (state.direction === InfuseDirection.INFUSE) { return { ...state, target: state.query, source: action.item, }; } else { return { ...state, target: action.item, source: state.query, }; } } case 'setFilter': { return { ...state, filter: action.filter, }; } } } export default function InfusionFinder() { const dispatch = useThunkDispatch(); const allItems = useSelector(allItemsSelector); const currentStore = useSelector(currentStoreSelector); const getTag = useSelector(getTagSelector); const filters = useSelector(filterFactorySelector); const [lastInfusionDirection, setLastInfusionDirection] = useSetting('infusionDirection'); const [{ direction, query, source, target, filter: liveFilter }, stateDispatch] = useReducer( stateReducer, { direction: lastInfusionDirection, filter: '', }, ); const filter = useDeferredValue(liveFilter); const reset = () => stateDispatch({ type: 'reset' }); const selectItem = (item: DimItem) => stateDispatch({ type: 'selectItem', item }); const onQueryChanged = (filter: string) => stateDispatch({ type: 'setFilter', filter }); const switchDirection = () => stateDispatch({ type: 'swapDirection' }); const show = query !== undefined; const destinyVersion = currentStore?.destinyVersion; useEffect(() => { if (show && destinyVersion) { gaPageView(`/profileMembershipId/d${destinyVersion}/infuse`); } }, [destinyVersion, show]); // Listen for items coming in via showInfuse$ useEventBusListener( showInfuse$, useCallback( (item) => { const hasInfusables = allItems.some((i) => isInfusable(item, i)); const hasFuel = allItems.some((i) => isInfusable(i, item)); stateDispatch({ type: 'init', item, hasInfusables: hasInfusables, hasFuel }); }, [allItems], ), ); // Close the sheet on navigation const { pathname } = useLocation(); useEffect(reset, [pathname]); // Save direction to settings useEffect(() => { if (direction !== lastInfusionDirection) { setLastInfusionDirection(direction); } }, [direction, lastInfusionDirection, dispatch, setLastInfusionDirection]); if (!query || !currentStore) { return null; } const filterFn = filters(filter); let items = allItems.filter( (item) => (direction === InfuseDirection.INFUSE ? isInfusable(query, item) : isInfusable(item, query)) && filterFn(item), ); const dupes = items.filter((item) => item.hash === query.hash); dupes.sort(itemComparator); items = items.filter((item) => item.hash !== query.hash); items.sort(itemComparator); const preferredSource = dupes.find((i) => getTag(i) === 'infuse') || items.find((i) => getTag(i) === 'infuse'); const effectiveTarget = target || dupes[0] || items[0]; const effectiveSource = source || preferredSource || dupes[0] || items[0]; let result: DimItem | undefined; if (effectiveSource?.power && effectiveTarget?.power) { const infused = effectiveSource.power; result = { ...effectiveTarget, power: infused, primaryStat: { ...effectiveTarget.primaryStat!, value: infused, }, }; } const missingItem = (
???
); const header = ({ onClose }: { onClose: () => void }) => (

{direction === InfuseDirection.INFUSE ? t('Infusion.InfuseTarget', { name: query.name, }) : t('Infusion.InfuseSource', { name: query.name, })}

{effectiveTarget ? : missingItem}
{effectiveSource ? : missingItem}
{result ? : missingItem}
{result && effectiveSource && effectiveTarget && ( )}
); const renderItem = (item: DimItem) => (
selectItem(item)} >
); return (
{items.length > 0 || dupes.length > 0 ? ( <>
{dupes.map(renderItem)}
{items.map(renderItem)}
) : ( {t('Infusion.NoItems')} )}
); } /** * Can source be infused into target? */ function isInfusable(target: DimItem, source: DimItem) { if (!target.infusable || !source.infusionFuel) { return false; } if (source.destinyVersion === 1 && target.destinyVersion === 1) { return source.bucket.hash === target.bucket.hash && target.power < source.power; } return ( source.infusionCategoryHashes && target.infusionCategoryHashes?.some((h) => source.infusionCategoryHashes!.includes(h)) && target.power < source.power ); } async function transferItems( dispatch: DimThunkDispatch, currentStore: DimStore, onClose: () => void, source: DimItem, target: DimItem, ) { if (!source || !target) { return; } if (target.notransfer || source.notransfer) { const name = source.notransfer ? source.name : target.name; showNotification({ type: 'error', title: t('Infusion.NoTransfer', { target: name }) }); return; } onClose(); const items: LoadoutItem[] = [ convertToLoadoutItem(target, false), // Include the source, since we wouldn't want it to get moved out of the way convertToLoadoutItem(source, source.equipped), ]; if (source.destinyVersion === 1) { if (target.bucket.inGeneral) { // Mote of Light items.push({ id: '0', hash: 937555249, amount: 2, equip: false, }); } else if (target.bucket.inWeapons) { // Weapon Parts items.push({ id: '0', hash: 1898539128, amount: 10, equip: false, }); } else { // Armor Materials items.push({ id: '0', hash: 1542293174, amount: 10, equip: false, }); } if (source.isExotic) { // Exotic shard items.push({ id: '0', hash: 452597397, amount: 1, equip: false, }); } } // TODO: another one where we want to respect equipped const loadout = newLoadout(t('Infusion.InfusionMaterials'), items); await dispatch(applyLoadout(currentStore, loadout)); } ================================================ FILE: src/app/infuse/infuse.ts ================================================ import { EventBus } from 'app/utils/observable'; import { DimItem } from '../inventory/item-types'; export const showInfuse$ = new EventBus(); /** * Show the infusion fuel finder. */ export function showInfuse(item: DimItem) { showInfuse$.next(item); } ================================================ FILE: src/app/inventory/ArtifactXP.m.scss ================================================ .xpIcon { height: 1.2em; width: auto; } .artifactProgress { background-color: #1c9d9e !important; } ================================================ FILE: src/app/inventory/ArtifactXP.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'artifactProgress': string; 'xpIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/ArtifactXP.tsx ================================================ import { ObjectiveDescription, ObjectiveProgress, ObjectiveProgressBar, ObjectiveText, } from 'app/progress/Objective'; import { DestinyCharacterProgressionComponent } from 'bungie-api-ts/destiny2'; import XPIcon from '../../images/xpIcon.svg?react'; import * as styles from './ArtifactXP.m.scss'; export function ArtifactXP({ characterProgress, bonusPowerProgressionHash, }: { characterProgress: DestinyCharacterProgressionComponent | undefined; bonusPowerProgressionHash: number | undefined; }) { if (!bonusPowerProgressionHash) { return null; } const artifactProgress = characterProgress?.progressions[bonusPowerProgressionHash] ?? ({} as { progressToNextLevel: undefined; nextLevelAt: undefined; level: undefined }); const { progressToNextLevel, nextLevelAt, level } = artifactProgress; if (!progressToNextLevel || !nextLevelAt || level === undefined) { return null; } return ( <> } description={(level + 1).toLocaleString()} /> {progressToNextLevel.toLocaleString()} / {nextLevelAt.toLocaleString()} ); } ================================================ FILE: src/app/inventory/BadgeInfo.m.scss ================================================ @use '../variables.scss' as *; .badge { margin-top: -1 * $item-border-width; background-color: var(--theme-item-polaroid); color: var(--theme-item-polaroid-txt); height: calc(#{$badge-height}); font-size: calc(#{$badge-font-size}); width: 100%; display: flex; text-align: right; box-sizing: border-box; padding: 0 2px; white-space: pre; align-items: center; justify-content: flex-end; line-height: calc(#{$badge-height}); overflow: hidden; } /* this width keeps the span aligned right (short text appears on right side of polaroid) but if the text is too long, it will overflow(hidden) off the right side instead of left. */ .badgeContent { max-width: 100%; max-height: 100%; } .engram { width: fit-content; border-radius: 2px; margin-left: auto; margin-right: auto; padding: 0 4px; } .quality { display: none; margin-right: auto; :global(.itemQuality) & { display: block; padding: 0 2px; margin-left: -2px; } :global(.app-icon) { filter: drop-shadow(0 0 1px rgb(0, 0, 0, 0.8)); } } .fullstack { font-weight: bold; color: var(--theme-item-polaroid-capped-txt); } .capped { background-color: var(--theme-item-polaroid-capped); } .masterwork { background-color: var(--theme-item-polaroid-masterwork); color: var(--theme-item-polaroid-masterwork-txt); } .deepsight { border-bottom: 1px solid $deepsight-border-color; border-left: 1px solid $deepsight-border-color; border-right: 1px solid $deepsight-border-color; padding: 0 1px; } /* some elements icons (arc / void / strand) don't show up well on polaroid white. we darken these. */ .fixContrast { filter: brightness(var(--theme-item-polaroid-element-adjust-brightness)) saturate(2.5); background-color: transparent !important; } ================================================ FILE: src/app/inventory/BadgeInfo.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'badge': string; 'badgeContent': string; 'capped': string; 'deepsight': string; 'engram': string; 'fixContrast': string; 'fullstack': string; 'masterwork': string; 'quality': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/BadgeInfo.tsx ================================================ import { TOTAL_STAT_HASH } from 'app/search/d2-known-values'; import { getD1QualityColor } from 'app/shell/formatters'; import { isD1Item } from 'app/utils/item-utils'; import { InventoryWishListRoll, toUiWishListRoll } from 'app/wishlists/wishlists'; import { DamageType } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { BucketHashes } from 'data/d2/generated-enums'; import { useSelector } from 'react-redux'; import ElementIcon from '../dim-ui/ElementIcon'; import * as styles from './BadgeInfo.m.scss'; import RatingIcon from './RatingIcon'; import { DimItem } from './item-types'; import { notesSelector } from './selectors'; interface Props { item: DimItem; isCapped: boolean; wishlistRoll?: InventoryWishListRoll; } export function shouldShowBadge(item: DimItem) { const isBounty = Boolean(!item.primaryStat && item.objectives); const isStackable = Boolean(item.maxStackSize > 1); const isGeneric = !isBounty && !isStackable; const hideBadge = Boolean( item.location.hash === BucketHashes.Subclass || (item.isEngram && item.location.hash === BucketHashes.Engrams) || (isBounty && (item.complete || item.hidePercentage)) || (isStackable && item.amount === 1) || (isGeneric && !item.primaryStat?.value && !item.classified), ); return !hideBadge; } export default function BadgeInfo({ item, isCapped, wishlistRoll }: Props) { const isBounty = Boolean(!item.primaryStat && item.objectives); const isStackable = Boolean(item.maxStackSize > 1); const isGeneric = !isBounty && !isStackable; // For vendor armor that reports stats (thus often randomized), // show the total points as a means to indicate whether it's worth picking up const totalArmorStat = item.bucket?.inArmor && item.vendor && item.stats?.find((stat) => stat.statHash === TOTAL_STAT_HASH); const hideBadge = Boolean( item.location.hash === BucketHashes.Subclass || (item.isEngram && item.location.hash === BucketHashes.Engrams) || (isBounty && (item.complete || item.hidePercentage)) || (isStackable && item.amount === 1) || (isGeneric && !item.primaryStat?.value && !item.classified), ); if (hideBadge) { return null; } const badgeContent = (isBounty && `${Math.floor(100 * item.percentComplete)}%`) || (isStackable && item.amount.toString()) || (totalArmorStat && totalArmorStat.value.toString()) || (isGeneric && item.primaryStat?.value.toString()) || (item.classified && ); const fixContrast = item.element && (item.element.enumValue === DamageType.Arc || item.element.enumValue === DamageType.Void || item.element.enumValue === DamageType.Strand); const wishlistRollIcon = toUiWishListRoll(wishlistRoll); const summaryIcon = wishlistRollIcon !== undefined && ( ); return (
{isD1Item(item) && item.quality && (
{item.quality.min}%
)} {summaryIcon} {item.element && !(item.bucket.inWeapons && item.element.enumValue === DamageType.Kinetic) && ( )} {badgeContent}
); } /** * ClassifiedNotes shows the notes field for classified items as a way to make * them easier to ID. It's broken out into its own component so that the store * subscription for notes only happens for classified items. */ function ClassifiedNotes({ item }: { item: DimItem }) { const savedNotes = useSelector(notesSelector(item)); return <>{savedNotes ?? '???'}; } ================================================ FILE: src/app/inventory/ConnectedInventoryItem.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { queryValidSelector, searchFilterSelector } from 'app/search/items/item-search-filter'; import { stubTrue } from 'app/utils/functions'; import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { wishListSelector } from '../wishlists/selectors'; import InventoryItem from './InventoryItem'; import { DimItem } from './item-types'; import { isNewSelector, notesSelector, tagSelector } from './selectors'; const autoLockTaggedSelector = settingSelector('autoLockTagged'); /** * An item that can load its auxiliary state directly from Redux. Not suitable * for showing a ton of items, but useful! */ export default function ConnectedInventoryItem({ item, onClick, onShiftClick, onDoubleClick, hideSelectedSuper, dimArchived, allowFilter, ref, }: { item: DimItem; /** Make this item partially transparent when it does not match the current search filter */ allowFilter?: boolean; hideSelectedSuper?: boolean; ref?: React.Ref; onClick?: (e: React.MouseEvent) => void; onShiftClick?: (e: React.MouseEvent) => void; onDoubleClick?: (e: React.MouseEvent) => void; /** Make this item partially transparent if it has the archive tag */ dimArchived?: boolean; }) { // TODO: maybe send these down via Context? const tag = useSelector(tagSelector(item)); const currentFilter = useSelector(searchFilterSelector); const validQuery = useSelector(queryValidSelector); const autoLockTagged = useSelector(autoLockTaggedSelector); const defaultFilterActive = currentFilter === stubTrue; const isNew = useSelector(isNewSelector(item)); const notes = useSelector(notesSelector(item)); const wishlistRoll = useSelector(wishListSelector(item)); const searchHidden = // dim this item if there's no search filter and it's archived (dimArchived && defaultFilterActive && tag === 'archive') || // or if there is a valid filter and it doesn't meet the condition (allowFilter && validQuery && !currentFilter(item)); return useMemo( () => ( ), [ ref, isNew, item, notes, onClick, onDoubleClick, onShiftClick, searchHidden, hideSelectedSuper, tag, wishlistRoll, autoLockTagged, ], ); } ================================================ FILE: src/app/inventory/DragPerformanceFix.m.scss ================================================ // A div that overlays the whole inventory to block hit-testing during drag. // Note the z-index which should be above all other elements. Except for the // sub-bucket overlays. .dragPerfFix { // to test, set a nonzero opacity and a background color opacity: 0; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 8; display: none; .dragPerfShow & { display: block; } } // Each sub-bucket (inventory drag target) gets its own overlay as well, with a // z-index one higher than the global overlay. This allows them to still receive // drag events. :global(.sub-bucket) { &::before { content: ''; // to test, set a nonzero opacity and a background color opacity: 0; width: 100%; height: 100%; position: absolute; z-index: 9; display: none; .dragPerfShow & { display: block; } } } ================================================ FILE: src/app/inventory/DragPerformanceFix.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'dragPerfFix': string; 'dragPerfShow': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/DragPerformanceFix.tsx ================================================ import * as styles from './DragPerformanceFix.m.scss'; /** * This is a workaround for sluggish dragging in Chrome on Windows. It may or * may not be related to Logitech mouse drivers or high-DPI mice, but in Chrome * on Windows only, some users experience a problem where they can drag items, * but the drag targets do not get events very quickly, so it may take a second * or two of hovering over a drop target to make it light up. This was still a * problem as of January 2025. * * This workaround is to put a full-screen invisible div over the entire app, * and then put separate invisible divs over each drop target. That simplifies * the hit-testing Chrome has to do, and makes dragging feel normal. */ export default function DragPerformanceFix() { // Rarely (possibly never in typical usage), a browser will forget to dispatch the dragEnd event // So we try not to trap the user here by allowing them to click away the overlay. return
; } export function showDragFixOverlay() { document.body.classList.add(styles.dragPerfShow); } export function hideDragFixOverlay() { document.body.classList.remove(styles.dragPerfShow); } ================================================ FILE: src/app/inventory/DraggableInventoryItem.m.scss ================================================ @use '../variables.scss' as *; .engram { @include interactive($hover: true) { // don't display the default outline when hovering over a draggable subclass item outline: none; // allow the pseudo-element to render outside the bounds of the item contain: layout style; // render a hexagon-shaped pseudo-element to act as the border &::before { content: ''; position: absolute; width: var(--item-size); height: var(--item-size); box-sizing: border-box; background: url('images/engram_outline.svg'); } } } .cantDrag { cursor: pointer; } ================================================ FILE: src/app/inventory/DraggableInventoryItem.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'cantDrag': string; 'engram': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/DraggableInventoryItem.tsx ================================================ import { hideItemPopup } from 'app/item-popup/item-popup'; import clsx from 'clsx'; import React from 'react'; import { useDrag } from 'react-dnd'; import { hideDragFixOverlay, showDragFixOverlay } from './DragPerformanceFix'; import * as styles from './DraggableInventoryItem.m.scss'; import { isDragging$ } from './drag-events'; import { DimItem } from './item-types'; let dragTimeout: number | null = null; export default function DraggableInventoryItem({ children, item, anyBucket = false, }: { item: DimItem; /** Allow the item to be dragged onto any bucket, not just its own. */ anyBucket?: boolean; children?: React.ReactNode; }) { const canDrag = (!item.location.inPostmaster || item.destinyVersion === 2) && item.notransfer ? item.equipment : item.equipment || item.bucket.hasTransferDestination; const [_collect, dragRef] = useDrag( () => ({ type: item.location.inPostmaster || anyBucket ? 'postmaster' : item.notransfer ? `${item.owner}-${item.bucket.hash}` : item.bucket.hash.toString(), item: () => { hideItemPopup(); dragTimeout = requestAnimationFrame(() => { dragTimeout = null; showDragFixOverlay(); }); isDragging$.next(true); return item; }, end: () => { if (dragTimeout !== null) { cancelAnimationFrame(dragTimeout); } hideDragFixOverlay(); isDragging$.next(false); }, canDrag, }), [item], ); return (
{ dragRef(el); }} className={clsx('item-drag-container', { [styles.engram]: item.isEngram, [styles.cantDrag]: !canDrag, })} > {children}
); } ================================================ FILE: src/app/inventory/InventoryItem.m.scss ================================================ @use '../variables.scss' as *; // Items hidden by search. .searchHidden { opacity: var(--search-hidden-opacity, 0.2); // TODO: Testing whether scale transform triggers slow sheet animations: https://github.com/DestinyItemManager/DIM/issues/7458 // transform: scale(0.75); } // The top-level item container. Global because it's referenced by other styles. :global(.item) { position: relative; contain: layout paint style size; box-sizing: border-box; width: var(--item-size); height: var(--item-size); // searchHidden will adjust opacity/transform, this transitions them transition: opacity 0.2s, transform 0.2s; } .hasBadge { height: calc(var(--item-size) + #{$badge-height} - #{$item-border-width}); } // The wrapper for draggable items. Global because it's referenced by other styles. :global(.item-drag-container) { contain: layout paint style; box-sizing: border-box; width: var(--item-size); cursor: grab; @include interactive($hover: true) { @include draggable-hover-border; } } // Subclass items .subclassBase { border-color: transparent !important; } .subclass { &::before { background-image: url('../../images/subclass-border.svg'); content: ''; position: absolute; width: 100%; height: 100%; z-index: 1; left: 0; top: 0; } } .subclassSuperIcon { position: absolute; width: calc(var(--item-size) - 4px); height: calc(var(--item-size) - 4px); left: 2px; top: 2px; } // The bar we show for items that have some progress on them .xpBar { background: rgb(0, 0, 0, 0.5); position: absolute; width: auto; left: $item-border-width + 2px; right: $item-border-width + 2px; opacity: 1; top: $item-border-width + 2px; height: calc(var(--item-size) / 9); } .xpBarAmount { height: 100%; background-color: $xp; } // The container for the tag/notes/wishlist icons .icons { position: absolute; right: $item-border-width + 2px; top: calc(var(--item-size) - #{$badge-height}); display: flex; flex-direction: row; } // Individual icons in the icon tray .icon { display: block; position: static; width: calc(var(--item-size) / 5); height: calc(var(--item-size) / 5); font-size: calc(var(--item-size) / 5); margin-right: 1px; color: #29f36a; // #5eff92; filter: drop-shadow(0 0 2px rgb(0, 0, 0, 0.8)); } .warningIcon { display: block; width: calc(var(--item-size) / 5); height: calc(var(--item-size) / 5); font-size: calc(var(--item-size) / 5); position: absolute; top: $item-border-width + 2px; right: $item-border-width + 3px; pointer-events: none; filter: drop-shadow(0 0 2px rgb(0, 0, 0, 0.8)); } .topRight { display: block; width: calc(#{dim-item-px(13)}); height: calc(#{dim-item-px(13)}); position: absolute; top: $item-border-width; right: $item-border-width; pointer-events: none; background: no-repeat center; background-size: contain; } .statFocus { filter: drop-shadow(0 0 1px rgb(0, 0, 0, 1)) drop-shadow(0 0 0.5px rgb(0, 0, 0, 1)); } .weaponFrame { width: calc(#{dim-item-px(11)}); height: calc(#{dim-item-px(11)}); background-color: #222; background-size: 130%; top: calc($item-border-width * 2); right: calc($item-border-width * 2); border-radius: calc(#{dim-item-px(1.5)}); filter: drop-shadow(0 0 0.75px rgb(0, 0, 0)); } .ergoSum { top: $item-border-width; right: $item-border-width; width: calc(#{dim-item-px(13)}); height: calc(#{dim-item-px(13)}); background-color: #555; border-radius: 50%; filter: none; } ================================================ FILE: src/app/inventory/InventoryItem.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'ergoSum': string; 'hasBadge': string; 'icon': string; 'icons': string; 'searchHidden': string; 'statFocus': string; 'subclass': string; 'subclassBase': string; 'subclassSuperIcon': string; 'topRight': string; 'warningIcon': string; 'weaponFrame': string; 'xpBar': string; 'xpBarAmount': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/InventoryItem.tsx ================================================ import { AlertIcon } from 'app/dim-ui/AlertIcon'; import { SpecialtyModSlotIcon } from 'app/dim-ui/SpecialtyModSlotIcon'; import { useD2Definitions } from 'app/manifest/selectors'; import { percent } from 'app/shell/formatters'; import { getArmor3StatFocus, getSpecialtySocketMetadata, isArmor3, isArtifice, nonPullablePostmasterItem, } from 'app/utils/item-utils'; import { getWeaponArchetype } from 'app/utils/socket-utils'; import { DestinyAmmunitionType } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { BucketHashes, ItemCategoryHashes } from 'data/d2/generated-enums'; import React, { useMemo } from 'react'; import BungieImage, { bungieBackgroundStyle } from '../dim-ui/BungieImage'; import { AppIcon, lockIcon, stickyNoteIcon } from '../shell/icons'; import { InventoryWishListRoll } from '../wishlists/wishlists'; import BadgeInfo, { shouldShowBadge } from './BadgeInfo'; import { TagValue } from './dim-item-info'; import * as styles from './InventoryItem.m.scss'; import { DimItem } from './item-types'; import ItemIcon from './ItemIcon'; import ItemIconPlaceholder from './ItemIconPlaceholder'; import NewItemIndicator from './NewItemIndicator'; import { getSubclassIconInfo } from './subclass'; import { canSyncLockState } from './SyncTagLock'; import TagIcon from './TagIcon'; export default function InventoryItem({ item, isNew, tag, notes, searchHidden, autoLockTagged, wishlistRoll, hideSelectedSuper, onClick, onShiftClick, onDoubleClick, ref, }: { item: DimItem; /** Show this item as new? */ isNew?: boolean; /** User defined tag */ tag?: TagValue; /** Notes for the item. Used to show the icon and put notes in tooltips. */ notes?: string; /** Has this been hidden by a search? */ searchHidden?: boolean; /** Is the setting to automatically lock tagged items on? */ autoLockTagged: boolean; wishlistRoll?: InventoryWishListRoll; /** Hide the selected Super ability on subclasses? */ hideSelectedSuper?: boolean; ref?: React.Ref; /** TODO: item locked needs to be passed in */ onClick?: (e: React.MouseEvent) => void; onShiftClick?: (e: React.MouseEvent) => void; onDoubleClick?: (e: React.MouseEvent) => void; }) { let enhancedOnClick = onClick; if (onShiftClick) { enhancedOnClick = (e: React.MouseEvent) => { if (e.shiftKey) { onShiftClick(e); } else if (onClick) { onClick(e); } }; } const hasNotes = Boolean(notes); const savedNotes = hasNotes ? `\nNotes: ${notes}` : ''; const isSubclass = item?.destinyVersion === 2 && item.bucket.hash === BucketHashes.Subclass; const subclassIconInfo = isSubclass && !hideSelectedSuper ? getSubclassIconInfo(item) : null; const hasBadge = shouldShowBadge(item); const itemStyles = clsx('item', { [styles.searchHidden]: searchHidden, [styles.subclass]: isSubclass, [styles.hasBadge]: hasBadge, }); // Subtitle for engram powerlevel vs regular item type const subtitle = item.destinyVersion === 2 && item.isEngram ? item.power : item.typeName; const statFocusHash = item.bucket.inArmor && isArmor3(item) ? getArmor3StatFocus(item)?.[0] : undefined; const hasInterestingModSlots = item.bucket.inArmor && (getSpecialtySocketMetadata(item) || isArtifice(item)); // Memoize the contents of the item - most of the time if this is re-rendering it's for a search, or a new item const contents = useMemo(() => { // Subclasses have limited, but customized, display. They can't be new, or tagged, or locked, etc. if (subclassIconInfo) { return ( <> {subclassIconInfo.base ? ( ) : ( )} {subclassIconInfo.super && ( )} ); } const isCapped = item.maxStackSize > 1 && item.amount === item.maxStackSize && item.uniqueStack; return ( <> {item.percentComplete > 0 && !item.complete && (
)} {(tag || item.locked || hasNotes) && (
{item.locked && (!autoLockTagged || !tag || !canSyncLockState(item)) && ( )} {tag && } {hasNotes && }
)} {statFocusHash !== undefined ? ( ) : hasInterestingModSlots ? ( ) : ( item.bucket.inWeapons && )} {(nonPullablePostmasterItem(item) && ) || ($featureFlags.newItems && isNew && )} ); }, [ isNew, item, hasNotes, subclassIconInfo, tag, wishlistRoll, autoLockTagged, statFocusHash, hasInterestingModSlots, ]); return (
{contents}
); } function StatFocus({ statHash }: { statHash: number }) { const defs = useD2Definitions()!; const icon = defs.Stat.get(statHash).displayProperties.icon; return ( defs && ( ) ); } function WeaponFrame({ item }: { item: DimItem }) { const isErgoSum = item.ammoType === DestinyAmmunitionType.Special && item.itemCategoryHashes.includes(ItemCategoryHashes.Sword); if (!item.isExotic || isErgoSum) { const frame = getWeaponArchetype(item); return ( frame && (
) ); } } ================================================ FILE: src/app/inventory/ItemDragPreview.tsx ================================================ import { usePreview } from 'react-dnd-multi-backend'; import ConnectedInventoryItem from './ConnectedInventoryItem'; import { DimItem } from './item-types'; /** * When we are using the React DnD Touch Backend (iOS < 15 only), this will * render a placeholder so users can see the item they're dragging. * * This can be removed when we drop iOS 14 support and the TouchBackend. */ export function ItemDragPreview() { const preview = usePreview(); if ( !preview.display || // Basic check that it's a DimItem !('bucket' in (preview.item as any)) ) { return null; } const style = preview.style; const item = preview.item as DimItem; return (
); } ================================================ FILE: src/app/inventory/ItemIcon.m.scss ================================================ @use '../variables.scss' as *; @use 'sass:math'; $legendaryBg: #522f65; $exoticBg: #ceae33; $basicBg: #c3bcb4; $rareBg: #5076a3; $commonBg: #366f42; // The image within the overall item. Global because it's referenced by other styles. :global(.item-img) { display: block; position: relative; width: var(--item-size); height: var(--item-size); box-sizing: border-box; border: $item-border-width solid var(--theme-item-polaroid); background-size: contain; background-position: center; background-repeat: no-repeat; pointer-events: none; &:focus { outline: none; } // Used by vendors &:global(.transparent) { border-color: transparent; } > * { position: absolute; background-size: contain; background-position: center; background-repeat: no-repeat; pointer-events: none; height: 100%; width: 100%; } } .legendary { background-color: $legendaryBg; } .exotic { background-color: $exoticBg; } .basic { background-color: $basicBg; } .rare { background-color: $rareBg; } .common { background-color: $commonBg; } // Alternate border styles .masterwork { border-color: var(--theme-item-polaroid-masterwork); } .deepsight { border-color: $deepsight-border-color; &::after { content: ''; position: absolute; box-sizing: border-box; inset: 0; border: 2px solid $deepsight-border-color; } } .animatedBackground { display: block; opacity: 0; content-visibility: hidden; @media (prefers-reduced-motion: no-preference) { transition: opacity 0.2s ease-out, content-visibility 0.2s allow-discrete; :global(.item):hover & { opacity: 1; content-visibility: auto; } } } .hasAltIcon { display: block; opacity: calc(1 - var(--ornament-display-opacity, 1)); content-visibility: var(--ornament-display-visibility-inverse, hidden); @media (prefers-reduced-motion: no-preference) { transition: opacity 0.2s ease-out, content-visibility 0.2s allow-discrete; } :global(.item):hover &.isArmor { opacity: 1; content-visibility: auto; } } .altIcon { display: block; opacity: var(--ornament-display-opacity, 1); content-visibility: var(--ornament-display-visibility, auto); @media (prefers-reduced-motion: no-preference) { transition: opacity 0.2s ease-out, content-visibility 0.2s allow-discrete; } :global(.item):hover &.isArmor { opacity: 0; content-visibility: hidden; } } // This is the same size as the item image but shifted up and left by 1px to overlap the border .shiftedLayer { top: -1 * $item-border-width; left: -1 * $item-border-width; } .adjustOpacity { // Bungie said they darkened the overlay "by 40%" so let's un-darken it that much opacity: math.div(1, 1.4); } .craftedLayer { top: 1 * $item-border-width; left: -1 * $item-border-width; } // Completed items or capped stackables image .complete { border-color: var(--theme-item-polaroid-capped); } // Engrams and packages .borderless { border-color: transparent; } .seasonIcon { top: calc(#{dim-item-px(0.5)}); left: calc(#{dim-item-px(0.5)}); height: calc(#{dim-item-px(11)}); width: calc(#{dim-item-px(11)}); } .energyCost { position: absolute; inset: $item-border-width; pointer-events: none; > text { fill: white; } } .highlightedObjective { position: absolute; display: block; width: calc((var(--item-size) + 1px) / 2) !important; height: calc((var(--item-size) + 1px) / 2) !important; right: 0; top: calc(var(--item-size) - ((var(--item-size) + 1px) / 2) - 1px); // Seems to fix https://github.com/DestinyItemManager/DIM/issues/7974 pointer-events: none; } .inverted { border-color: #222; } .strandColorFix { filter: hue-rotate(265deg) brightness(1.3); } ================================================ FILE: src/app/inventory/ItemIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'adjustOpacity': string; 'altIcon': string; 'animatedBackground': string; 'basic': string; 'borderless': string; 'common': string; 'complete': string; 'craftedLayer': string; 'deepsight': string; 'energyCost': string; 'exotic': string; 'hasAltIcon': string; 'highlightedObjective': string; 'inverted': string; 'isArmor': string; 'legendary': string; 'masterwork': string; 'rare': string; 'seasonIcon': string; 'shiftedLayer': string; 'strandColorFix': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/ItemIcon.tsx ================================================ import { itemConstants } from 'app/destiny2/d2-definitions'; import { bungieBackgroundStyle, bungieBackgroundStyles } from 'app/dim-ui/BungieImage'; import BucketIcon from 'app/dim-ui/svgs/BucketIcon'; import { getBucketSvgIcon } from 'app/dim-ui/svgs/itemCategory'; import { useD2Definitions } from 'app/manifest/selectors'; import { d2MissingIcon, ItemRarityMap, ItemRarityName } from 'app/search/d2-known-values'; import { compact } from 'app/utils/collections'; import { errorLog } from 'app/utils/log'; import { isArmorArchetypePlug, isModCostVisible } from 'app/utils/socket-utils'; import { DestinyInventoryItemDefinition, TierType } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { BucketHashes, ItemCategoryHashes, PlugCategoryHashes, TraitHashes, } from 'data/d2/generated-enums'; import holofoilAnim from 'images/holofoil-anim.apng'; import pursuitComplete from 'images/pursuitComplete.svg'; import { DimItem } from './item-types'; import * as styles from './ItemIcon.m.scss'; import { isPluggableItem } from './store/sockets'; const itemTierStyles: Record = { Legendary: styles.legendary, Exotic: styles.exotic, Common: styles.basic, Rare: styles.rare, Uncommon: styles.common, Unknown: styles.common, Currency: styles.common, }; const strandWrongColorPlugCategoryHashes = [ PlugCategoryHashes.TitanStrandClassAbilities, PlugCategoryHashes.HunterStrandClassAbilities, PlugCategoryHashes.WarlockStrandClassAbilities, PlugCategoryHashes.TitanStrandMovement, PlugCategoryHashes.HunterStrandMovement, PlugCategoryHashes.WarlockStrandMovement, ]; export function getItemImageStyles(item: DimItem, className?: string) { const isCapped = item.maxStackSize > 1 && item.amount === item.maxStackSize && item.uniqueStack; const borderless = (item?.destinyVersion === 2 && (item.bucket.hash === BucketHashes.Subclass || item.itemCategoryHashes.includes(ItemCategoryHashes.Packages))) || item.isEngram; const itemImageStyles = clsx('item-img', className, { [styles.complete]: item.complete || isCapped, [styles.borderless]: borderless, [styles.masterwork]: item.masterwork, [styles.deepsight]: item.deepsightInfo, [itemTierStyles[item.rarity]]: !borderless, }); return itemImageStyles; } // BRAVE and Rite of the Nine holofoils have a different background const oldShinyTraitHashes = [TraitHashes.ReleasesV730Season, TraitHashes.ReleasesV820Season]; /** * This is just the icon part of the inventory tile - without the bottom stats bar, tag icons, etc. * This exists because we have to do a fair bit of work to make the icon look like it does in game * with respect to masterwork, season icons, mod overlays, etc. * * This renders just a fragment - it always needs to be rendered inside another div with class "item". * * Since this is used a *lot*, it should not use any hooks, subscriptions, etc. */ export default function ItemIcon({ item, className }: { item: DimItem; className?: string }) { const classifiedPlaceholder = item.icon === d2MissingIcon && item.classified && getBucketSvgIcon(item.bucket.hash); const itemImageStyles = getItemImageStyles(item, className); // Sadly we can't just layer all the backgrounds into a single div because: // 1) Some of them need to be offset a bit because we display the whole image // while in-game they display a border over the icon. // 2) Some of the backgrounds like the masterwork glow and season stripe need // to be lower opacity to match the in-game look. // 3) We want to show the animated holofoil effect only on hover (and even // then only if the user allows animation). // Keep in mind that CSS multiple backgrounds go from front to back, so that's // how these arrays are. const backgrounds = compact([ // The ornament knot background (item.ornamentIconDef || item.itemCategoryHashes.includes(ItemCategoryHashes.Mods_Ornament) || item.itemCategoryHashes.includes(ItemCategoryHashes.WeaponModsOrnaments)) && (item.rarity === 'Exotic' ? itemConstants?.universalOrnamentExoticBackgroundOverlayPath : item.rarity === 'Legendary' ? itemConstants?.universalOrnamentLegendaryBackgroundOverlayPath : itemConstants?.universalOrnamentBackgroundOverlayPath), // Holofoil background (two types for some reason, BRAVE weapons have one with stripes) item.holofoil ? oldShinyTraitHashes.some((h) => item.traitHashes?.includes(h)) ? itemConstants?.holofoilBackgroundOverlayPath : itemConstants?.holofoil900BackgroundOverlayPath : undefined, item.iconDef?.specialBackground, // I don't think any icon defines this // So far this is only a solid color, which we already handle. Can // uncomment if it ever becomes interesting. // item.iconDef?.background, ]); const animatedBackground = item.holofoil && !oldShinyTraitHashes.some((h) => item.traitHashes?.includes(h)) ? holofoilAnim : undefined; // The actual item icon. Use the ornamented version where available. let foreground = (item.iconDef?.foreground ?? item.icon) || ''; let altIcon = ''; if (item.ornamentIconDef) { altIcon = item.ornamentIconDef.foreground; } if (!animatedBackground && !altIcon) { backgrounds.unshift(foreground); foreground = ''; } // This needs to be shown at half opacity to match the in-game look const masterworkGlow = item.masterwork && (item.isExotic ? itemConstants?.masterworkExoticOverlayPath : itemConstants?.masterworkOverlayPath); // These are aligned with the border, not the image. let seasonBanner = item.iconDef?.secondaryBackground && itemConstants?.watermarkDropShadowPath; const craftedOverlays = compact([ // The crafted/enhanced icon item.crafted === 'crafted' ? itemConstants?.craftedOverlayPath : item.crafted === 'enhanced' ? itemConstants?.enhancedItemOverlayPath : undefined, // Crafted item background item.crafted ? itemConstants?.craftedBackgroundPath : undefined, ]); // These are aligned with the border, not the image const seasonAndPips = compact([ // Featured flags item.featured ? itemConstants?.featuredItemFlagPath : undefined, // Tier pips item.tier > 0 && !item.isEngram && itemConstants?.gearTierOverlayImagePaths[Math.min(item.tier - 1, 4)], ]); if (craftedOverlays.length === 0 && seasonBanner) { seasonAndPips.push(seasonBanner); seasonBanner = ''; } const seasonIcon = item.iconDef?.secondaryBackground; return ( <> {classifiedPlaceholder ? ( ) : !item.iconDef ? (
) : (
{animatedBackground && ( )} {foreground && (
)} {altIcon && (
)} {masterworkGlow && (
)} {seasonBanner && (
)} {craftedOverlays.length > 0 && (
)} {seasonAndPips.length > 0 && (
)} {seasonIcon && (
)}
)} {item.plug?.energyCost !== undefined && item.plug.energyCost > 0 && ( {item.plug.energyCost} )} {item.highlightedObjective && !item.deepsightInfo && ( )} ); } /** * A variant of ItemIcon that operates directly on an item definition. */ export function DefItemIcon({ itemDef, className, borderless, }: { itemDef: DestinyInventoryItemDefinition; className?: string; borderless?: boolean; }) { const defs = useD2Definitions()!; if (!itemDef) { errorLog('temp-deficon', new Error('DefItemIcon was called with a missing def')); return null; } // This is only ever used in D2 if (!itemConstants) { return null; } const classifiedPlaceholder = (!itemDef.displayProperties.icon || itemDef.displayProperties.icon === d2MissingIcon) && itemDef.redacted && itemDef.inventory && getBucketSvgIcon(itemDef.inventory.bucketTypeHash); const itemCategoryHashes = itemDef.itemCategoryHashes || []; borderless ||= itemDef.plug?.plugCategoryHash === PlugCategoryHashes.Intrinsics || isArmorArchetypePlug(itemDef) || itemCategoryHashes.includes(ItemCategoryHashes.Packages) || itemCategoryHashes.includes(ItemCategoryHashes.Engrams); const needsStrandColorFix = itemDef.plug && strandWrongColorPlugCategoryHashes.includes(itemDef.plug.plugCategoryHash); const isMasterworkMod = isPluggableItem(itemDef) && itemDef.plug.plugCategoryIdentifier.includes('.masterworks.stat.'); const itemImageStyles = clsx( 'item-img', className, { [styles.borderless]: borderless, [styles.strandColorFix]: needsStrandColorFix, }, !borderless && !itemDef.plug && itemDef.inventory && [itemTierStyles[ItemRarityMap[itemDef.inventory.tierType]]], ); const energyCost = getModCostInfo(itemDef); const iconDef = itemDef.displayProperties.iconHash ? defs.Icon.get(itemDef.displayProperties.iconHash) : null; const backgrounds = compact([ // The ornament knot background (itemDef.itemCategoryHashes?.includes(ItemCategoryHashes.Mods_Ornament) || itemDef.itemCategoryHashes?.includes(ItemCategoryHashes.WeaponModsOrnaments)) && (itemDef.inventory?.tierType === TierType.Exotic ? itemConstants.universalOrnamentExoticBackgroundOverlayPath : itemDef.inventory?.tierType === TierType.Superior ? itemConstants.universalOrnamentLegendaryBackgroundOverlayPath : itemConstants.universalOrnamentBackgroundOverlayPath), // Holofoil background (two types for some reason, BRAVE weapons have one with stripes) itemDef.isHolofoil ? oldShinyTraitHashes.some((h) => itemDef.traitHashes?.includes(h)) ? itemConstants.holofoilBackgroundOverlayPath : itemConstants.holofoil900BackgroundOverlayPath : undefined, iconDef?.background, ]); const animatedBackground = itemDef.isHolofoil && !oldShinyTraitHashes.some((h) => itemDef.traitHashes?.includes(h)) ? holofoilAnim : undefined; // The actual item icon. Use the ornamented version where available. const foreground = compact([ // When the icon is a masterwork mod, the season background is actually a full // size overlay that has the level. isMasterworkMod && iconDef?.secondaryBackground, iconDef?.foreground ?? itemDef.displayProperties.icon, ]); if (!animatedBackground) { backgrounds.unshift(...foreground); foreground.splice(0, foreground.length); } // These are aligned with the border, not the image const seasonAndPips = compact([ // Featured flags itemDef.isFeaturedItem ? itemConstants.featuredItemFlagPath : undefined, iconDef?.secondaryBackground && !isMasterworkMod && itemConstants.watermarkDropShadowPath, ]); // When the icon is a masterwork mod, the season background is actually a full // size overlay that has the level. const seasonIcon = !isMasterworkMod && iconDef?.secondaryBackground; return ( <> {classifiedPlaceholder ? ( ) : !iconDef ? (
) : (
{animatedBackground && ( )} {foreground.length > 0 &&
} {seasonAndPips.length > 0 && (
)} {seasonIcon && (
)}
)} {energyCost !== undefined && energyCost > 0 && ( {energyCost} )} ); } /** * given a mod definition or hash, returns its energy cost if it should be shown */ function getModCostInfo(mod: DestinyInventoryItemDefinition) { if (isPluggableItem(mod) && isModCostVisible(mod)) { return mod.plug.energyCost!.energyCost; } return undefined; } ================================================ FILE: src/app/inventory/ItemIconPlaceholder.m.scss ================================================ @use '../variables.scss' as *; .placeholderBadge { height: calc(var(--item-size) + #{$badge-height}) !important; border-bottom-width: calc(#{$item-border-width} + #{$badge-height}) !important; } ================================================ FILE: src/app/inventory/ItemIconPlaceholder.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'placeholderBadge': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/ItemIconPlaceholder.tsx ================================================ // TODO: cache intersection observer? import clsx from 'clsx'; import { useEffect, useRef, useState } from 'react'; import { getItemImageStyles } from './ItemIcon'; import * as styles from './ItemIconPlaceholder.m.scss'; import { DimItem } from './item-types'; // We'll use a single intersection observer instead of one per item, roughly following the strategy // from https://github.com/thebuilder/react-intersection-observer/blob/master/src/observe.ts const elements = new WeakMap void>(); const observer = 'IntersectionObserver' in window ? new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { const elem = entry.target; const callback = elements.get(elem); if (callback) { callback(); elements.delete(elem); observer.unobserve(elem); } } } }, { root: null, rootMargin: '16px', threshold: 0, }, ) : { // eslint-disable-next-line @typescript-eslint/no-empty-function observe: () => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function unobserve: () => {}, }; /** * A placeholder div that's the same size as our icon, which is replaced by its * children when it is roughly onscreen. This is to work around a major * performance regression on iOS Safari 15 where rendering image tags hangs the * browser. */ export default function ItemIconPlaceholder({ item, children, hasBadge, }: { item: DimItem; children: React.ReactNode; hasBadge: boolean; }) { const [visible, setVisible] = useState(false); const ref = useRef(null); useEffect(() => { const elem = ref.current; if (!elem) { return; } elements.set(elem, () => setVisible(true)); observer.observe(elem); return () => observer.unobserve(elem); }, []); return visible ? ( <>{children} ) : (
); } ================================================ FILE: src/app/inventory/ItemPopupTrigger.tsx ================================================ import { addCompareItem } from 'app/compare/actions'; import { compareOpenSelector } from 'app/compare/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { ThunkResult } from 'app/store/types'; import React, { JSX, useCallback, useEffect, useRef } from 'react'; import { ItemPopupExtraInfo, hideItemPopup, showItemPopup, showItemPopup$, } from '../item-popup/item-popup'; import { clearNewItem } from './actions'; import { DimItem } from './item-types'; /** * This provides a ref and onclick function for a component that will show the move popup for the provided item. */ export default function ItemPopupTrigger({ item, extraData, children, noCompare, }: { item: DimItem; extraData?: ItemPopupExtraInfo; /** Don't allow adding to compare */ noCompare?: boolean; children: ( ref: React.Ref, onClick: (e: React.MouseEvent) => void, ) => React.ReactNode; }): JSX.Element { const ref = useRef(null); const dispatch = useThunkDispatch(); const clicked = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); dispatch(itemPopupTriggerClicked(item, ref, extraData, noCompare)); }, [dispatch, extraData, item, noCompare], ); // Close the popup if this component is unmounted useEffect( () => () => { if (showItemPopup$.getCurrentValue()?.item?.index === item.index) { hideItemPopup(); } }, [item.index], ); return children(ref, clicked) as JSX.Element; } function itemPopupTriggerClicked( item: DimItem, ref: React.RefObject, extraData?: ItemPopupExtraInfo, noCompare?: boolean, ): ThunkResult { return async (dispatch, getState) => { dispatch(clearNewItem(item.id)); if (!noCompare && compareOpenSelector(getState())) { dispatch(addCompareItem(item)); } else if (ref.current) { showItemPopup(item, ref.current, extraData); } }; } ================================================ FILE: src/app/inventory/ItemPowerSet.m.scss ================================================ @use '../variables.scss' as *; .itemPowerSet { display: grid; margin: auto; width: fit-content; grid-template-columns: max-content max-content max-content max-content; gap: 1px 6px; align-items: center; img, svg { height: 20px; width: 20px; } .bucketName { text-align: right; opacity: 0.6; } .powerDiff { text-align: center; font-weight: bold; &.positive { color: $green; } &.negative { color: $red; } } .spanGrid { grid-column-end: span 4; hr { margin: 3px; } } } ================================================ FILE: src/app/inventory/ItemPowerSet.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bucketName': string; 'itemPowerSet': string; 'negative': string; 'positive': string; 'powerDiff': string; 'spanGrid': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/ItemPowerSet.tsx ================================================ import BucketIcon from 'app/dim-ui/svgs/BucketIcon'; import clsx from 'clsx'; import React from 'react'; import * as styles from './ItemPowerSet.m.scss'; import { DimItem } from './item-types'; export function ItemPowerSet({ items, powerFloor }: { items: DimItem[]; powerFloor: number }) { let lastSort: string | undefined; return (
{items.map((i, j) => { const sortChanged = Boolean(j) && lastSort !== i.bucket.sort; lastSort = i.bucket.sort; const powerDiff = (powerFloor - i.power) * -1; const diffSymbol = powerDiff > 0 ? '+' : ''; return ( {sortChanged && (
)} {i.bucket.name} {i.power} 0, [styles.negative]: powerDiff < 0, })} > {powerDiff ? `${diffSymbol}${powerDiff}` : ''}
); })}
); } ================================================ FILE: src/app/inventory/MoveNotifications.m.scss ================================================ @use '../variables.scss' as *; .progressIcon { height: calc(var(--item-size) / 2); width: calc(var(--item-size) / 2); z-index: 1; border-radius: 50%; background-color: rgb(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; place-self: center; :global(.app-icon) { height: calc(-4px + var(--item-size) / 2); width: calc(-4px + var(--item-size) / 2); font-size: calc(-4px + var(--item-size) / 2); } } .succeeded { background-color: $green; } .failed { background-color: $red; } .loadoutDetails { font-weight: bold; display: block; margin: 4px 0; :global(.app-icon) { border-radius: 50%; margin-right: 4px; } } .iconList { --item-size: 32px; display: flex; flex-flow: row wrap; margin: 8px 0; &:last-child { margin-bottom: 0; } > * { margin: 0 4px 4px 0; } } .loadoutItemPending { opacity: 0.3; transform: scale(0.8); } .loadoutItemFailed { outline: 2px solid $red; } .errorList { display: flex; flex-direction: column; gap: 4px; } .warning { margin: 8px 0; } .warningIcon { margin-right: 4px; } ================================================ FILE: src/app/inventory/MoveNotifications.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'errorList': string; 'failed': string; 'iconList': string; 'loadoutDetails': string; 'loadoutItemFailed': string; 'loadoutItemPending': string; 'progressIcon': string; 'succeeded': string; 'warning': string; 'warningIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/MoveNotifications.tsx ================================================ import { AlertIcon } from 'app/dim-ui/AlertIcon'; import { I18nKey, t, tl } from 'app/i18next-t'; import { LoadoutApplyPhase, LoadoutApplyState, LoadoutItemState, LoadoutModState, LoadoutSocketOverrideState, } from 'app/loadout-drawer/loadout-apply-state'; import InGameLoadoutIcon from 'app/loadout/ingame/InGameLoadoutIcon'; import { InGameLoadout, Loadout, isInGameLoadout } from 'app/loadout/loadout-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { NotificationError, NotifyInput } from 'app/notifications/notifications'; import { AppIcon, faCheckCircle, faExclamationCircle, refreshIcon } from 'app/shell/icons'; import { isEmpty } from 'app/utils/collections'; import { DimError } from 'app/utils/dim-error'; import { errorMessage } from 'app/utils/errors'; import { useThrottledSubscription } from 'app/utils/hooks'; import { Observable } from 'app/utils/observable'; import { LookupTable } from 'app/utils/util-types'; import clsx from 'clsx'; import { useEffect, useState } from 'react'; import ConnectedInventoryItem from './ConnectedInventoryItem'; import ItemIcon, { DefItemIcon } from './ItemIcon'; import * as styles from './MoveNotifications.m.scss'; import { DimItem } from './item-types'; import { DimStore } from './store-types'; /** How long to leave the notification up after it's done. */ const lingerMs = 2000; /** * Generate JSX for a move item notification. This isn't a component. */ export function moveItemNotification( item: DimItem, target: DimStore, movePromise: Promise, cancel: () => void, ): NotifyInput { return { promise: movePromise, duration: lingerMs, title: item.name, icon: , trailer: , body: t('ItemMove.MovingItem', { name: item.name, target: target.name, context: target.genderName, }), onCancel: cancel, }; } /** * Generate JSX for a loadout apply notification. This isn't a component. */ export function loadoutNotification( loadout: Loadout | InGameLoadout, stateObservable: Observable, loadoutPromise: Promise, cancel: () => void, ): NotifyInput { return { promise: loadoutPromise.catch((e) => { throw new NotificationError(errorMessage(e), { body: , type: stateObservable.getCurrentValue().inGameLoadoutInActivity ? 'warning' : 'error', }); }), duration: 5_000, title: t('Loadouts.NotificationTitle', { name: loadout.name }), icon: isInGameLoadout(loadout) && , body: , onCancel: cancel, }; } const messageByPhase: { [phase in LoadoutApplyPhase]: I18nKey } = { [LoadoutApplyPhase.NotStarted]: tl('Loadouts.NotStarted'), [LoadoutApplyPhase.Deequip]: tl('Loadouts.Deequip'), [LoadoutApplyPhase.MoveItems]: tl('Loadouts.MoveItems'), [LoadoutApplyPhase.EquipItems]: tl('Loadouts.EquipItems'), [LoadoutApplyPhase.SocketOverrides]: tl('Loadouts.SocketOverrides'), [LoadoutApplyPhase.ApplyMods]: tl('Loadouts.ApplyMods'), [LoadoutApplyPhase.ClearSpace]: tl('Loadouts.ClearingSpace'), [LoadoutApplyPhase.InGameLoadout]: tl('Loadouts.EquipInGameLoadout'), [LoadoutApplyPhase.Succeeded]: tl('Loadouts.Succeeded'), [LoadoutApplyPhase.Failed]: tl('Loadouts.Failed'), }; function ApplyLoadoutProgressBody({ stateObservable, }: { stateObservable: Observable; }) { // TODO: throttle subscription? const { phase, equipNotPossible, itemStates, socketOverrideStates, modStates, inGameLoadoutInActivity, } = useThrottledSubscription(stateObservable, 100); const defs = useD2Definitions()!; const progressIcon = phase === LoadoutApplyPhase.Succeeded ? faCheckCircle : phase === LoadoutApplyPhase.Failed ? faExclamationCircle : refreshIcon; const itemStatesList = Object.values(itemStates); // TODO: when we have per-item socket overrides this will probably need to be more subtle const socketOverrideStatesList = Object.values(socketOverrideStates); const groupErrors = (items: T[]) => Object.groupBy( items.filter(({ error }) => error), ({ error }) => (error instanceof DimError ? (error.bungieErrorCode()?.toString() ?? error.cause?.message) : undefined) ?? error?.message ?? 'Unknown', ); const groupedItemErrors = groupErrors(itemStatesList); const groupedModErrors = groupErrors(modStates); return ( <>
{t(messageByPhase[phase])}
{equipNotPossible && (
{t('BungieService.DestinyCannotPerformActionAtThisLocation')}
)} {inGameLoadoutInActivity && (
{t('Loadouts.ApplyInGameLoadoutInGame')}
)} {itemStatesList.length > 0 && (
{itemStatesList.map(({ item, state }) => (
))}
)} {!isEmpty(groupedItemErrors) && (
{Object.values(groupedItemErrors).map((errorStates) => (
{t('Loadouts.ItemErrorSummary', { count: errorStates.length })}{' '} {errorStates[0].error instanceof DimError && errorStates[0].error.cause ? errorStates[0].error.cause.message : (errorStates[0].error?.message ?? 'Unknown')}
))}
)} {socketOverrideStatesList.length > 0 && (
{socketOverrideStatesList.map(({ item, results }) => (
{Object.entries(results).map(([socketIndex, { plugHash, state }]) => (
))}
))}
)} {modStates.length > 0 && (
{modStates.map(({ modHash, state }, i) => (
))}
)} {!isEmpty(groupedModErrors) && (
{Object.values(groupedModErrors).map((errorStates) => (
{t('Loadouts.ModErrorSummary', { count: errorStates.length })}{' '} {errorStates[0].error instanceof DimError && errorStates[0].error.cause ? errorStates[0].error.cause.message : (errorStates[0].error?.message ?? 'Unknown')}
))}
)} ); } /** * Generate JSX for a pull from postmaster notification. This isn't a component. */ export function postmasterNotification( count: number, store: DimStore, promise: Promise, cancel: () => void, ): NotifyInput { // TODO: pass in a state updater that can communicate application state return { promise, duration: lingerMs, title: t('Loadouts.PullFromPostmasterPopupTitle'), trailer: , body: t('Loadouts.PullFromPostmasterNotification', { count, store: store.name, context: store.genderName, }), onCancel: cancel, }; } const enum MoveState { InProgress, Failed, Succeeded, } const moveStateClasses: LookupTable = { [MoveState.Failed]: styles.failed, [MoveState.Succeeded]: styles.succeeded, }; function MoveItemNotificationIcon({ completion }: { completion: Promise }) { const [inProgress, setInProgress] = useState(MoveState.InProgress); useEffect(() => { let cancel = false; completion .then(() => !cancel && setInProgress(MoveState.Succeeded)) .catch(() => !cancel && setInProgress(MoveState.Failed)); return () => { cancel = true; }; }, [completion]); const progressIcon = inProgress === MoveState.InProgress ? refreshIcon : inProgress === MoveState.Succeeded ? faCheckCircle : faExclamationCircle; return (
); } ================================================ FILE: src/app/inventory/NewItemIndicator.m.scss ================================================ @use '../variables.scss' as *; .newItem { display: block; width: calc(var(--item-size) * 0.08); height: calc(var(--item-size) * 0.08); border: calc(var(--item-size) * 0.08) solid $new-notification-dot; position: absolute; top: $item-border-width + 2px; right: $item-border-width + 2px; border-radius: 50%; pointer-events: none; background-color: white; filter: drop-shadow(0 0 2px rgb(0, 0, 0, 0.8)); } ================================================ FILE: src/app/inventory/NewItemIndicator.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'newItem': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/NewItemIndicator.tsx ================================================ import clsx from 'clsx'; import * as styles from './NewItemIndicator.m.scss'; export default function NewItemIndicator({ className }: { className?: string }) { return
; } ================================================ FILE: src/app/inventory/PullFromPostmaster.m.scss ================================================ @use '../variables.scss' as *; .badge { border-radius: 50%; border-radius: 10px; background: var(--theme-accent-primary); padding: 0 4px; color: var(--theme-text-invert); font-size: 9px; transition: all 150ms ease-out; line-height: 13px; } .button { composes: dim-button from global; align-self: center; display: inline-flex; align-items: center; gap: 4px; @include interactive($hover: true) { .badge { background-color: black; color: var(--theme-accent-primary); } } > :global(.app-icon) { margin-right: 1px; } } ================================================ FILE: src/app/inventory/PullFromPostmaster.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'badge': string; 'button': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/PullFromPostmaster.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { RootState } from 'app/store/types'; import { useState } from 'react'; import { useSelector } from 'react-redux'; import { pullablePostmasterItems, pullFromPostmaster } from '../loadout-drawer/postmaster'; import { AppIcon, refreshIcon, sendIcon } from '../shell/icons'; import { queueAction } from '../utils/action-queue'; import * as styles from './PullFromPostmaster.m.scss'; import { storesSelector } from './selectors'; import { DimStore } from './store-types'; export function PullFromPostmaster({ store }: { store: DimStore }) { const [working, setWorking] = useState(false); const dispatch = useThunkDispatch(); const hidePullFromPostmaster = useSelector(settingSelector('hidePullFromPostmaster')); const numPullablePostmasterItems = useSelector( (state: RootState) => pullablePostmasterItems(store, storesSelector(state)).length, ); if (hidePullFromPostmaster || numPullablePostmasterItems === 0) { return null; } const onClick = () => { queueAction(async () => { setWorking(true); try { await dispatch(pullFromPostmaster(store)); } finally { setWorking(false); } }); }; return ( ); } ================================================ FILE: src/app/inventory/RatingIcon.m.scss ================================================ .godroll { font-size: 0.8em !important; margin-right: auto; color: var(--theme-item-polaroid-godroll); } .trashlist { font-size: 0.8em !important; margin-right: auto; color: #d14334; } ================================================ FILE: src/app/inventory/RatingIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'godroll': string; 'trashlist': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory/RatingIcon.tsx ================================================ import { t } from 'app/i18next-t'; import { UiWishListRoll } from 'app/wishlists/wishlists'; import { AppIcon, thumbsDownIcon, thumbsUpIcon } from '../shell/icons'; import * as styles from './RatingIcon.m.scss'; export default function RatingIcon({ uiWishListRoll }: { uiWishListRoll: UiWishListRoll }) { if (uiWishListRoll === UiWishListRoll.Bad) { return ( ); } return ; } ================================================ FILE: src/app/inventory/SyncTagLock.tsx ================================================ import { TagValue } from '@destinyitemmanager/dim-api-types'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { errorLog, infoLog } from 'app/utils/log'; import { memo, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import { setItemLockState } from './item-move-service'; import { DimItem } from './item-types'; import { allItemsSelector, getTagSelector, profileErrorSelector } from './selectors'; /** Whether an item's lock state can be controlled by its tag (irrespective of whether it currently is) */ export function canSyncLockState(item: DimItem) { return ( item.lockable && item.taggable && // don't auto-lock crafted items because they must be unlocked to reshape and DIM shouldn't re-lock an item while the user is choosing new perks item.crafted !== 'crafted' ); } /** * Rather than getting all items that need to change lock state, we return just the first. * Once that item changes state, the selector will return the next item, and so on. */ function getNextItemToChangeLockState( allItems: DimItem[], getTag: (item: DimItem) => TagValue | undefined, ): [item: DimItem, lock: boolean] | [] { for (const item of allItems) { if (canSyncLockState(item)) { switch (getTag(item)) { case 'favorite': case 'keep': case 'archive': { if (!item.locked) { return [item, true]; } break; } case 'infuse': case 'junk': { if (item.locked) { return [item, false]; } break; } case undefined: break; } } } return []; } const getNextItemSelector = createSelector( allItemsSelector, getTagSelector, profileErrorSelector, (allItems, getTag, profileError) => profileError ? [] : getNextItemToChangeLockState(allItems, getTag), ); // Some extra protection against locking the same thing twice in parallel - for example if you // refreshed inventory while locking was already going on. We don't care so much if two separate items // lock in parallel though. const inProgressLocks = new Set(); /** * While this (invisible) component is in the tree, it will watch changes to the inventory and tag state, * and sync the tag state with the lock state. e.g. favorite items are always locked, junk items are always * unlocked. */ export default memo(function SyncTagLock() { const dispatch = useThunkDispatch(); const [nextItem, lock] = useSelector(getNextItemSelector); useEffect(() => { if (nextItem && lock !== undefined && !inProgressLocks.has(nextItem.id)) { (async () => { infoLog( 'autoLockTagged', lock ? 'Locking' : 'Unlocking', nextItem.name, 'to match its tag', ); inProgressLocks.add(nextItem.id); try { await dispatch(setItemLockState(nextItem, lock)); } catch (e) { errorLog( 'autoLockTagged', 'Failed to ', lock ? 'lock' : 'unlock', nextItem.name, 'to match its tag:', e, ); } finally { inProgressLocks.delete(nextItem.id); } })(); } }, [nextItem, lock, dispatch]); return null; }); ================================================ FILE: src/app/inventory/TagIcon.tsx ================================================ import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; import { t } from 'app/i18next-t'; import { AppIcon } from 'app/shell/icons'; import { itemTagList, tagConfig, TagValue } from './dim-item-info'; const tagIcons: { [tag: string]: string | IconDefinition | undefined } = {}; for (const tag of itemTagList) { if (tag.type) { tagIcons[tag.type] = tag.icon; } } export default function TagIcon({ className, tag }: { className?: string; tag: TagValue }) { return tagIcons[tag] ? ( ) : null; } ================================================ FILE: src/app/inventory/__snapshots__/d2-stores.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`process stores generates a correct armor CSV export 1`] = ` [ { "Archetype": undefined, "Class": 18, "Class (Base)": 16, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 32, "Grenade (Base)": 30, "Hash": 1444894250, "Health": 9, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529177247119179"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Strides of the Great Hunt", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Minor Weapons Mod*", "Void Weapon Surge*", "Void Weapon Surge*", "Void Weapon Surge*", "Illuminus Strides*", ], "Power": 10, "Rarity": "Legendary", "Season": 4, "Seasonal Mod": "lastwish", "Source": "lastwish", "Super": 4, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 82, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 15, "Weapons (Base)": 8, "Year": 2, }, { "Archetype": undefined, "Class": 22, "Class (Base)": 22, "Energy Capacity": 6, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 2443609020, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529193253791078"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Seventh Seraph Gloves", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 10, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 3, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 6, "Equippable": "Titan", "Equipped": true, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 3046434751, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529193263424628"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Seventh Seraph Gauntlets", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 10, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 3, }, { "Archetype": undefined, "Class": 9, "Class (Base)": 9, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 17, "Grenade (Base)": 17, "Hash": 1734144409, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529194095508332"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 2, "Melee (Base)": 2, "Name": "Mechaneer's Tricksleeves", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Spring-Loaded Mounting*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 11, "Super (Base)": 11, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 18, "Weapons (Base)": 18, "Year": 1, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 1, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 235591051, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529194290568490"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 6, "Melee (Base)": 6, "Name": "Promethium Spur", "New Gear": true, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Embers of Light*", ], "Power": 10, "Rarity": "Exotic", "Season": 9, "Seasonal Mod": "artifice", "Source": "", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 9, "Weapons (Base)": 9, "Year": 3, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 6, "Grenade (Base)": 6, "Hash": 1160559849, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529194309796730"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 12, "Melee (Base)": 12, "Name": "Dunemarchers", "New Gear": true, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Linear Actuators*", "Weapons Mod*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 50, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 23, "Weapons (Base)": 13, "Year": 1, }, { "Archetype": undefined, "Class": 9, "Class (Base)": 9, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 2240152949, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529194460339897"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 7, "Melee (Base)": 7, "Name": "Stronghold", "New Gear": true, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Clenched Fist*", ], "Power": 10, "Rarity": "Exotic", "Season": 6, "Seasonal Mod": "artifice", "Source": "", "Super": 24, "Super (Base)": 24, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 9, "Weapons (Base)": 9, "Year": 2, }, { "Archetype": undefined, "Class": 4, "Class (Base)": 2, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 4, "Grenade (Base)": 2, "Hash": 3671337107, "Health": 9, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529196739096092"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 9, "Melee (Base)": 7, "Name": "Iron Fellowship Grips", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Iron Lord's Pride*", ], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "ironbanner", "Super": 24, "Super (Base)": 22, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 75, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 25, "Weapons (Base)": 23, "Year": 3, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 6, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 3750210364, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529196852762424"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Holdfast Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 22, "Weapons (Base)": 22, "Year": 3, }, { "Archetype": undefined, "Class": 16, "Class (Base)": 14, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 12, "Hash": 1321354573, "Health": 22, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529198421306306"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 14, "Melee (Base)": 12, "Name": "Celestial Nighthawk", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Hawkeye Hack*", "Health Mod*", "Ashes to Assets*", "Heavy Ammo Finder*", "Dark Fluorescence*", "Hrafnagud*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 9, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 86, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 11, "Weapons (Base)": 9, "Year": 1, }, { "Archetype": undefined, "Class": 18, "Class (Base)": 18, "Energy Capacity": 1, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 18, "Grenade (Base)": 18, "Hash": 3084282676, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529205988691165"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Getaway Artist", "New Gear": true, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Dynamic Duo*", ], "Power": 10, "Rarity": "Exotic", "Season": 6, "Seasonal Mod": "artifice", "Source": "", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 2, }, { "Archetype": undefined, "Class": 17, "Class (Base)": 17, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 4057299719, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529211183048269"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 12, "Melee (Base)": 12, "Name": "Phoenix Protocol", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Battle-Hearth*", "Weapons Mod*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 72, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 21, "Weapons (Base)": 11, "Year": 2, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 1321354572, "Health": 3, "Health (Base)": 3, "Holofoil": false, "Id": ""6917529211323676310"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 13, "Melee (Base)": 13, "Name": "Knucklehead Radar", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Upgraded Sensor Pack*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 16, "Super (Base)": 16, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 24, "Weapons (Base)": 24, "Year": 1, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 6, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 8, "Hash": 1458739906, "Health": 20, "Health (Base)": 18, "Holofoil": false, "Id": ""6917529212209954620"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 13, "Melee (Base)": 6, "Name": "Wild Hunt Vest", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Minor Melee Mod*", "Charged Up*", "Charged Up*", "Emergency Reinforcement*", "Mercurian Sunrise*", "Iron Fellowship Vest*", ], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "", "Source": "seasonpass", "Super": 21, "Super (Base)": 19, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 83, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 11, "Weapons (Base)": 9, "Year": 4, }, { "Archetype": undefined, "Class": 13, "Class (Base)": 13, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 1190497097, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529212676304074"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 14, "Melee (Base)": 14, "Name": "Citan's Ramparts", "New Gear": true, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Assault Barricade*", ], "Power": 10, "Rarity": "Exotic", "Season": 10, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 59, "Total (Base)": 59, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 3, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 21, "Grenade (Base)": 21, "Hash": 2509940440, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529217619762584"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 2, "Melee (Base)": 2, "Name": "Iron Will Vest", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Iron Lord's Pride*", ], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "", "Source": "ironbanner", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 7, "Weapons (Base)": 7, "Year": 3, }, { "Archetype": undefined, "Class": 15, "Class (Base)": 13, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 17, "Grenade (Base)": 15, "Hash": 691578978, "Health": 12, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529217629873690"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Oathkeeper", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adamantine Brace*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 15, "Super (Base)": 13, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 76, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 13, "Weapons (Base)": 11, "Year": 2, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 6, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 20, "Hash": 3977387640, "Health": 4, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529221592709126"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Phobos Warden Mask", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Ammo Finder*", "Heavy Ammo Finder*", "Heavy Ammo Finder*", "Calus's Treasured*", "Iron Truage Casque*", ], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "", "Source": "strikes", "Super": 8, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 72, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 26, "Weapons (Base)": 24, "Year": 4, }, { "Archetype": undefined, "Class": 3, "Class (Base)": 3, "Energy Capacity": 1, "Equippable": "Warlock", "Equipped": true, "Event": "", "Grenade": 7, "Grenade (Base)": 7, "Hash": 3948284065, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529238086798976"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 7, "Melee (Base)": 7, "Name": "Astrocyte Verse", "New Gear": true, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Move to Survive*", ], "Power": 10, "Rarity": "Exotic", "Season": 7, "Seasonal Mod": "artifice", "Source": "", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 29, "Weapons (Base)": 29, "Year": 2, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 6, "Grenade (Base)": 6, "Hash": 2578771006, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529238086799985"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 14, "Melee (Base)": 14, "Name": "One-Eyed Mask", "New Gear": true, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Vengeance*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 16, "Weapons (Base)": 16, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 0, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 0, "Hash": 1021060724, "Health": 2, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529248316159693"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 2, "Melee (Base)": 0, "Name": "Legacy's Oath Cloak", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Weapons Mod*", "Outreach*", "Outreach*", "Reaper*", "Colubrid Forester*", "Illuminus Cloak*", ], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 2, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 22, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 12, "Weapons (Base)": 0, "Year": 4, }, { "Archetype": undefined, "Class": 19, "Class (Base)": 19, "Energy Capacity": 6, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 7, "Grenade (Base)": 7, "Hash": 3351935136, "Health": 8, "Health (Base)": 8, "Holofoil": false, "Id": ""6917529257088142693"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 6, "Melee (Base)": 6, "Name": "Wild Hunt Plate", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "", "Source": "seasonpass", "Super": 20, "Super (Base)": 20, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 4, }, { "Archetype": undefined, "Class": 19, "Class (Base)": 19, "Energy Capacity": 6, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 3180809346, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529257088142832"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 6, "Melee (Base)": 6, "Name": "Wild Hunt Greaves", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "", "Source": "seasonpass", "Super": 16, "Super (Base)": 16, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 12, "Weapons (Base)": 12, "Year": 4, }, { "Archetype": undefined, "Class": 19, "Class (Base)": 19, "Energy Capacity": 6, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 2545401128, "Health": 8, "Health (Base)": 8, "Holofoil": false, "Id": ""6917529257098138677"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 7, "Melee (Base)": 7, "Name": "Wild Hunt Gauntlets", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "", "Source": "seasonpass", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 4, }, { "Archetype": undefined, "Class": 19, "Class (Base)": 19, "Energy Capacity": 6, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 3887272785, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529257098141390"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 11, "Melee (Base)": 11, "Name": "Wild Hunt Helm", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 8, "Weapons (Base)": 8, "Year": 4, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 1264765761, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529257763011737"", "Loadouts": "", "Locked": true, "Masterwork Tier": 5, "Melee": 6, "Melee (Base)": 6, "Name": "Legacy's Oath Strides", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 16, "Weapons (Base)": 16, "Year": 4, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 6, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 2180477077, "Health": 15, "Health (Base)": 15, "Holofoil": false, "Id": ""6917529259302516394"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 16, "Melee (Base)": 16, "Name": "Vest of the Great Hunt", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 4, "Seasonal Mod": "lastwish", "Source": "lastwish", "Super": 16, "Super (Base)": 16, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 14, "Weapons (Base)": 14, "Year": 2, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 11, "Energy Capacity": 2, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 6, "Grenade (Base)": 6, "Hash": 3874247549, "Health": 8, "Health (Base)": 8, "Holofoil": false, "Id": ""6917529259487786849"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 7, "Melee (Base)": 7, "Name": "Armamentarium", "New Gear": true, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "And Another Thing*", ], "Power": 10, "Rarity": "Exotic", "Season": 3, "Seasonal Mod": "artifice", "Source": "", "Super": 19, "Super (Base)": 19, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 15, "Weapons (Base)": 15, "Year": 1, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 11, "Energy Capacity": 2, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 1887490701, "Health": 20, "Health (Base)": 20, "Holofoil": false, "Id": ""6917529271951173867"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 20, "Melee (Base)": 20, "Name": "Legacy's Oath Gauntlets", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 4, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 19, "Grenade (Base)": 19, "Hash": 3136019014, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529293108655018"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Holdfast Strides", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "seasonpass", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 10, "Weapons (Base)": 10, "Year": 3, }, { "Archetype": undefined, "Class": 4, "Class (Base)": 2, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 4, "Grenade (Base)": 2, "Hash": 48951631, "Health": 4, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529340883986339"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 8, "Melee (Base)": 6, "Name": "Praefectus Strides", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Kairos Bronze*", "Siegebreak Strides*", ], "Power": 10, "Rarity": "Legendary", "Season": 13, "Seasonal Mod": "", "Source": "seasonpass", "Super": 24, "Super (Base)": 22, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 76, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 32, "Weapons (Base)": 30, "Year": 4, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 6, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 2804094976, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529350285165739"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Praefectus Boots", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 13, "Seasonal Mod": "", "Source": "seasonpass", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 4, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 6, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 2166974634, "Health": 18, "Health (Base)": 18, "Holofoil": false, "Id": ""6917529350290716306"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Praefectus Robes", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 13, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 7, "Weapons (Base)": 7, "Year": 4, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 6, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 6, "Grenade (Base)": 6, "Hash": 3245065734, "Health": 20, "Health (Base)": 20, "Holofoil": false, "Id": ""6917529350295800036"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 8, "Melee (Base)": 8, "Name": "Praefectus Gloves", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 13, "Seasonal Mod": "", "Source": "seasonpass", "Super": 19, "Super (Base)": 19, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 7, "Weapons (Base)": 7, "Year": 4, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 6, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 412014143, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529350295800648"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Praefectus Plate", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 13, "Seasonal Mod": "", "Source": "seasonpass", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 22, "Weapons (Base)": 22, "Year": 4, }, { "Archetype": undefined, "Class": 9, "Class (Base)": 9, "Energy Capacity": 6, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 1947723211, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529350300120230"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Praefectus Greaves", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Weapons Mod*", ], "Power": 10, "Rarity": "Legendary", "Season": 13, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 75, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 26, "Weapons (Base)": 16, "Year": 4, }, { "Archetype": undefined, "Class": 4, "Class (Base)": 2, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 23, "Grenade (Base)": 21, "Hash": 215674186, "Health": 25, "Health (Base)": 18, "Holofoil": false, "Id": ""6917529356969050330"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Errant Knight 1.0", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Minor Health Mod*", "Firepower*", "Firepower*", "Heavy Handed*", "Iron Forerunner Grips*", ], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "engram", "Super": 11, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 81, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 14, "Weapons (Base)": 12, "Year": 1, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 6, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 10, "Hash": 2913217908, "Health": 14, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529356981500980"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 10, "Melee (Base)": 8, "Name": "Woven Firesmith Grips", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Satou Tribe*", "Legatus Grips*", ], "Power": 10, "Rarity": "Legendary", "Season": 5, "Seasonal Mod": "", "Source": "", "Super": 13, "Super (Base)": 11, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 75, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 18, "Weapons (Base)": 16, "Year": 2, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 10, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 26, "Grenade (Base)": 24, "Hash": 129332559, "Health": 4, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529363584635170"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Prime Zealot Strides", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Innervation*", "Arc Weapon Surge*", "Stacks on Stacks*", ], "Power": 10, "Rarity": "Legendary", "Season": 14, "Seasonal Mod": "vaultofglass", "Source": "vaultofglass", "Super": 9, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 77, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 22, "Weapons (Base)": 20, "Year": 4, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 8, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 1163283805, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529369212845768"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 6, "Melee (Base)": 6, "Name": "Gemini Jester", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Misdirection*", ], "Power": 10, "Rarity": "Exotic", "Season": 2, "Seasonal Mod": "artifice", "Source": "", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 16, "Weapons (Base)": 16, "Year": 1, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 2773056939, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529371065651122"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 2, "Melee (Base)": 2, "Name": "Graviton Forfeit", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Vanishing Shadow*", ], "Power": 10, "Rarity": "Exotic", "Season": 2, "Seasonal Mod": "artifice", "Source": "", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 20, "Weapons (Base)": 20, "Year": 1, }, { "Archetype": undefined, "Class": 5, "Class (Base)": 3, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 12, "Hash": 3562696927, "Health": 9, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529391094192245"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 13, "Melee (Base)": 11, "Name": "Wormhusk Crown", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Burning Souls*", ], "Power": 10, "Rarity": "Exotic", "Season": 3, "Seasonal Mod": "artifice", "Source": "", "Super": 8, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 74, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 25, "Weapons (Base)": 23, "Year": 1, }, { "Archetype": undefined, "Class": 15, "Class (Base)": 15, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 20, "Grenade (Base)": 20, "Hash": 475652357, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529399442271515"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Young Ahamkara's Spine", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Wish-Dragon Teeth*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 8, "Weapons (Base)": 8, "Year": 1, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 6, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 32, "Grenade (Base)": 30, "Hash": 3479737253, "Health": 19, "Health (Base)": 17, "Holofoil": false, "Id": ""6917529402411449980"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Lightkin Grips", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Minor Weapons Mod*", "Impact Induction*", "Heavy Handed*", "Grenade Kickstart*", "Iron Mossbone*", "Illuminus Grasps*", ], "Power": 10, "Rarity": "Legendary", "Season": 14, "Seasonal Mod": "", "Source": "servitor", "Super": 4, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 83, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 16, "Weapons (Base)": 9, "Year": 4, }, { "Archetype": undefined, "Class": 15, "Class (Base)": 15, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 18, "Grenade (Base)": 18, "Hash": 2268523867, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529424657352268"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Raiju's Harness", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Mobius Conduit*", ], "Power": 10, "Rarity": "Exotic", "Season": 10, "Seasonal Mod": "artifice", "Source": "", "Super": 11, "Super (Base)": 11, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 14, "Weapons (Base)": 14, "Year": 3, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 5, "Equippable": "Titan", "Equipped": true, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 3015085684, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529431777484028"", "Loadouts": "", "Locked": true, "Masterwork Tier": 5, "Melee": 2, "Melee (Base)": 2, "Name": "Legacy's Oath Helm", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 12, "Weapons (Base)": 12, "Year": 4, }, { "Archetype": undefined, "Class": 28, "Class (Base)": 26, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 11, "Grenade (Base)": 9, "Hash": 586128500, "Health": 18, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529450154939824"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 21, "Melee (Base)": 19, "Name": "Prime Zealot Mask", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Health Mod*", "Harmonic Siphon*", "Radiant Light*", "Kinetic Siphon*", "Queensguard Valor*", "Iron Truage Casque*", ], "Power": 10, "Rarity": "Legendary", "Season": 14, "Seasonal Mod": "vaultofglass", "Source": "vaultofglass", "Super": 4, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 86, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 4, "Weapons (Base)": 2, "Year": 4, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 2766109872, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529501085668452"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 2, "Melee (Base)": 2, "Name": "Raiden Flux", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Synapse Junctions*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 17, "Super (Base)": 17, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 69, "Total (Base)": 69, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 22, "Weapons (Base)": 22, "Year": 1, }, { "Archetype": undefined, "Class": 20, "Class (Base)": 13, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 32, "Grenade (Base)": 30, "Hash": 988607392, "Health": 11, "Health (Base)": 9, "Holofoil": false, "Id": ""6917529520949384299"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Scorned Baron Vest", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Minor Class Mod*", "Void Resistance*", "Void Resistance*", "Emergency Reinforcement*", "Regal Medallion*", "Iron Fellowship Vest*", ], "Power": 10, "Rarity": "Legendary", "Season": 4, "Seasonal Mod": "", "Source": "tangled", "Super": 4, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 82, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 11, "Weapons (Base)": 9, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 21, "Grenade (Base)": 21, "Hash": 903984858, "Health": 11, "Health (Base)": 11, "Holofoil": false, "Id": ""6917529527778301874"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 2, "Melee (Base)": 2, "Name": "Lucky Raspberry", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Probability Matrix*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 8, "Super (Base)": 8, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 20, "Weapons (Base)": 20, "Year": 1, }, { "Archetype": undefined, "Class": 3, "Class (Base)": 3, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 1474735277, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529534610903797"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 9, "Melee (Base)": 9, "Name": "The Sixth Coyote", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Double Dodge*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 23, "Weapons (Base)": 23, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 4001862073, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529575758314620"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 6, "Melee (Base)": 6, "Name": "Legacy's Oath Vest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 22, "Weapons (Base)": 22, "Year": 4, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 8, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 2766109874, "Health": 8, "Health (Base)": 8, "Holofoil": false, "Id": ""6917529683344521948"", "Loadouts": "", "Locked": true, "Masterwork Tier": 8, "Melee": 17, "Melee (Base)": 17, "Name": "The Dragon's Shadow", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Wraithmetal Mail*", "Super Mod*", "Void Resistance*", "Void Resistance*", "Queensguard Valor*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 16, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 79, "Total (Base)": 69, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 17, "Weapons (Base)": 17, "Year": 1, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 10, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 17, "Grenade (Base)": 15, "Hash": 2516879931, "Health": 4, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529746551236327"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Tusked Allegiance Mask", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Weapons Mod*", "Harmonic Siphon*", "Harmonic Targeting*", "Heavy Ammo Finder*", "House of Meyrin*", "Iron Truage Casque*", ], "Power": 10, "Rarity": "Legendary", "Season": 16, "Seasonal Mod": "", "Source": "psiops", "Super": 17, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 86, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 32, "Weapons (Base)": 20, "Year": 5, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 22, "Hash": 1615052875, "Health": 17, "Health (Base)": 17, "Holofoil": false, "Id": ""6917529747682713483"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Iron Forerunner Vest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Iron Lord's Pride*", ], "Power": 10, "Rarity": "Legendary", "Season": 15, "Seasonal Mod": "", "Source": "ironbanner", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 9, "Weapons (Base)": 9, "Year": 4, }, { "Archetype": undefined, "Class": 29, "Class (Base)": 29, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 803939997, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529747748503629"", "Loadouts": "", "Locked": true, "Masterwork Tier": 5, "Melee": 0, "Melee (Base)": 0, "Name": "War Mantis", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Rare", "Season": 4, "Seasonal Mod": "", "Source": "campaign", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 51, "Total (Base)": 51, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 10, "Weapons (Base)": 10, "Year": 2, }, { "Archetype": undefined, "Class": 18, "Class (Base)": 18, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 187431790, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529748781353698"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 7, "Melee (Base)": 7, "Name": "Tusked Allegiance Gauntlets", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 16, "Seasonal Mod": "", "Source": "psiops", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 15, "Class (Base)": 15, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 849255710, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529748781353959"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Tusked Allegiance Hood", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Weapons Mod*", "Thy Fearful Symmetry*", ], "Power": 10, "Rarity": "Legendary", "Season": 16, "Seasonal Mod": "", "Source": "psiops", "Super": 21, "Super (Base)": 21, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 74, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 12, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 9, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 20, "Hash": 2203146422, "Health": 4, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529748845845050"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 18, "Melee (Base)": 6, "Name": "Assassin's Cowl", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Vanishing Execution*", "Melee Mod*", "Hands-On*", ], "Power": 10, "Rarity": "Exotic", "Season": 8, "Seasonal Mod": "artifice", "Source": "", "Super": 8, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 85, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 22, "Weapons (Base)": 20, "Year": 3, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 1566699968, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529748885715674"", "Loadouts": "", "Locked": true, "Masterwork Tier": 5, "Melee": 8, "Melee (Base)": 8, "Name": "Resonant Fury Strides", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 16, "Seasonal Mod": "vowofthedisciple", "Source": "vowofthedisciple", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 24, "Weapons (Base)": 24, "Year": 5, }, { "Archetype": undefined, "Class": 22, "Class (Base)": 22, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 803939997, "Health": 8, "Health (Base)": 8, "Holofoil": false, "Id": ""6917529748959843620"", "Loadouts": "", "Locked": true, "Masterwork Tier": 5, "Melee": 0, "Melee (Base)": 0, "Name": "War Mantis", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Rare", "Season": 4, "Seasonal Mod": "", "Source": "campaign", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 48, "Total (Base)": 48, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 18, "Weapons (Base)": 18, "Year": 2, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 22, "Hash": 3651424085, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529749719173607"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 8, "Melee (Base)": 8, "Name": "Iron Forerunner Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Iron Lord's Pride*", ], "Power": 10, "Rarity": "Legendary", "Season": 15, "Seasonal Mod": "", "Source": "ironbanner", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 11, "Weapons (Base)": 11, "Year": 4, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 8, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": true, "Event": "", "Grenade": 7, "Grenade (Base)": 7, "Hash": 2773056939, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529776970746801"", "Loadouts": "", "Locked": true, "Masterwork Tier": 5, "Melee": 16, "Melee (Base)": 16, "Name": "Graviton Forfeit", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Vanishing Shadow*", "Weapons Mod*", "Harmonic Siphon*", "Harmonic Siphon*", ], "Power": 10, "Rarity": "Exotic", "Season": 2, "Seasonal Mod": "artifice", "Source": "", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 75, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 35, "Weapons (Base)": 25, "Year": 1, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 8, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 12, "Hash": 3380315063, "Health": 14, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529776994950637"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Strides of Ascendancy", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Minor Weapons Mod*", "Innervation*", "Arc Weapon Surge*", "Stacks on Stacks*", "Enhanced Relay Defender*", "Molten Bronze*", "Iron Pledge Ornament*", ], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "gardenofsalvation", "Source": "gardenofsalvation", "Super": 22, "Super (Base)": 20, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 83, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 19, "Weapons (Base)": 12, "Year": 3, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 210208587, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529777047124599"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 6, "Melee (Base)": 6, "Name": "Vest of Transcendence", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Enhanced Relay Defender*", ], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "gardenofsalvation", "Source": "gardenofsalvation", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 10, "Weapons (Base)": 10, "Year": 3, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 9, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 23, "Grenade (Base)": 21, "Hash": 1649346047, "Health": 8, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529786008753033"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Resonant Fury Mask", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 16, "Seasonal Mod": "vowofthedisciple", "Source": "vowofthedisciple", "Super": 12, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 78, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 20, "Weapons (Base)": 18, "Year": 5, }, { "Archetype": undefined, "Class": 22, "Class (Base)": 20, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 17, "Grenade (Base)": 15, "Hash": 193869523, "Health": 8, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529788363061838"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Orpheus Rig", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Uncanny Arrows*", "Weapons Mod*", "Void Weapon Surge*", "Stacks on Stacks*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 14, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 86, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 21, "Weapons (Base)": 9, "Year": 1, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 22, "Hash": 2489136103, "Health": 20, "Health (Base)": 20, "Holofoil": false, "Id": ""6917529789867917589"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Eidolon Pursuant Handguards", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 17, "Seasonal Mod": "", "Source": "haunted", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 10, "Weapons (Base)": 10, "Year": 5, }, { "Archetype": undefined, "Class": 4, "Class (Base)": 2, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 25, "Grenade (Base)": 23, "Hash": 1285042454, "Health": 25, "Health (Base)": 23, "Holofoil": false, "Id": ""6917529792300964891"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 9, "Melee (Base)": 7, "Name": "Eidolon Pursuant Mask", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Minor Weapons Mod*", "Heavy Ammo Finder*", "Heavy Ammo Finder*", "Void Siphon*", ], "Power": 10, "Rarity": "Legendary", "Season": 17, "Seasonal Mod": "", "Source": "haunted", "Super": 4, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 82, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 15, "Weapons (Base)": 8, "Year": 5, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 10, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 4, "Grenade (Base)": 2, "Hash": 1241941801, "Health": 22, "Health (Base)": 20, "Holofoil": false, "Id": ""6917529792311704013"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 25, "Melee (Base)": 23, "Name": "Eidolon Pursuant Legguards", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 17, "Seasonal Mod": "", "Source": "haunted", "Super": 9, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 76, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 4, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 21, "Grenade (Base)": 21, "Hash": 1703551922, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529805341928802"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Blight Ranger", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Voltaic Mirror*", ], "Power": 10, "Rarity": "Exotic", "Season": 16, "Seasonal Mod": "artifice", "Source": "", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 10, "Weapons (Base)": 10, "Year": 5, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 17, "Grenade (Base)": 17, "Hash": 609852545, "Health": 11, "Health (Base)": 11, "Holofoil": false, "Id": ""6917529842744285130"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 6, "Melee (Base)": 6, "Name": "Fr0st-EE5", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid Cooldown*", ], "Power": 10, "Rarity": "Exotic", "Season": 3, "Seasonal Mod": "artifice", "Source": "", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 12, "Weapons (Base)": 12, "Year": 1, }, { "Archetype": undefined, "Class": 4, "Class (Base)": 2, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 24, "Grenade (Base)": 22, "Hash": 1053737370, "Health": 18, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529842752289413"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Shinobu's Vow", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "New Tricks*", "Weapons Mod*", "Harmonic Loader*", "Impact Induction*", "Grenade Kickstart*", ], "Power": 10, "Rarity": "Exotic", "Season": 2, "Seasonal Mod": "artifice", "Source": "", "Super": 12, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 90, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 28, "Weapons (Base)": 16, "Year": 1, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 6, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 29, "Grenade (Base)": 17, "Hash": 1095145125, "Health": 8, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529843194816779"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 16, "Melee (Base)": 14, "Name": "Ketchkiller's Mask", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Grenade Mod*", "Harmonic Siphon*", "Harmonic Targeting*", "Heavy Ammo Finder*", ], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "", "Source": "plunder", "Super": 4, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 89, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 24, "Weapons (Base)": 22, "Year": 5, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 8, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 803939997, "Health": 34, "Health (Base)": 34, "Holofoil": false, "Id": ""6917529847378163799"", "Loadouts": "", "Locked": true, "Masterwork Tier": 5, "Melee": 0, "Melee (Base)": 0, "Name": "War Mantis", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Rare", "Season": 4, "Seasonal Mod": "", "Source": "campaign", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 50, "Total (Base)": 50, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 8, "Weapons (Base)": 8, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 1619425569, "Health": 9, "Health (Base)": 9, "Holofoil": false, "Id": ""6917529847415827116"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Mask of Bakris", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Light Shift*", ], "Power": 10, "Rarity": "Exotic", "Season": 12, "Seasonal Mod": "artifice", "Source": "", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 20, "Weapons (Base)": 20, "Year": 4, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 193869520, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529853768061464"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 10, "Melee (Base)": 10, "Name": "St0mp-EE5", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Hydraulic Boosters*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 16, "Weapons (Base)": 16, "Year": 1, }, { "Archetype": undefined, "Class": 9, "Class (Base)": 9, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 2773056939, "Health": 15, "Health (Base)": 15, "Holofoil": false, "Id": ""6917529853774587295"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 8, "Melee (Base)": 8, "Name": "Graviton Forfeit", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Vanishing Shadow*", ], "Power": 10, "Rarity": "Exotic", "Season": 2, "Seasonal Mod": "artifice", "Source": "", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 9, "Weapons (Base)": 9, "Year": 1, }, { "Archetype": undefined, "Class": 4, "Class (Base)": 2, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": true, "Event": "", "Grenade": 24, "Grenade (Base)": 22, "Hash": 4116381015, "Health": 32, "Health (Base)": 30, "Holofoil": false, "Id": ""6917529858669298225"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 8, "Melee (Base)": 6, "Name": "Warmind's Avatar Vest", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Weapons Mod*", "Concussive Dampener*", "Health Font*", ], "Power": 10, "Rarity": "Legendary", "Season": 19, "Seasonal Mod": "", "Source": "rasputin", "Super": 8, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 90, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 14, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 10, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 21, "Grenade (Base)": 19, "Hash": 648266032, "Health": 24, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529861829179845"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 8, "Melee (Base)": 6, "Name": "Warmind's Avatar Mask", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Minor Weapons Mod*", "Heavy Ammo Finder*", "Heavy Ammo Finder*", "Void Siphon*", ], "Power": 10, "Rarity": "Legendary", "Season": 19, "Seasonal Mod": "", "Source": "rasputin", "Super": 10, "Super (Base)": 8, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 84, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 9, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 1059446290, "Health": 30, "Health (Base)": 30, "Holofoil": false, "Id": ""6917529865854205664"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 22, "Melee (Base)": 22, "Name": "Warmind's Avatar Helm", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Weapons Mod*", ], "Power": 10, "Rarity": "Legendary", "Season": 19, "Seasonal Mod": "", "Source": "rasputin", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 78, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 12, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 2710316218, "Health": 30, "Health (Base)": 30, "Holofoil": false, "Id": ""6917529865884358862"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 9, "Melee (Base)": 9, "Name": "Warmind's Avatar Robes", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 19, "Seasonal Mod": "", "Source": "rasputin", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": true, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 4270910189, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529865884360943"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 10, "Melee (Base)": 10, "Name": "Warmind's Avatar Legplates", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 19, "Seasonal Mod": "", "Source": "rasputin", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 5, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": true, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 2471829328, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529865894105496"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 10, "Melee (Base)": 10, "Name": "Warmind's Avatar Pants", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Weapons Mod*", ], "Power": 10, "Rarity": "Legendary", "Season": 19, "Seasonal Mod": "", "Source": "rasputin", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 74, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 20, "Weapons (Base)": 10, "Year": 5, }, { "Archetype": undefined, "Class": 22, "Class (Base)": 22, "Energy Capacity": 6, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 327547301, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529866397668943"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Holdfast Gloves", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 3, }, { "Archetype": undefined, "Class": 22, "Class (Base)": 22, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 22, "Hash": 2030782835, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529866397671650"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Ketchkiller's Boots", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "", "Source": "plunder", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 68, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 5, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 23, "Grenade (Base)": 23, "Hash": 1640403802, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529866409984495"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Ketchkiller's Greaves", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "", "Source": "plunder", "Super": 8, "Super (Base)": 8, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 5, }, { "Archetype": undefined, "Class": 20, "Class (Base)": 20, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 2945464224, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529866420984221"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 6, "Melee (Base)": 6, "Name": "Ketchkiller's Gauntlets", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "", "Source": "plunder", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 5, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 20, "Grenade (Base)": 20, "Hash": 3833868247, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529866420985117"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Ketchkiller's Robes", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "", "Source": "plunder", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 68, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 5, }, { "Archetype": undefined, "Class": 22, "Class (Base)": 22, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 738153648, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529866420989387"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 6, "Melee (Base)": 6, "Name": "Ketchkiller's Hood", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "", "Source": "plunder", "Super": 11, "Super (Base)": 11, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 2, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 26, "Class (Base)": 26, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 11, "Grenade (Base)": 11, "Hash": 130618344, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529866420989434"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 10, "Melee (Base)": 10, "Name": "Ketchkiller's Plate", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "", "Source": "plunder", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 6, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 287888126, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529866429103379"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Holdfast Gauntlets", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 3, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 6, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 1131831128, "Health": 20, "Health (Base)": 20, "Holofoil": false, "Id": ""6917529866429106816"", "Loadouts": "", "Locked": true, "Masterwork Tier": 6, "Melee": 12, "Melee (Base)": 12, "Name": "Holdfast Greaves", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "seasonpass", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 7, "Weapons (Base)": 7, "Year": 3, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 1474735276, "Health": 15, "Health (Base)": 15, "Holofoil": false, "Id": ""6917529867279308417"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 9, "Melee (Base)": 9, "Name": "Gwisin Vest", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Roving Assassin*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 11, "Super (Base)": 11, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 19, "Weapons (Base)": 19, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 7, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 328467570, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529867286952481"", "Loadouts": "", "Locked": true, "Masterwork Tier": 7, "Melee": 14, "Melee (Base)": 14, "Name": "Dreambane Strides", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "nightmare", "Source": "moon", "Super": 16, "Super (Base)": 16, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 9, "Weapons (Base)": 9, "Year": 3, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 2813078109, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529867286958952"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 15, "Melee (Base)": 15, "Name": "Dreambane Helm", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "nightmare", "Source": "moon", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 23, "Weapons (Base)": 23, "Year": 3, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 1, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 2975563522, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529867286959913"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 0, "Melee (Base)": 0, "Name": "Dreambane Bond", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "nightmare", "Source": "moon", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Warlock Bond", "Weapons": 0, "Weapons (Base)": 0, "Year": 3, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 7, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 7, "Grenade (Base)": 7, "Hash": 1721938300, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529867293410580"", "Loadouts": "", "Locked": true, "Masterwork Tier": 7, "Melee": 2, "Melee (Base)": 2, "Name": "Dreambane Hood", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "nightmare", "Source": "moon", "Super": 24, "Super (Base)": 24, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 7, "Weapons (Base)": 7, "Year": 3, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 169689932, "Health": 11, "Health (Base)": 11, "Holofoil": false, "Id": ""6917529867681970027"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 6, "Melee (Base)": 6, "Name": "Darkhollow Chiton", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "kingsfall", "Source": "kingsfall", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 12, "Weapons (Base)": 12, "Year": 5, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 2169905051, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529868510820734"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 6, "Melee (Base)": 6, "Name": "Renewal Grasps", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Depths of Duskfield*", ], "Power": 10, "Rarity": "Exotic", "Season": 16, "Seasonal Mod": "artifice", "Source": "", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 46, "Total (Base)": 46, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 5, }, { "Archetype": undefined, "Class": 19, "Class (Base)": 19, "Energy Capacity": 2, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 756445218, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529879099834499"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 10, "Melee (Base)": 10, "Name": "Legacy's Oath Boots", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 13, "Super (Base)": 13, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 4, }, { "Archetype": undefined, "Class": 14, "Class (Base)": 14, "Energy Capacity": 2, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 1462908657, "Health": 9, "Health (Base)": 9, "Holofoil": false, "Id": ""6917529879108995280"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 10, "Melee (Base)": 10, "Name": "Legacy's Oath Cowl", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 9, "Weapons (Base)": 9, "Year": 4, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 4, "Equippable": "Warlock", "Equipped": true, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 3975122240, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529879109000062"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 10, "Melee (Base)": 10, "Name": "Legacy's Oath Robes", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 10, "Weapons (Base)": 10, "Year": 4, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 8, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 26, "Grenade (Base)": 26, "Hash": 3608027009, "Health": 21, "Health (Base)": 21, "Holofoil": false, "Id": ""6917529890672482544"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 6, "Melee (Base)": 6, "Name": "Grips of Trepidation", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "rootofnightmares", "Source": "rootofnightmares", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 6, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 11, "Grenade (Base)": 11, "Hash": 2390471904, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529892161400589"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 12, "Melee (Base)": 12, "Name": "Speedloader Slacks", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Tight Fit*", ], "Power": 10, "Rarity": "Exotic", "Season": 20, "Seasonal Mod": "artifice", "Source": "", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 16, "Weapons (Base)": 16, "Year": 6, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 2343515647, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529895497274824"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 10, "Melee (Base)": 10, "Name": "Legacy's Oath Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 13, "Super (Base)": 13, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 12, "Weapons (Base)": 12, "Year": 4, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 893751566, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529895511875548"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 7, "Melee (Base)": 7, "Name": "Legacy's Oath Mask", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 10, "Weapons (Base)": 10, "Year": 4, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 20, "Grenade (Base)": 20, "Hash": 1219761634, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529895516437129"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 8, "Melee (Base)": 8, "Name": "The Bombardiers", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Parting Gift*", ], "Power": 10, "Rarity": "Exotic", "Season": 9, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 11, "Weapons (Base)": 11, "Year": 3, }, { "Archetype": undefined, "Class": 4, "Class (Base)": 2, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 30, "Grenade (Base)": 28, "Hash": 3995603239, "Health": 24, "Health (Base)": 17, "Holofoil": false, "Id": ""6917529897731172007"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Techeun's Regalia Vest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Minor Health Mod*", ], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "", "Source": "seasonpass", "Super": 4, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 80, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 14, "Weapons (Base)": 12, "Year": 6, }, { "Archetype": undefined, "Class": 15, "Class (Base)": 13, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 23, "Grenade (Base)": 21, "Hash": 2766109874, "Health": 6, "Health (Base)": 4, "Holofoil": false, "Id": ""6917529898517426551"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "The Dragon's Shadow", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Wraithmetal Mail*", "Weapons Mod*", "Charged Up*", "Arc Resistance*", "Arc Resistance*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 10, "Super (Base)": 8, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 85, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 27, "Weapons (Base)": 15, "Year": 1, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 1264765761, "Health": 19, "Health (Base)": 19, "Holofoil": false, "Id": ""6917529899245353287"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 12, "Melee (Base)": 12, "Name": "Legacy's Oath Strides", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 11, "Weapons (Base)": 11, "Year": 4, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 20, "Grenade (Base)": 20, "Hash": 807905267, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529906811146151"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 10, "Melee (Base)": 10, "Name": "Boots of Trepidation", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "rootofnightmares", "Source": "rootofnightmares", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 9, "Weapons (Base)": 9, "Year": 6, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 3889633083, "Health": 24, "Health (Base)": 24, "Holofoil": false, "Id": ""6917529906811149835"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 8, "Melee (Base)": 8, "Name": "Grips of the Great Hunt", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 4, "Seasonal Mod": "lastwish", "Source": "lastwish", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 2, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 2437510452, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529908830405424"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 9, "Melee (Base)": 9, "Name": "Darkhollow Grasps", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "kingsfall", "Source": "kingsfall", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 5, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 3810243376, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529910393119454"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 14, "Melee (Base)": 14, "Name": "Mask of Trepidation", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "rootofnightmares", "Source": "rootofnightmares", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 8, "Weapons (Base)": 8, "Year": 6, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 3608027009, "Health": 26, "Health (Base)": 26, "Holofoil": false, "Id": ""6917529910420236276"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 7, "Melee (Base)": 7, "Name": "Grips of Trepidation", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "rootofnightmares", "Source": "rootofnightmares", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 6, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 621315878, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529910438741889"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 0, "Melee (Base)": 0, "Name": "Cloak of Trepidation", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Precise Jolts*", ], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "rootofnightmares", "Source": "rootofnightmares", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 6, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 2868448385, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529920101928521"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 0, "Melee (Base)": 0, "Name": "Darkhollow Mantle", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Mortal Medicine*", ], "Power": 10, "Rarity": "Legendary", "Season": 18, "Seasonal Mod": "kingsfall", "Source": "kingsfall", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 5, }, { "Archetype": undefined, "Class": 21, "Class (Base)": 21, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 751162931, "Health": 9, "Health (Base)": 9, "Holofoil": false, "Id": ""6917529920176005262"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 16, "Melee (Base)": 16, "Name": "Legacy's Oath Plate", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 12, "Seasonal Mod": "deepstonecrypt", "Source": "deepstonecrypt", "Super": 16, "Super (Base)": 16, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 4, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 8, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 13, "Grenade (Base)": 13, "Hash": 3423279826, "Health": 13, "Health (Base)": 13, "Holofoil": false, "Id": ""6917529920253221923"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Mask of the Great Hunt", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 4, "Seasonal Mod": "lastwish", "Source": "lastwish", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 9, "Weapons (Base)": 9, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 24, "Grenade (Base)": 24, "Hash": 3889633083, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529920269540598"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 6, "Melee (Base)": 6, "Name": "Grips of the Great Hunt", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 4, "Seasonal Mod": "lastwish", "Source": "lastwish", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 17, "Weapons (Base)": 17, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 193869522, "Health": 18, "Health (Base)": 18, "Holofoil": false, "Id": ""6917529920431787002"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 2, "Melee (Base)": 2, "Name": "Lucky Pants", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Illegally Modded Holster*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 15, "Weapons (Base)": 15, "Year": 1, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 1001356380, "Health": 20, "Health (Base)": 20, "Holofoil": false, "Id": ""6917529920440858714"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Star-Eater Scales", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Feast of Light*", ], "Power": 10, "Rarity": "Exotic", "Season": 14, "Seasonal Mod": "artifice", "Source": "", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 12, "Weapons (Base)": 12, "Year": 4, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 26, "Grenade (Base)": 26, "Hash": 896224899, "Health": 20, "Health (Base)": 20, "Holofoil": false, "Id": ""6917529920456261793"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 6, "Melee (Base)": 6, "Name": "Foetracer", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Relentless Tracker*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 68, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 4, "Weapons (Base)": 4, "Year": 1, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 3352415987, "Health": 13, "Health (Base)": 13, "Holofoil": false, "Id": ""6917529920466909261"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 6, "Melee (Base)": 6, "Name": "Iron Companion Mask", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Iron Lord's Pride*", ], "Power": 10, "Rarity": "Legendary", "Season": 19, "Seasonal Mod": "", "Source": "ironbanner", "Super": 17, "Super (Base)": 17, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 12, "Weapons (Base)": 12, "Year": 5, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 23, "Grenade (Base)": 13, "Hash": 2007908580, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529920483328868"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 6, "Melee (Base)": 6, "Name": "NPA "Weir-Walker" Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Grenade Mod*", ], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 73, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 20, "Weapons (Base)": 20, "Year": 6, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 4124357755, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529920506932814"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 0, "Melee (Base)": 0, "Name": "Resonant Fury Cloak", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Umbral Sharpening*", ], "Power": 10, "Rarity": "Legendary", "Season": 16, "Seasonal Mod": "vowofthedisciple", "Source": "vowofthedisciple", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 5, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 2757274117, "Health": 25, "Health (Base)": 25, "Holofoil": false, "Id": ""6917529920511542670"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 12, "Melee (Base)": 12, "Name": "Khepri's Sting", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Touch of Venom*", ], "Power": 10, "Rarity": "Exotic", "Season": 7, "Seasonal Mod": "artifice", "Source": "", "Super": 17, "Super (Base)": 17, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 3, "Weapons (Base)": 3, "Year": 2, }, { "Archetype": undefined, "Class": 15, "Class (Base)": 15, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 3487540074, "Health": 15, "Health (Base)": 15, "Holofoil": false, "Id": ""6917529920511551854"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 14, "Melee (Base)": 14, "Name": "Resonant Fury Vest", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 16, "Seasonal Mod": "vowofthedisciple", "Source": "vowofthedisciple", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "Solstice", "Grenade": 30, "Grenade (Base)": 30, "Hash": 476513004, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529920950746976"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Sunlit Grips", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "events", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 6, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 19, "Grenade (Base)": 19, "Hash": 1935198785, "Health": 17, "Health (Base)": 17, "Holofoil": false, "Id": ""6917529923581822005"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 10, "Melee (Base)": 10, "Name": "Omnioculus", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Beyond The Veil*", ], "Power": 10, "Rarity": "Exotic", "Season": 13, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 10, "Weapons (Base)": 10, "Year": 4, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 1702288800, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529923896533912"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 16, "Melee (Base)": 16, "Name": "Radiant Dance Machines", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "The Dance*", ], "Power": 10, "Rarity": "Exotic", "Season": 15, "Seasonal Mod": "artifice", "Source": "", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 25, "Weapons (Base)": 25, "Year": 4, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 23, "Grenade (Base)": 23, "Hash": 3453042252, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529924519221493"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 6, "Melee (Base)": 6, "Name": "Caliban's Hand", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Roast 'Em*", ], "Power": 10, "Rarity": "Exotic", "Season": 17, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 16, "Weapons (Base)": 16, "Year": 5, }, { "Archetype": undefined, "Class": 14, "Class (Base)": 14, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 3434445392, "Health": 8, "Health (Base)": 8, "Holofoil": false, "Id": ""6917529925596667875"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 6, "Melee (Base)": 6, "Name": "Dreambane Vest", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "nightmare", "Source": "moon", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 9, "Weapons (Base)": 9, "Year": 3, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 192896783, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529925605822250"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 10, "Melee (Base)": 10, "Name": "Cyrtarachne's Facade", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Acrobat's Focus*", "Ashes to Assets*", ], "Power": 10, "Rarity": "Exotic", "Season": 20, "Seasonal Mod": "artifice", "Source": "", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 10, "Weapons (Base)": 10, "Year": 6, }, { "Archetype": undefined, "Class": 16, "Class (Base)": 16, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 20, "Grenade (Base)": 20, "Hash": 1688602431, "Health": 17, "Health (Base)": 17, "Holofoil": false, "Id": ""6917529928216817282"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Sealed Ahamkara Grasps", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Nightmare Fuel*", ], "Power": 10, "Rarity": "Exotic", "Season": 3, "Seasonal Mod": "artifice", "Source": "", "Super": 11, "Super (Base)": 11, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 69, "Total (Base)": 69, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 3, "Weapons (Base)": 3, "Year": 1, }, { "Archetype": undefined, "Class": 13, "Class (Base)": 13, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 3942036043, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529928224293677"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 6, "Melee (Base)": 6, "Name": "Aeon Swift", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Sect of Force*", ], "Power": 10, "Rarity": "Exotic", "Season": 2, "Seasonal Mod": "artifice", "Source": "", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 11, "Weapons (Base)": 11, "Year": 1, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 11, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 13, "Grenade (Base)": 13, "Hash": 978537162, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529928256694472"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 15, "Melee (Base)": 15, "Name": "Ophidia Spathe", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Scissor Fingers*", ], "Power": 10, "Rarity": "Exotic", "Season": 3, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 9, "Weapons (Base)": 9, "Year": 1, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 2787963735, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529932883394154"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 13, "Melee (Base)": 13, "Name": "Vest of Trepidation", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "rootofnightmares", "Source": "rootofnightmares", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 16, "Weapons (Base)": 16, "Year": 6, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 11, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 2787963735, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529932892230949"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 18, "Melee (Base)": 18, "Name": "Vest of Trepidation", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "rootofnightmares", "Source": "rootofnightmares", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 9, "Weapons (Base)": 9, "Year": 6, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 2386078741, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529933746196429"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 0, "Melee (Base)": 0, "Name": "NPA "Weir-Walker" Mark", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Titan Mark", "Weapons": 0, "Weapons (Base)": 0, "Year": 6, }, { "Archetype": undefined, "Class": 18, "Class (Base)": 18, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": true, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 3607600504, "Health": 13, "Health (Base)": 13, "Holofoil": false, "Id": ""6917529933746199568"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 6, "Melee (Base)": 6, "Name": "NPA "Weir-Walker" Plate", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 16, "Super (Base)": 16, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 6, }, { "Archetype": undefined, "Class": 23, "Class (Base)": 23, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 19, "Grenade (Base)": 19, "Hash": 748663978, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529933753872569"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 6, "Melee (Base)": 6, "Name": "NPA "Weir-Walker" Legguards", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 8, "Super (Base)": 8, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 431605328, "Health": 13, "Health (Base)": 13, "Holofoil": false, "Id": ""6917529933753880108"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 6, "Melee (Base)": 6, "Name": "NPA "Weir-Walker" Gauntlets", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 11, "Energy Capacity": 4, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 6, "Grenade (Base)": 6, "Hash": 2797536515, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529933761627192"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 11, "Melee (Base)": 11, "Name": "NPA "Weir-Walker" Pants", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 14, "Weapons (Base)": 14, "Year": 6, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 11, "Energy Capacity": 2, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 1685051367, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529933770257039"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 10, "Melee (Base)": 10, "Name": "NPA "Weir-Walker" Robes", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 61, "Total (Base)": 61, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 14, "Weapons (Base)": 14, "Year": 6, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 1, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 1063769568, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529933770257085"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "NPA "Weir-Walker" Visor", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 2, "Equippable": "Warlock", "Equipped": true, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 2400549110, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529933770259631"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 0, "Melee (Base)": 0, "Name": "NPA "Weir-Walker" Bond", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Warlock Bond", "Weapons": 0, "Weapons (Base)": 0, "Year": 6, }, { "Archetype": undefined, "Class": 16, "Class (Base)": 16, "Energy Capacity": 1, "Equippable": "Warlock", "Equipped": true, "Event": "", "Grenade": 11, "Grenade (Base)": 11, "Hash": 2966283697, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529933770262243"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 10, "Melee (Base)": 10, "Name": "NPA "Weir-Walker" Gloves", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 21, "Seasonal Mod": "", "Source": "seasonpass", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 7, "Grenade (Base)": 7, "Hash": 1734144409, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529940127784979"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 14, "Melee (Base)": 14, "Name": "Mechaneer's Tricksleeves", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Spring-Loaded Mounting*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 68, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 17, "Weapons (Base)": 17, "Year": 1, }, { "Archetype": undefined, "Class": 17, "Class (Base)": 17, "Energy Capacity": 7, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 3714937821, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917529942744869328"", "Loadouts": "", "Locked": true, "Masterwork Tier": 7, "Melee": 8, "Melee (Base)": 8, "Name": "Relentless Harness", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Arc Resistance*", "Arc Resistance*", "Arc Resistance*", "Benevolent Overflow*", ], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 11, "Weapons (Base)": 11, "Year": 6, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 23, "Grenade (Base)": 23, "Hash": 4165919945, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917529950723165306"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 7, "Melee (Base)": 7, "Name": "Liar's Handshake", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Cross Counter*", ], "Power": 10, "Rarity": "Exotic", "Season": 6, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 9, "Weapons (Base)": 9, "Year": 2, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 1703551922, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529950761756509"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 15, "Melee (Base)": 15, "Name": "Blight Ranger", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Voltaic Mirror*", ], "Power": 10, "Rarity": "Exotic", "Season": 16, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 6, "Weapons (Base)": 6, "Year": 5, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 859929450, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529953233530483"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Unyielding Casque", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 12, "Weapons (Base)": 12, "Year": 6, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": true, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 807905267, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529953268330044"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Boots of Trepidation", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Weapons Mod*", ], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "rootofnightmares", "Source": "rootofnightmares", "Super": 19, "Super (Base)": 19, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 77, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 26, "Weapons (Base)": 16, "Year": 6, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 3832002135, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529953283294875"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 7, "Melee (Base)": 7, "Name": "Veiled Tithes Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 26, "Weapons (Base)": 26, "Year": 6, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 1345569657, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529955056529345"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 10, "Melee (Base)": 10, "Name": "Veiled Tithes Strides", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 20, "Weapons (Base)": 20, "Year": 6, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 2415768376, "Health": 17, "Health (Base)": 17, "Holofoil": false, "Id": ""6917529955071195828"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 14, "Melee (Base)": 14, "Name": "Athrys's Embrace", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Skittering Stinger*", ], "Power": 10, "Rarity": "Exotic", "Season": 12, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 10, "Weapons (Base)": 10, "Year": 4, }, { "Archetype": undefined, "Class": 4, "Class (Base)": 2, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 26, "Grenade (Base)": 24, "Hash": 2672840191, "Health": 24, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529955258648871"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 4, "Melee (Base)": 2, "Name": "Lost Pacific Strides", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Health Mod*", "Recuperation*", "Absolution*", ], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "", "Super": 9, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 87, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 20, "Weapons (Base)": 18, "Year": 1, }, { "Archetype": undefined, "Class": 4, "Class (Base)": 2, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 14, "Hash": 1833326563, "Health": 24, "Health (Base)": 22, "Holofoil": false, "Id": ""6917529955258650898"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 19, "Melee (Base)": 17, "Name": "Lost Pacific Vest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "", "Super": 4, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 79, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 12, "Weapons (Base)": 10, "Year": 1, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 3737404623, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529955665212037"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 0, "Melee (Base)": 0, "Name": "Wolfswood Cloak", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Iron Lord's Pride*", "Mantle of Efrideet*", ], "Power": 10, "Rarity": "Legendary", "Season": 19, "Seasonal Mod": "", "Source": "ironbanner", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 5, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 2931145020, "Health": 23, "Health (Base)": 23, "Holofoil": false, "Id": ""6917529955674662794"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 17, "Melee (Base)": 17, "Name": "Iron Companion Boots", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Iron Lord's Pride*", ], "Power": 10, "Rarity": "Legendary", "Season": 19, "Seasonal Mod": "", "Source": "ironbanner", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 11, "Grenade (Base)": 11, "Hash": 691578979, "Health": 11, "Health (Base)": 11, "Holofoil": false, "Id": ""6917529955681980807"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Shards of Galanor", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Sharp Edges*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 19, "Super (Base)": 19, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 16, "Weapons (Base)": 16, "Year": 2, }, { "Archetype": undefined, "Class": 18, "Class (Base)": 16, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 20, "Hash": 4067720865, "Health": 8, "Health (Base)": 6, "Holofoil": false, "Id": ""6917529955681981595"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 8, "Melee (Base)": 6, "Name": "Veiled Tithes Vest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 8, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 78, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 14, "Weapons (Base)": 12, "Year": 6, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 10, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 17, "Grenade (Base)": 12, "Hash": 461841403, "Health": 9, "Health (Base)": 7, "Holofoil": false, "Id": ""6917529955698399474"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 9, "Melee (Base)": 7, "Name": "Gyrfalcon's Hauberk", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "See Me, Feel Me*", "Minor Weapons Mod*", "Arc Resistance*", "Arc Resistance*", "Charged Up*", "Grenade Forged*", "Magical Transformation*", ], "Power": 10, "Rarity": "Exotic", "Season": 18, "Seasonal Mod": "artifice", "Source": "", "Super": 16, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 86, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 23, "Weapons (Base)": 16, "Year": 5, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 844097260, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529960090661259"", "Loadouts": "", "Locked": true, "Masterwork Tier": 2, "Melee": 0, "Melee (Base)": 0, "Name": "Reverie Dawn Cloak", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Riven's Curse*", ], "Power": 10, "Rarity": "Legendary", "Season": 4, "Seasonal Mod": "", "Source": "dreaming", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 2, }, { "Archetype": undefined, "Class": 16, "Class (Base)": 16, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 24, "Grenade (Base)": 24, "Hash": 121305948, "Health": 8, "Health (Base)": 8, "Holofoil": false, "Id": ""6917529993269268392"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 7, "Melee (Base)": 7, "Name": "Geomag Stabilizers", "New Gear": true, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Close Enough*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 10, "Weapons (Base)": 10, "Year": 2, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 11, "Grenade (Base)": 11, "Hash": 2339155434, "Health": 13, "Health (Base)": 13, "Holofoil": false, "Id": ""6917529993271329839"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Tesseract Trace IV", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "engram", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 53, "Total (Base)": 53, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 7, "Weapons (Base)": 7, "Year": 3, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 2432846050, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917529993320757034"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 7, "Melee (Base)": 7, "Name": "Heiro Camo", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "engram", "Super": 17, "Super (Base)": 17, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 51, "Total (Base)": 51, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 7, "Weapons (Base)": 7, "Year": 1, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": true, "Event": "Guardian Games", "Grenade": 0, "Grenade (Base)": 0, "Hash": 3720087872, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917529993587809987"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 0, "Melee (Base)": 0, "Name": "Allstar Mark", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 23, "Seasonal Mod": "", "Source": "events", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Titan Mark", "Weapons": 0, "Weapons (Base)": 0, "Year": 6, }, { "Archetype": undefined, "Class": 13, "Class (Base)": 13, "Energy Capacity": 1, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 3381022969, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917529993593413954"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 20, "Melee (Base)": 20, "Name": "Crown of Tempests", "New Gear": true, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Conduction Tines*", ], "Power": 10, "Rarity": "Exotic", "Season": 1, "Seasonal Mod": "artifice", "Source": "", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 70, "Total (Base)": 70, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 13, "Weapons (Base)": 13, "Year": 1, }, { "Archetype": undefined, "Class": 13, "Class (Base)": 13, "Energy Capacity": 4, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 22, "Hash": 901647822, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917529993595353091"", "Loadouts": "", "Locked": false, "Masterwork Tier": 4, "Melee": 8, "Melee (Base)": 8, "Name": "Heiro Camo", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "engram", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 59, "Total (Base)": 59, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 1, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 22, "Hash": 3013248110, "Health": 30, "Health (Base)": 30, "Holofoil": false, "Id": ""6917530009468570468"", "Loadouts": "", "Locked": false, "Masterwork Tier": 4, "Melee": 6, "Melee (Base)": 6, "Name": "Wyrmguard Tunic", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 23, "Seasonal Mod": "", "Source": "rivenslair", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 68, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 6, }, { "Archetype": undefined, "Class": 32, "Class (Base)": 30, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 17, "Grenade (Base)": 15, "Hash": 2687273800, "Health": 4, "Health (Base)": 2, "Holofoil": false, "Id": ""6917530018729488429"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 8, "Melee (Base)": 6, "Name": "Substitutional Alloy Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "", "Source": "vexoffensive", "Super": 12, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 77, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 4, "Weapons (Base)": 2, "Year": 3, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 786695846, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530018768626410"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 0, "Melee (Base)": 0, "Name": "Veiled Tithes Mark", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Titan Mark", "Weapons": 0, "Weapons (Base)": 0, "Year": 6, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 2, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 20, "Grenade (Base)": 20, "Hash": 260708852, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917530018787453896"", "Loadouts": "", "Locked": false, "Masterwork Tier": 2, "Melee": 2, "Melee (Base)": 2, "Name": "Veiled Tithes Robes", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 68, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 15, "Grenade (Base)": 15, "Hash": 2598029856, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917530018792342807"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Techeun's Regalia Mask", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 20, "Seasonal Mod": "", "Source": "seasonpass", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 20, "Weapons (Base)": 20, "Year": 6, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 5, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 1004538825, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530018792343235"", "Loadouts": "", "Locked": false, "Masterwork Tier": 5, "Melee": 0, "Melee (Base)": 0, "Name": "Veiled Tithes Bond", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Warlock Bond", "Weapons": 0, "Weapons (Base)": 0, "Year": 6, }, { "Archetype": undefined, "Class": 22, "Class (Base)": 22, "Energy Capacity": 3, "Equippable": "Warlock", "Equipped": false, "Event": "", "Grenade": 22, "Grenade (Base)": 22, "Hash": 3663544278, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530018792344864"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 2, "Melee (Base)": 2, "Name": "Veiled Tithes Boots", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 68, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 20, "Class (Base)": 20, "Energy Capacity": 4, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 1322406401, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530018792345494"", "Loadouts": "", "Locked": false, "Masterwork Tier": 4, "Melee": 6, "Melee (Base)": 6, "Name": "Veiled Tithes Gauntlets", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 26, "Class (Base)": 26, "Energy Capacity": 3, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 11, "Grenade (Base)": 11, "Hash": 2953343703, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530018792346489"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 10, "Melee (Base)": 10, "Name": "Veiled Tithes Plate", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 6, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 1, "Equippable": "Titan", "Equipped": false, "Event": "", "Grenade": 23, "Grenade (Base)": 23, "Hash": 2817251955, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917530018792351089"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Veiled Tithes Greaves", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "seasonpass", "Super": 8, "Super (Base)": 8, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": true, "Event": "", "Grenade": 6, "Grenade (Base)": 6, "Hash": 3202902670, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530025496553992"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 12, "Melee (Base)": 12, "Name": "Hinterland Grips", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Weapons Mod*", ], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "", "Source": "", "Super": 16, "Super (Base)": 16, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 78, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 32, "Weapons (Base)": 22, "Year": 6, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 23, "Grenade (Base)": 23, "Hash": 2339694345, "Health": 9, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530028081424553"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 6, "Melee (Base)": 6, "Name": "Hinterland Cowl", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Artifice Armor*", "Radiant Light*", "Health Forged*", ], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 14, "Weapons (Base)": 14, "Year": 6, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 1512311134, "Health": 14, "Health (Base)": 14, "Holofoil": false, "Id": ""6917530028085921079"", "Loadouts": "", "Locked": false, "Masterwork Tier": 2, "Melee": 24, "Melee (Base)": 24, "Name": "Simulator Vest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "fwc", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 13, "Weapons (Base)": 13, "Year": 1, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 1958630864, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917530039520433034"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 7, "Melee (Base)": 7, "Name": "Untethered Edge Vest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 24, "Seasonal Mod": "", "Source": "echoes", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 61, "Total (Base)": 61, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 18, "Weapons (Base)": 18, "Year": 7, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 8, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 13, "Grenade (Base)": 13, "Hash": 859929450, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917530066328434863"", "Loadouts": "", "Locked": false, "Masterwork Tier": 2, "Melee": 7, "Melee (Base)": 7, "Name": "Unyielding Casque", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 56, "Total (Base)": 56, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 8, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 3714937821, "Health": 13, "Health (Base)": 13, "Holofoil": false, "Id": ""6917530066349857573"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 17, "Melee (Base)": 17, "Name": "Relentless Harness", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 14, "Super (Base)": 14, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 9, "Weapons (Base)": 9, "Year": 6, }, { "Archetype": undefined, "Class": 20, "Class (Base)": 20, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 175883909, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530066356832891"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Tireless Striders", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 8, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 13, "Grenade (Base)": 13, "Hash": 441033139, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917530066359307962"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 9, "Melee (Base)": 9, "Name": "Dogged Gage", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 12, "Weapons (Base)": 12, "Year": 6, }, { "Archetype": undefined, "Class": 13, "Class (Base)": 13, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 441033139, "Health": 11, "Health (Base)": 11, "Holofoil": false, "Id": ""6917530068050205274"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 10, "Melee (Base)": 10, "Name": "Dogged Gage", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 6, "Weapons (Base)": 6, "Year": 6, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 175883909, "Health": 22, "Health (Base)": 22, "Holofoil": false, "Id": ""6917530068056841079"", "Loadouts": "", "Locked": false, "Masterwork Tier": 2, "Melee": 2, "Melee (Base)": 2, "Name": "Tireless Striders", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 7, "Weapons (Base)": 7, "Year": 6, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 7, "Grenade (Base)": 7, "Hash": 745736759, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530068061592728"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 6, "Melee (Base)": 6, "Name": "Shadestalker Strides", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 25, "Seasonal Mod": "", "Source": "revenant", "Super": 13, "Super (Base)": 13, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 51, "Total (Base)": 51, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 12, "Weapons (Base)": 12, "Year": 7, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 1291141296, "Health": 17, "Health (Base)": 17, "Holofoil": false, "Id": ""6917530068064117666"", "Loadouts": "", "Locked": false, "Masterwork Tier": 4, "Melee": 2, "Melee (Base)": 2, "Name": "Red Moon Phantom Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 4, "Seasonal Mod": "", "Source": "engram", "Super": 22, "Super (Base)": 22, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 52, "Total (Base)": 52, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 441033139, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530068071206838"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 12, "Melee (Base)": 12, "Name": "Dogged Gage", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 26, "Weapons (Base)": 26, "Year": 6, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 1306415888, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530068071208425"", "Loadouts": "", "Locked": true, "Masterwork Tier": 3, "Melee": 0, "Melee (Base)": 0, "Name": "Shroud of Flies", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 6, }, { "Archetype": undefined, "Class": 8, "Class (Base)": 8, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 3967899461, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917530068071208693"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 12, "Melee (Base)": 12, "Name": "Frumious Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "ikora", "Super": 17, "Super (Base)": 17, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 59, "Total (Base)": 59, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 13, "Weapons (Base)": 13, "Year": 1, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 11, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 14, "Grenade (Base)": 14, "Hash": 3380315063, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530068080111310"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 11, "Melee (Base)": 11, "Name": "Strides of Ascendancy", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Solar Weapon Surge*", "Enhanced Relay Defender*", ], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "gardenofsalvation", "Source": "gardenofsalvation", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 61, "Total (Base)": 61, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 13, "Weapons (Base)": 13, "Year": 3, }, { "Archetype": undefined, "Class": 14, "Class (Base)": 14, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 1804192853, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917530068080111473"", "Loadouts": "", "Locked": false, "Masterwork Tier": 5, "Melee": 2, "Melee (Base)": 2, "Name": "Grips of Exaltation", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Minor Weapons Mod*", "Impact Induction*", "Impact Induction*", "Enhanced Relay Defender*", ], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "gardenofsalvation", "Source": "gardenofsalvation", "Super": 17, "Super (Base)": 17, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 61, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 13, "Weapons (Base)": 8, "Year": 3, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 19, "Grenade (Base)": 19, "Hash": 210208587, "Health": 24, "Health (Base)": 24, "Holofoil": false, "Id": ""6917530068088999352"", "Loadouts": "", "Locked": false, "Masterwork Tier": 5, "Melee": 12, "Melee (Base)": 12, "Name": "Vest of Transcendence", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Charged Up*", "Enhanced Relay Defender*", ], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "gardenofsalvation", "Source": "gardenofsalvation", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 7, "Weapons (Base)": 7, "Year": 3, }, { "Archetype": undefined, "Class": 18, "Class (Base)": 18, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 18, "Grenade (Base)": 18, "Hash": 407842012, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530068344592011"", "Loadouts": "", "Locked": false, "Masterwork Tier": 2, "Melee": 13, "Melee (Base)": 13, "Name": "Cowl of Righteousness", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 8, "Seasonal Mod": "gardenofsalvation", "Source": "gardenofsalvation", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 66, "Total (Base)": 66, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 9, "Weapons (Base)": 9, "Year": 3, }, { "Archetype": undefined, "Class": 9, "Class (Base)": 9, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 7, "Grenade (Base)": 7, "Hash": 3487540074, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917530068349458158"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 14, "Melee (Base)": 14, "Name": "Resonant Fury Vest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 16, "Seasonal Mod": "vowofthedisciple", "Source": "vowofthedisciple", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 21, "Weapons (Base)": 21, "Year": 5, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 6, "Grenade (Base)": 6, "Hash": 3051359899, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917530068373921425"", "Loadouts": "", "Locked": true, "Masterwork Tier": 4, "Melee": 22, "Melee (Base)": 22, "Name": "Errant Knight 1.0", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "engram", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 8, "Weapons (Base)": 8, "Year": 1, }, { "Archetype": undefined, "Class": 13, "Class (Base)": 13, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 1479557835, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530068379225690"", "Loadouts": "", "Locked": true, "Masterwork Tier": 1, "Melee": 13, "Melee (Base)": 13, "Name": "Shadestalker Vest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 25, "Seasonal Mod": "", "Source": "revenant", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 54, "Total (Base)": 54, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 8, "Weapons (Base)": 8, "Year": 7, }, { "Archetype": undefined, "Class": 23, "Class (Base)": 23, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 2805231813, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917530069398541391"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 12, "Melee (Base)": 12, "Name": "Dead End Cure 2.1", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "engram", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 62, "Total (Base)": 62, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 1, }, { "Archetype": "Specialist", "Class": 30, "Class (Base)": 30, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 13, "Grenade (Base)": 13, "Hash": 2809120022, "Health": 2, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530069400900465"", "Loadouts": "", "Locked": false, "Masterwork Tier": 2, "Melee": 2, "Melee (Base)": 0, "Name": "Relativism", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Spirit of the Dragon*", "Spirit of Verity*", ], "Power": 10, "Rarity": "Exotic", "Season": 24, "Seasonal Mod": "", "Source": "paleheart", "Super": 2, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": "grenade", "Tier": 0, "Total": 69, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 20, "Weapons (Base)": 20, "Year": 7, }, { "Archetype": undefined, "Class": 17, "Class (Base)": 17, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 691578978, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530069627531669"", "Loadouts": "", "Locked": false, "Masterwork Tier": 2, "Melee": 28, "Melee (Base)": 28, "Name": "Oathkeeper", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adamantine Brace*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 68, "Total (Base)": 68, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 13, "Weapons (Base)": 13, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 1291141296, "Health": 11, "Health (Base)": 11, "Holofoil": false, "Id": ""6917530071063744795"", "Loadouts": "", "Locked": false, "Masterwork Tier": 4, "Melee": 2, "Melee (Base)": 2, "Name": "Red Moon Phantom Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 4, "Seasonal Mod": "", "Source": "engram", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 54, "Total (Base)": 54, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 11, "Weapons (Base)": 11, "Year": 2, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 441033139, "Health": 10, "Health (Base)": 10, "Holofoil": false, "Id": ""6917530071070290086"", "Loadouts": "", "Locked": false, "Masterwork Tier": 4, "Melee": 22, "Melee (Base)": 22, "Name": "Dogged Gage", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 7, "Weapons (Base)": 7, "Year": 6, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 175883909, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917530071093516151"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 7, "Melee (Base)": 7, "Name": "Tireless Striders", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 22, "Seasonal Mod": "crotasend", "Source": "crotasend", "Super": 17, "Super (Base)": 17, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 9, "Weapons (Base)": 9, "Year": 6, }, { "Archetype": undefined, "Class": 15, "Class (Base)": 15, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 7, "Grenade (Base)": 7, "Hash": 2567710435, "Health": 7, "Health (Base)": 7, "Holofoil": false, "Id": ""6917530071100903443"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 12, "Melee (Base)": 12, "Name": "Icarus Drifter Mask", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "engram", "Super": 10, "Super (Base)": 10, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 58, "Total (Base)": 58, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 7, "Weapons (Base)": 7, "Year": 3, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 11, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 215674186, "Health": 2, "Health (Base)": 2, "Holofoil": false, "Id": ""6917530071100904248"", "Loadouts": "", "Locked": false, "Masterwork Tier": 2, "Melee": 11, "Melee (Base)": 11, "Name": "Errant Knight 1.0", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "engram", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 50, "Total (Base)": 50, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 12, "Weapons (Base)": 12, "Year": 1, }, { "Archetype": undefined, "Class": 13, "Class (Base)": 13, "Energy Capacity": 2, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 6, "Grenade (Base)": 6, "Hash": 3967899461, "Health": 11, "Health (Base)": 11, "Holofoil": false, "Id": ""6917530071100910386"", "Loadouts": "", "Locked": false, "Masterwork Tier": 2, "Melee": 19, "Melee (Base)": 19, "Name": "Frumious Grips", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "ikora", "Super": 7, "Super (Base)": 7, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 58, "Total (Base)": 58, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 1, }, { "Archetype": undefined, "Class": 21, "Class (Base)": 21, "Energy Capacity": 3, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 691578979, "Health": 11, "Health (Base)": 11, "Holofoil": false, "Id": ""6917530071752441690"", "Loadouts": "", "Locked": false, "Masterwork Tier": 3, "Melee": 8, "Melee (Base)": 8, "Name": "Shards of Galanor", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Sharp Edges*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 13, "Super (Base)": 13, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 64, "Total (Base)": 64, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 3, "Weapons (Base)": 3, "Year": 2, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 1310346765, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530071758857379"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 0, "Melee (Base)": 0, "Name": "Untethered Edge Cape", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 24, "Seasonal Mod": "", "Source": "echoes", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 7, }, { "Archetype": undefined, "Class": 19, "Class (Base)": 19, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 18, "Grenade (Base)": 18, "Hash": 691578979, "Health": 3, "Health (Base)": 3, "Holofoil": false, "Id": ""6917530084557628808"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 2, "Melee (Base)": 2, "Name": "Shards of Galanor", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Sharp Edges*", ], "Power": 10, "Rarity": "Exotic", "Season": 4, "Seasonal Mod": "artifice", "Source": "", "Super": 12, "Super (Base)": 12, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 67, "Total (Base)": 67, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 13, "Weapons (Base)": 13, "Year": 2, }, { "Archetype": undefined, "Class": 10, "Class (Base)": 10, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 7, "Grenade (Base)": 7, "Hash": 2203146422, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917530084559527547"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 10, "Melee (Base)": 10, "Name": "Assassin's Cowl", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Vanishing Execution*", ], "Power": 10, "Rarity": "Exotic", "Season": 8, "Seasonal Mod": "artifice", "Source": "", "Super": 15, "Super (Base)": 15, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 65, "Total (Base)": 65, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 11, "Weapons (Base)": 11, "Year": 3, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 0, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "Guardian Games", "Grenade": 2, "Grenade (Base)": 0, "Hash": 3131849739, "Health": 2, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530099168326892"", "Loadouts": "", "Locked": true, "Masterwork Tier": 10, "Melee": 2, "Melee (Base)": 0, "Name": "Mantle of Contests", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Artifice Armor*", ], "Power": 10, "Rarity": "Legendary", "Season": 26, "Seasonal Mod": "artifice", "Source": "events", "Super": 2, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 12, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 2, "Weapons (Base)": 0, "Year": 7, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 1, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 714372706, "Health": 13, "Health (Base)": 13, "Holofoil": false, "Id": ""6917530111284606370"", "Loadouts": "", "Locked": false, "Masterwork Tier": 1, "Melee": 20, "Melee (Base)": 20, "Name": "Crimson Plume Cowl", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 24, "Seasonal Mod": "", "Source": "crucible", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 57, "Total (Base)": 57, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 2, "Weapons (Base)": 2, "Year": 7, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 8, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 3075372781, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917530112158136535"", "Loadouts": "", "Locked": false, "Masterwork Tier": 8, "Melee": 2, "Melee (Base)": 2, "Name": "Flowing Cowl (CODA)", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "dungeon", "Super": 20, "Super (Base)": 20, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 2, "Weapons (Base)": 2, "Year": 3, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 8, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 3155320806, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917530112165844131"", "Loadouts": "", "Locked": false, "Masterwork Tier": 8, "Melee": 2, "Melee (Base)": 2, "Name": "Flowing Boots (CODA)", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "dungeon", "Super": 20, "Super (Base)": 20, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 3, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 8, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 1717940633, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530112165848175"", "Loadouts": "", "Locked": false, "Masterwork Tier": 8, "Melee": 0, "Melee (Base)": 0, "Name": "Cloak Judgment (CODA)", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "dungeon", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 3, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 8, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 508076356, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917530112165848576"", "Loadouts": "", "Locked": false, "Masterwork Tier": 8, "Melee": 2, "Melee (Base)": 2, "Name": "Flowing Vest (CODA)", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "dungeon", "Super": 20, "Super (Base)": 20, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 3, }, { "Archetype": undefined, "Class": 13, "Class (Base)": 13, "Energy Capacity": 8, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 3942036043, "Health": 13, "Health (Base)": 13, "Holofoil": false, "Id": ""6917530112165849298"", "Loadouts": "", "Locked": false, "Masterwork Tier": 8, "Melee": 2, "Melee (Base)": 2, "Name": "Aeon Swift", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Sect of Force*", ], "Power": 10, "Rarity": "Exotic", "Season": 2, "Seasonal Mod": "artifice", "Source": "", "Super": 20, "Super (Base)": 20, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 63, "Total (Base)": 63, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 3, "Weapons (Base)": 3, "Year": 1, }, { "Archetype": undefined, "Class": 12, "Class (Base)": 12, "Energy Capacity": 8, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 12, "Grenade (Base)": 12, "Hash": 4194072668, "Health": 12, "Health (Base)": 12, "Holofoil": false, "Id": ""6917530112165851836"", "Loadouts": "", "Locked": false, "Masterwork Tier": 8, "Melee": 2, "Melee (Base)": 2, "Name": "Flowing Grips (CODA)", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 11, "Seasonal Mod": "", "Source": "dungeon", "Super": 20, "Super (Base)": 20, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 60, "Total (Base)": 60, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 3, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 4, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 1861032455, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530113019037345"", "Loadouts": "", "Locked": false, "Masterwork Tier": 4, "Melee": 0, "Melee (Base)": 0, "Name": "A Cloak Called Home", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 10, "Rarity": "Legendary", "Season": 1, "Seasonal Mod": "", "Source": "engram", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 1, }, { "Archetype": "Grenadier", "Class": 9, "Class (Base)": 9, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 28, "Grenade (Base)": 28, "Hash": 3389206211, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530126815615565"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 0, "Melee (Base)": 0, "Name": "AION Renewal Cloak", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 24, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "kepler", "Super": 16, "Super (Base)": 16, "Tag": undefined, "Tertiary Stat": "class", "Tier": 1, "Total": 53, "Total (Base)": 53, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 8, }, { "Archetype": "Specialist", "Class": 30, "Class (Base)": 30, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": true, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 4150538093, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530126868288953"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 9, "Melee (Base)": 9, "Name": "Techsec Cloak", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Weapons Mod*", "Bomber*", "Reaper*", "Reaper*", ], "Power": 20, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": "melee", "Tier": 1, "Total": 65, "Total (Base)": 55, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 26, "Weapons (Base)": 16, "Year": 8, }, { "Archetype": "Paragon", "Class": 8, "Class (Base)": 8, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 4150538093, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530126870310653"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 16, "Melee (Base)": 16, "Name": "Techsec Cloak", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 20, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "", "Super": 28, "Super (Base)": 28, "Tag": undefined, "Tertiary Stat": "class", "Tier": 1, "Total": 52, "Total (Base)": 52, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 8, }, { "Archetype": "Bulwark", "Class": 17, "Class (Base)": 17, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 9, "Grenade (Base)": 9, "Hash": 2292070913, "Health": 30, "Health (Base)": 30, "Holofoil": false, "Id": ""6917530126874226657"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 0, "Melee (Base)": 0, "Name": "Techsec Mask", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 20, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": "grenade", "Tier": 1, "Total": 56, "Total (Base)": 56, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 0, "Weapons (Base)": 0, "Year": 8, }, { "Archetype": "Brawler", "Class": 0, "Class (Base)": 0, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 4150538093, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917530126876830928"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 29, "Melee (Base)": 29, "Name": "Techsec Cloak", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [], "Power": 20, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": "weapons", "Tier": 1, "Total": 54, "Total (Base)": 54, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 9, "Weapons (Base)": 9, "Year": 8, }, { "Archetype": "Gunner", "Class": 0, "Class (Base)": 0, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 1589715538, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530129154281648"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 8, "Melee (Base)": 8, "Name": "Techsec Strides", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 20, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": "melee", "Tier": 1, "Total": 53, "Total (Base)": 53, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 29, "Weapons (Base)": 29, "Year": 8, }, { "Archetype": "Paragon", "Class": 9, "Class (Base)": 9, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 3218422834, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530129156398359"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 17, "Melee (Base)": 17, "Name": "AION Renewal Vest", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 20, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "kepler", "Super": 29, "Super (Base)": 29, "Tag": undefined, "Tertiary Stat": "class", "Tier": 1, "Total": 55, "Total (Base)": 55, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 0, "Weapons (Base)": 0, "Year": 8, }, { "Archetype": undefined, "Class": 7, "Class (Base)": 7, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 10, "Grenade (Base)": 10, "Hash": 3160437036, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917530129159935580"", "Loadouts": "", "Locked": false, "Masterwork Tier": 5, "Melee": 6, "Melee (Base)": 6, "Name": "Shadow Specter", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 19, "Rarity": "Rare", "Season": 4, "Seasonal Mod": "", "Source": "campaign", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 47, "Total (Base)": 47, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 2, "Weapons (Base)": 2, "Year": 2, }, { "Archetype": undefined, "Class": 2, "Class (Base)": 2, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 2, "Grenade (Base)": 2, "Hash": 2891906302, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917530129171178839"", "Loadouts": "", "Locked": false, "Masterwork Tier": 5, "Melee": 16, "Melee (Base)": 16, "Name": "Gumshoe Gumption Mask", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 25, "Rarity": "Rare", "Season": 16, "Seasonal Mod": "", "Source": "campaign", "Super": 6, "Super (Base)": 6, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 50, "Total (Base)": 50, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 8, "Weapons (Base)": 8, "Year": 5, }, { "Archetype": "Specialist", "Class": 29, "Class (Base)": 29, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 3129990424, "Health": 8, "Health (Base)": 8, "Holofoil": false, "Id": ""6917530129172938772"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 0, "Melee (Base)": 0, "Name": "Techsec Grasps", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 25, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": "health", "Tier": 1, "Total": 54, "Total (Base)": 54, "Tuning Stat": undefined, "Type": "Gauntlets", "Weapons": 17, "Weapons (Base)": 17, "Year": 8, }, { "Archetype": "Bulwark", "Class": 16, "Class (Base)": 16, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 503854896, "Health": 28, "Health (Base)": 28, "Holofoil": false, "Id": ""6917530129172940001"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 10, "Melee (Base)": 10, "Name": "Techsec Vest", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 29, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": "melee", "Tier": 1, "Total": 54, "Total (Base)": 54, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 0, "Weapons (Base)": 0, "Year": 8, }, { "Archetype": undefined, "Class": 11, "Class (Base)": 11, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 8, "Grenade (Base)": 8, "Hash": 2270345041, "Health": 11, "Health (Base)": 11, "Holofoil": false, "Id": ""6917530129172940362"", "Loadouts": "", "Locked": false, "Masterwork Tier": 5, "Melee": 16, "Melee (Base)": 16, "Name": "Gumshoe Gumption Strides", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 23, "Rarity": "Rare", "Season": 16, "Seasonal Mod": "", "Source": "campaign", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 50, "Total (Base)": 50, "Tuning Stat": undefined, "Type": "Leg Armor", "Weapons": 2, "Weapons (Base)": 2, "Year": 5, }, { "Archetype": undefined, "Class": 0, "Class (Base)": 0, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 3437155610, "Health": 0, "Health (Base)": 0, "Holofoil": false, "Id": ""6917530129176645663"", "Loadouts": "", "Locked": false, "Masterwork Tier": 5, "Melee": 0, "Melee (Base)": 0, "Name": "War Mantis Cloak", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 30, "Rarity": "Rare", "Season": 4, "Seasonal Mod": "", "Source": "campaign", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 0, "Total (Base)": 0, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 2, }, { "Archetype": "Gunner", "Class": 0, "Class (Base)": 0, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 17, "Grenade (Base)": 17, "Hash": 4150538093, "Health": 9, "Health (Base)": 9, "Holofoil": false, "Id": ""6917530129176651304"", "Loadouts": "", "Locked": true, "Masterwork Tier": 0, "Melee": 0, "Melee (Base)": 0, "Name": "Techsec Cloak", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 33, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "", "Super": 0, "Super (Base)": 0, "Tag": undefined, "Tertiary Stat": "health", "Tier": 1, "Total": 55, "Total (Base)": 55, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 29, "Weapons (Base)": 29, "Year": 8, }, { "Archetype": "Bulwark", "Class": 17, "Class (Base)": 17, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 2292070913, "Health": 30, "Health (Base)": 30, "Holofoil": false, "Id": ""6917530129178803400"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 0, "Melee (Base)": 0, "Name": "Techsec Mask", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 32, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "", "Super": 8, "Super (Base)": 8, "Tag": undefined, "Tertiary Stat": "super", "Tier": 1, "Total": 55, "Total (Base)": 55, "Tuning Stat": undefined, "Type": "Helmet", "Weapons": 0, "Weapons (Base)": 0, "Year": 8, }, { "Archetype": "Brawler", "Class": 0, "Class (Base)": 0, "Energy Capacity": 10, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 0, "Grenade (Base)": 0, "Hash": 3389206211, "Health": 16, "Health (Base)": 16, "Holofoil": false, "Id": ""6917530141793399654"", "Loadouts": "", "Locked": false, "Masterwork Tier": 0, "Melee": 29, "Melee (Base)": 29, "Name": "AION Renewal Cloak", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 32, "Rarity": "Legendary", "Season": 27, "Seasonal Mod": "", "Source": "kepler", "Super": 9, "Super (Base)": 9, "Tag": undefined, "Tertiary Stat": "super", "Tier": 1, "Total": 54, "Total (Base)": 54, "Tuning Stat": undefined, "Type": "Hunter Cloak", "Weapons": 0, "Weapons (Base)": 0, "Year": 8, }, { "Archetype": undefined, "Class": 6, "Class (Base)": 6, "Energy Capacity": 5, "Equippable": "Hunter", "Equipped": false, "Event": "", "Grenade": 16, "Grenade (Base)": 16, "Hash": 3309120116, "Health": 6, "Health (Base)": 6, "Holofoil": false, "Id": ""6917530141795467364"", "Loadouts": "", "Locked": false, "Masterwork Tier": 5, "Melee": 6, "Melee (Base)": 6, "Name": "Shadow Specter", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Power": 32, "Rarity": "Rare", "Season": 4, "Seasonal Mod": "", "Source": "campaign", "Super": 2, "Super (Base)": 2, "Tag": undefined, "Tertiary Stat": undefined, "Tier": 0, "Total": 48, "Total (Base)": 48, "Tuning Stat": undefined, "Type": "Chest Armor", "Weapons": 12, "Weapons (Base)": 12, "Year": 2, }, ] `; exports[`process stores generates a correct ghost CSV export 1`] = ` [ { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 573576346, "Holofoil": false, "Id": ""6917529085740071304"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Sagira's Shell", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Rarity": "Exotic", "Season": 2, "Source": "mercury", "Tag": undefined, "Tier": 0, "Year": 1, }, { "Energy Capacity": undefined, "Equipped": true, "Event": "", "Hash": 1283654212, "Holofoil": false, "Id": ""6917529127946162805"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Gilded Shell", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Wombo Detector*", ], "Rarity": "Exotic", "Season": 8, "Source": "deluxe", "Tag": undefined, "Tier": 0, "Year": 3, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 227918504, "Holofoil": false, "Id": ""6917529156697381898"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Future Perfect Shell", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Rarity": "Exotic", "Season": 5, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 2, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 4238874520, "Holofoil": false, "Id": ""6917529193444242909"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Hardlink Shell", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Rarity": "Exotic", "Season": 10, "Source": "seasonpass", "Tag": undefined, "Tier": 0, "Year": 3, }, { "Energy Capacity": undefined, "Equipped": true, "Event": "", "Hash": 3398078482, "Holofoil": false, "Id": ""6917529198421309490"", "Loadouts": "", "Locked": true, "Masterwork Tier": undefined, "Name": "Adonis Shell", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Crucible Lazurite*", "Blinding Light*", "Prize Pursuant*", "Standard Glimmer Booster*", ], "Rarity": "Exotic", "Season": 11, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 3, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 1558857471, "Holofoil": false, "Id": ""6917529261791074579"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Cosmos Shell", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Wombo Detector*", ], "Rarity": "Exotic", "Season": 2, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 1, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 1090082587, "Holofoil": false, "Id": ""6917529441305449079"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Orbweaver Shell", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Rarity": "Exotic", "Season": 10, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 3, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 527607310, "Holofoil": false, "Id": ""6917529613981176946"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Hissing Silence Shell", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Rarity": "Exotic", "Season": 6, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 2, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 634549439, "Holofoil": false, "Id": ""6917529805257494505"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Phantasmal Shell", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Rarity": "Exotic", "Season": 17, "Source": "seasonpass", "Tag": undefined, "Tier": 0, "Year": 5, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 2313814566, "Holofoil": false, "Id": ""6917529812262344063"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Speed Metal Shell", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Rarity": "Exotic", "Season": 18, "Source": "deluxe", "Tag": undefined, "Tier": 0, "Year": 5, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 227918505, "Holofoil": false, "Id": ""6917529812274003175"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Sanctum Plate Shell", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Rarity": "Exotic", "Season": 5, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 2, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 1302968378, "Holofoil": false, "Id": ""6917529842269845415"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Castaway's Shell", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Rarity": "Exotic", "Season": 18, "Source": "seasonpass", "Tag": undefined, "Tier": 0, "Year": 5, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 3809059822, "Holofoil": false, "Id": ""6917529859037522184"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Warsat Shell", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Rarity": "Exotic", "Season": 19, "Source": "rasputin", "Tag": undefined, "Tier": 0, "Year": 5, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 1490914249, "Holofoil": false, "Id": ""6917529891097015415"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Guardian's Angel Shell", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Rarity": "Exotic", "Season": 20, "Source": "seasonpass", "Tag": undefined, "Tier": 0, "Year": 6, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 3705925878, "Holofoil": false, "Id": ""6917529932856531597"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Aoki/Faas Shell", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Rarity": "Exotic", "Season": 14, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 4, }, { "Energy Capacity": undefined, "Equipped": true, "Event": "", "Hash": 210874516, "Holofoil": false, "Id": ""6917529932874117020"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Final Shell", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Rarity": "Exotic", "Season": 22, "Source": "deluxe", "Tag": undefined, "Tier": 0, "Year": 6, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 1013853356, "Holofoil": false, "Id": ""6917529950744002122"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Halcyon Shell", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Rarity": "Exotic", "Season": 12, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 4, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 3775649487, "Holofoil": false, "Id": ""6917529951182029807"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Hexed Shell", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [], "Rarity": "Exotic", "Season": 22, "Source": "seasonpass", "Tag": undefined, "Tier": 0, "Year": 6, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "The Dawning", "Hash": 2549404869, "Holofoil": false, "Id": ""6917529966167567978"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Winter Lotus Shell", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [], "Rarity": "Exotic", "Season": 2, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 1, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 3733702440, "Holofoil": false, "Id": ""6917529993263920330"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Enhanced Defense Shell", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Rarity": "Exotic", "Season": 23, "Source": "eververse", "Tag": undefined, "Tier": 0, "Year": 6, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 1163767710, "Holofoil": false, "Id": ""6917530045775640926"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Tech Witch Shell", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [], "Rarity": "Exotic", "Season": 20, "Source": "wartable", "Tag": undefined, "Tier": 0, "Year": 6, }, { "Energy Capacity": undefined, "Equipped": false, "Event": "", "Hash": 3293891384, "Holofoil": false, "Id": ""6917530141788022072"", "Loadouts": "", "Locked": false, "Masterwork Tier": undefined, "Name": "Quarter Quartz Shell", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [], "Rarity": "Exotic", "Season": 27, "Source": "deluxe", "Tag": undefined, "Tier": 0, "Year": 8, }, ] `; exports[`process stores generates a correct weapon CSV export 1`] = ` [ { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Aggressive Burst", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 4138174248, "Holofoil": false, "Id": ""6917529080189540125"", "Impact": 35, "Kill Tracker": 8179, "Loadouts": "", "Locked": true, "Mag": 40, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Go Figure", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Aggressive Burst*", "Dusk Sight D1*", "King Dot K2", "Alloy Magazine", "Flared Magwell*", "Outlaw*", "Kill Clip*", "Kill Tracker", "Masterworked: Range*", "Desert of Gold*", ], "Power": 10, "ROF": 450, "Range": 82, "Rarity": "Legendary", "Recoil": 69, "Reload": 48, "Season": 4, "Shield Duration": 0, "Source": "engram", "Stability": 62, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 2, "Zoom": 18, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 77, "Hash": 2009277538, "Holofoil": false, "Id": ""6917529080271344939"", "Impact": 75, "Kill Tracker": 4307, "Loadouts": "", "Locked": true, "Mag": 45, "Masterwork Tier": 10, "Masterwork Type": "Handling", "Name": "The Last Dance", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Burst*", "QuickDot SAS*", "Control SAS", "Target SAS", "Alloy Magazine", "Flared Magwell*", "Outlaw*", "Kill Clip*", "Kill Tracker", "Masterworked: Handling*", "New Monarchy Diamonds*", ], "Power": 10, "ROF": 491, "Range": 29, "Rarity": "Legendary", "Recoil": 97, "Reload": 43, "Season": 4, "Shield Duration": 0, "Source": "strikes", "Stability": 94, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 2, "Zoom": 12, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 29, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 667, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 4094657108, "Holofoil": false, "Id": ""6917529089061235239"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Techeun Force", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Fluted Barrel", "Hammer-Forged Rifling", "Liquid Coils*", "Projection Fuse", "Kill Clip*", "Backup Plan*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 90, "Reload": 34, "Season": 4, "Shield Duration": 0, "Source": "lastwish", "Stability": 39, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 2, "Zoom": 15, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Arc Traps", "Blast Radius": 70, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 79, "Hash": 2376481550, "Holofoil": false, "Id": ""6917529153398192114"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Anarchy", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Arc Traps*", "Quick Launch*", "High-Velocity Rounds*", "Moving Target*", "Composite Stock*", "Kill Tracker", ], "Power": 10, "ROF": 150, "Range": 0, "Rarity": "Exotic", "Recoil": 50, "Reload": 73, "Season": 5, "Shield Duration": 0, "Source": "", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 100, "Year": 2, "Zoom": 13, }, { "AA": 74, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 57, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": true, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 41, "Hash": 48643186, "Holofoil": false, "Id": ""6917529182050772376"", "Impact": 84, "Kill Tracker": 53, "Loadouts": "", "Locked": true, "Mag": 10, "Masterwork Tier": 5, "Masterwork Type": "Reload Speed", "Name": "Ancient Gospel", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Smallbore", "Accurized Rounds*", "Steady Rounds", "Rapid Hit*", "Dragonfly*", "Kill Tracker", "Tier 5: Reload Speed*", ], "Power": 10, "ROF": 140, "Range": 51, "Rarity": "Legendary", "Recoil": 100, "Reload": 48, "Season": 8, "Shield Duration": 0, "Source": "gardenofsalvation", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 3, "Zoom": 14, }, { "AA": 33, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "heavy", "Ammo Generation": 3, "Archetype": "Pyrotoxin Rounds", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 30, "Hash": 1395261499, "Holofoil": false, "Id": ""6917529190126749927"", "Impact": 100, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 20, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Xenophage", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Pyrotoxin Rounds*", "Full Bore*", "High-Caliber Rounds*", "Rangefinder*", "Composite Stock*", "Kill Tracker", "A Better Specimen*", ], "Power": 10, "ROF": 120, "Range": 88, "Rarity": "Exotic", "Recoil": 85, "Reload": 31, "Season": 8, "Shield Duration": 0, "Source": "exoticquest", "Stability": 27, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 3, "Zoom": 16, }, { "AA": 33, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 34, "Archetype": "Shot Package", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 26, "Hash": 1179141605, "Holofoil": false, "Id": ""6917529192766077966"", "Impact": 80, "Kill Tracker": 5, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Felwinter's Lie", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Shot Package*", "Full Choke*", "Accurized Rounds*", "Slideshot", "Surplus*", "Opening Shot*", "Vorpal Weapon", "Crucible Tracker", "Masterworked: Range*", ], "Power": 10, "ROF": 55, "Range": 41, "Rarity": "Legendary", "Recoil": 62, "Reload": 34, "Season": 10, "Shield Duration": 0, "Source": "rasputin", "Stability": 19, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 3, "Zoom": 12, }, { "AA": 74, "Accuracy": 95, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Split Electron", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 633, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 814876685, "Holofoil": false, "Id": ""6917529193629874318"", "Impact": 80, "Kill Tracker": 1684, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 10, "Masterwork Type": "Draw Time", "Name": "Trinity Ghoul", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Split Electron*", "High Tension String*", "Compact Arrow Shaft*", "Lightning Rod*", "Kill Tracker", "Trinity Ghoul Catalyst*", "Tangled Outrider*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Exotic", "Recoil": 77, "Reload": 60, "Season": 4, "Shield Duration": 0, "Source": "", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 2, "Zoom": 18, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 3143732432, "Holofoil": false, "Id": ""6917529193841282683"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 31, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "False Promises", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Arrowhead Brake*", "Corkscrew Rifling", "Alloy Magazine*", "Appended Mag", "Feeding Frenzy*", "Swashbuckler*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 360, "Range": 72, "Rarity": "Legendary", "Recoil": 100, "Reload": 38, "Season": 11, "Shield Duration": 0, "Source": "contact", "Stability": 30, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 3, "Zoom": 20, }, { "AA": 70, "Accuracy": 78, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 56, "Archetype": "Poison Arrows", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 600, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, "Hash": 3588934839, "Holofoil": false, "Id": ""6917529194315111258"", "Impact": 68, "Kill Tracker": 139, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Le Monarque", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Poison Arrows*", "Natural String*", "Compact Arrow Shaft*", "Snapshot Sights*", "Crucible Tracker", "Poison Wings Spread*", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Exotic", "Recoil": 78, "Reload": 50, "Season": 5, "Shield Duration": 0, "Source": "blackarmory", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 2, "Zoom": 18, }, { "AA": 70, "Accuracy": 71, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 667, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 80, "Hash": 4095896073, "Holofoil": false, "Id": ""6917529194883693554"", "Impact": 76, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Accrued Redemption", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Agile Bowstring*", "Elastic String", "Compact Arrow Shaft*", "Natural Fletching", "Rangefinder*", "Rampage*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 77, "Reload": 51, "Season": 8, "Shield Duration": 0, "Source": "gardenofsalvation", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 3, "Zoom": 18, }, { "AA": 84, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "special", "Ammo Generation": 46, "Archetype": "Primeval's Torment", "Blast Radius": 5, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": true, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 100, "Hash": 2357297366, "Holofoil": false, "Id": ""6917529195790275568"", "Impact": 0, "Kill Tracker": 860, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 10, "Masterwork Type": "Handling", "Name": "Witherhoard", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Primeval's Torment*", "Countermass*", "Black Powder*", "Break the Bank*", "Composite Stock*", "Kill Tracker", "White Collar Crime*", "Witherhoard Catalyst*", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Exotic", "Recoil": 100, "Reload": 33, "Season": 11, "Shield Duration": 0, "Source": "", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 12, "Year": 3, "Zoom": 12, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 69, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, "Hash": 65611680, "Holofoil": false, "Id": ""6917529196840622553"", "Impact": 35, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 20, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "The Fool's Remedy", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "QuickDot SAS", "Control SAS*", "Alloy Magazine", "Flared Magwell*", "Feeding Frenzy*", "Kill Clip*", "Kill Tracker", "Backup Mag*", "Tier 1: Reload Speed*", "Shrouded Stripes*", ], "Power": 10, "ROF": 450, "Range": 27, "Rarity": "Legendary", "Recoil": 94, "Reload": 54, "Season": 11, "Shield Duration": 0, "Source": "ironbanner", "Stability": 75, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 3, "Zoom": 12, }, { "AA": 32, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 46, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 1216130969, "Holofoil": false, "Id": ""6917529198344801101"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 36, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Cold Denial", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Arrowhead Brake*", "Corkscrew Rifling", "Extended Mag*", "High-Caliber Rounds", "Feeding Frenzy*", "Swashbuckler*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 340, "Range": 57, "Rarity": "Legendary", "Recoil": 100, "Reload": 14, "Season": 11, "Shield Duration": 0, "Source": "seasonpass", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 3, "Zoom": 14, }, { "AA": 83, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 69, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 74, "Hash": 65611680, "Holofoil": false, "Id": ""6917529200163324235"", "Impact": 35, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 20, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "The Fool's Remedy", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "QuickDot SAS*", "Tactic SAS", "Extended Mag*", "Light Mag", "Subsistence*", "Iron Gaze*", "Kill Tracker", "Tier 3: Stability*", "Iron Vendetta*", ], "Power": 10, "ROF": 450, "Range": 7, "Rarity": "Legendary", "Recoil": 94, "Reload": 18, "Season": 11, "Shield Duration": 0, "Source": "ironbanner", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 3, "Zoom": 12, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Vortex Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 10, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 60, "Guard Resistance": 70, "Handling": 0, "Hash": 614426548, "Holofoil": false, "Id": ""6917529200341055201"", "Impact": 68, "Kill Tracker": 721, "Loadouts": "", "Locked": true, "Mag": 62, "Masterwork Tier": 10, "Masterwork Type": "Impact", "Name": "Falling Guillotine", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Vortex Frame*", "Enduring Blade", "Honed Edge", "Jagged Edge*", "Balanced Guard", "Heavy Guard*", "Relentless Strikes*", "Whirlwind Blade*", "En Garde", "Kill Tracker", "Masterworked: Impact*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 11, "Shield Duration": 0, "Source": "seasonpass", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 3, "Zoom": 0, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Caster Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 50, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 80, "Handling": 0, "Hash": 35794111, "Holofoil": false, "Id": ""6917529200352312365"", "Impact": 59, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 69, "Masterwork Tier": 2, "Masterwork Type": "Impact", "Name": "Temptation's Hook", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Caster Frame*", "Enduring Blade*", "Jagged Edge", "Tempered Edge", "Burst Guard*", "Enduring Guard", "Tireless Blade*", "Vorpal Weapon*", "Whirlwind Blade", "Kill Tracker", "Tier 2: Impact*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 11, "Shield Duration": 0, "Source": "contact", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 3, "Zoom": 0, }, { "AA": 86, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 39, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 23, "Hash": 2199171672, "Holofoil": false, "Id": ""6917529201168095112"", "Impact": 51, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 15, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Lonesome", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Polygonal Rifling", "Extended Mag*", "Appended Mag", "Outlaw*", "Demolitionist*", "Kill Clip", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 260, "Range": 68, "Rarity": "Legendary", "Recoil": 100, "Reload": 1, "Season": 6, "Shield Duration": 0, "Source": "gambitprime", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 2, "Zoom": 12, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 52, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 56, "Hash": 3745990145, "Holofoil": false, "Id": ""6917529207795753895"", "Impact": 70, "Kill Tracker": 5, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Long Shadow", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "ATD Raptor", "ATA Scout*", "Appended Mag*", "Flared Magwell", "Field Prep*", "Outlaw*", "Kill Tracker", "Tier 3: Handling*", "Shattered Sky*", ], "Power": 10, "ROF": 90, "Range": 49, "Rarity": "Legendary", "Recoil": 47, "Reload": 40, "Season": 4, "Shield Duration": 0, "Source": "strikes", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 2, "Zoom": 51, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "fotc", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 2742838700, "Holofoil": false, "Id": ""6917529209057310067"", "Impact": 92, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 9, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "True Prophecy", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Fastdraw HCS*", "SteadyHand HCS", "Accurized Rounds*", "Tactical Mag", "Rangefinder*", "Rampage*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 120, "Range": 70, "Rarity": "Legendary", "Recoil": 93, "Reload": 22, "Season": 10, "Shield Duration": 0, "Source": "engram", "Stability": 27, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 3, "Zoom": 14, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 821154603, "Holofoil": false, "Id": ""6917529210841608217"", "Impact": 21, "Kill Tracker": 148, "Loadouts": "", "Locked": true, "Mag": 47, "Masterwork Tier": 10, "Masterwork Type": "Stability", "Name": "Gnawing Hunger", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator", "Smallbore*", "Appended Mag*", "Steady Rounds", "Subsistence*", "Rampage*", "Kill Tracker", "Sweaty Confetti*", "Masterworked: Stability*", "Coastal Suede*", ], "Power": 10, "ROF": 600, "Range": 55, "Rarity": "Legendary", "Recoil": 54, "Reload": 57, "Season": 6, "Shield Duration": 0, "Source": "gambitprime", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 2, "Zoom": 16, }, { "AA": 60, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 667, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 3055192515, "Holofoil": false, "Id": ""6917529211188834636"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 5, "Masterwork Type": "Handling", "Name": "Timelines' Vertex", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Impulse MS3", "Candle PS*", "Liquid Coils*", "Projection Fuse", "Auto-Loading Holster*", "Disruption Break*", "Kill Tracker", "Tier 5: Handling*", ], "Power": 10, "ROF": 0, "Range": 38, "Rarity": "Legendary", "Recoil": 60, "Reload": 40, "Season": 10, "Shield Duration": 0, "Source": "engram", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 3, "Zoom": 15, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 22, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 27, "Hash": 1513993763, "Holofoil": false, "Id": ""6917529211365207988"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 32, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Friction Fire", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Appended Mag*", "Tactical Mag", "Subsistence*", "Rampage*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 600, "Range": 56, "Rarity": "Legendary", "Recoil": 85, "Reload": 18, "Season": 12, "Shield Duration": 0, "Source": "wrathborn", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 4, "Zoom": 15, }, { "AA": 88, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 72, "Hash": 2742838701, "Holofoil": false, "Id": ""6917529212229492641"", "Impact": 84, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 13, "Masterwork Tier": 5, "Masterwork Type": "Stability", "Name": "Dire Promise", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fastdraw HCS*", "Sureshot HCS", "Appended Mag*", "High-Caliber Rounds", "Opening Shot*", "Rangefinder*", "Kill Tracker", "Tier 5: Stability*", ], "Power": 10, "ROF": 140, "Range": 42, "Rarity": "Legendary", "Recoil": 84, "Reload": 49, "Season": 10, "Shield Duration": 0, "Source": "engram", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 3, "Zoom": 14, }, { "AA": 79, "Accuracy": 0, "Airborne Effectiveness": 57, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 3281285075, "Holofoil": false, "Id": ""6917529212533899973"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Posterity", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator", "Hammer-Forged Rifling*", "Extended Mag*", "Steady Rounds", "Feeding Frenzy*", "Rampage*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 180, "Range": 49, "Rarity": "Legendary", "Recoil": 100, "Reload": 37, "Season": 12, "Shield Duration": 0, "Source": "deepstonecrypt", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 4, "Zoom": 14, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 40, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 23, "Hash": 2220884262, "Holofoil": false, "Id": ""6917529217609982463"", "Impact": 92, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 11, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "The Steady Hand", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Fastdraw HCS", "HitMark HCS*", "Ricochet Rounds*", "Flared Magwell", "Moving Target*", "Snapshot Sights*", "Kill Tracker", "Backup Mag*", "Tier 3: Handling*", ], "Power": 10, "ROF": 120, "Range": 70, "Rarity": "Legendary", "Recoil": 85, "Reload": 20, "Season": 12, "Shield Duration": 0, "Source": "ironbanner", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 4, "Zoom": 14, }, { "AA": 27, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 31, "Archetype": "Rapid-Fire Frame", "Blast Radius": 18, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 13, "Hash": 1972985595, "Holofoil": false, "Id": ""6917529219466269620"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 3, "Masterwork Type": "Blast Radius", "Name": "Swarm of the Raven", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Volatile Launch*", "Countermass", "Proximity Grenades*", "Sticky Grenades", "Auto-Loading Holster*", "Rampage*", "Kill Tracker", "Tier 3: Blast Radius*", ], "Power": 10, "ROF": 150, "Range": 0, "Rarity": "Legendary", "Recoil": 63, "Reload": 21, "Season": 4, "Shield Duration": 0, "Source": "ironbanner", "Stability": 21, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 44, "Year": 2, "Zoom": 13, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 26, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 967, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "The Dawning", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 48, "Hash": 1030895163, "Holofoil": false, "Id": ""6917529221549498175"", "Impact": 100, "Kill Tracker": 4, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Glacioclasm", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Corkscrew Rifling", "Fluted Barrel*", "Liquid Coils*", "Particle Repeater", "Surplus*", "Auto-Loading Holster*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 0, "Range": 63, "Rarity": "Legendary", "Recoil": 78, "Reload": 15, "Season": 12, "Shield Duration": 0, "Source": "events", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 3143732432, "Holofoil": false, "Id": ""6917529221576625028"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 31, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "False Promises", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Extended Barrel", "Polygonal Rifling*", "Appended Mag", "High-Caliber Rounds*", "Dynamic Sway Reduction*", "Rampage*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 360, "Range": 77, "Rarity": "Legendary", "Recoil": 86, "Reload": 38, "Season": 11, "Shield Duration": 0, "Source": "contact", "Stability": 39, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 3, "Zoom": 20, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Banshee's Wail", "Blast Radius": 0, "Category": "Power", "Charge Rate": 20, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 90, "Guard Resistance": 50, "Handling": 0, "Hash": 3487253372, "Holofoil": false, "Id": ""6917529228234751942"", "Impact": 78, "Kill Tracker": 19, "Loadouts": "", "Locked": true, "Mag": 65, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "The Lament", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Banshee's Wail*", "Jagged Edge*", "Enduring Guard*", "Tireless Blade*", "Revved Consumption*", "Kill Tracker", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Exotic", "Recoil": 0, "Reload": 0, "Season": 12, "Shield Duration": 0, "Source": "europa", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 4, "Zoom": 0, }, { "AA": 29, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 34, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "The Dawning", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 68, "Hash": 1506719573, "Holofoil": false, "Id": ""6917529230334723289"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Cold Front", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Fluted Barrel*", "Polygonal Rifling", "Accurized Rounds*", "Tactical Mag", "Surplus*", "Thresh*", "Kill Tracker", "Backup Mag*", "Tier 2: Stability*", ], "Power": 10, "ROF": 720, "Range": 50, "Rarity": "Legendary", "Recoil": 92, "Reload": 19, "Season": 9, "Shield Duration": 0, "Source": "events", "Stability": 14, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 3, "Zoom": 14, }, { "AA": 84, "Accuracy": 0, "Airborne Effectiveness": 47, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 29, "Hash": 3281285075, "Holofoil": false, "Id": ""6917529257805248292"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Posterity", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Fluted Barrel", "Accurized Rounds*", "Flared Magwell", "Surplus*", "Redirection*", "Kill Tracker", "Targeting Adjuster*", "Tier 3: Handling*", ], "Power": 10, "ROF": 180, "Range": 59, "Rarity": "Legendary", "Recoil": 100, "Reload": 57, "Season": 12, "Shield Duration": 0, "Source": "deepstonecrypt", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 4, "Zoom": 14, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 75, "Hash": 568515759, "Holofoil": false, "Id": ""6917529259363563817"", "Impact": 27, "Kill Tracker": 10, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Chattering Bone", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Corkscrew Rifling*", "Light Mag*", "Kill Clip*", "High-Impact Reserves*", "Kill Tracker", "Masterworked: Range*", "Dreaming Spectrum*", ], "Power": 10, "ROF": 450, "Range": 53, "Rarity": "Legendary", "Recoil": 57, "Reload": 73, "Season": 4, "Shield Duration": 0, "Source": "lastwish", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 2, "Zoom": 17, }, { "AA": 60, "Accuracy": 0, "Airborne Effectiveness": 27, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Black Hole", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 3628991658, "Holofoil": false, "Id": ""6917529261801957797"", "Impact": 29, "Kill Tracker": 1431, "Loadouts": "", "Locked": true, "Mag": 30, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Graviton Lance", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Black Hole*", "Hammer-Forged Rifling*", "Accurized Rounds*", "Cosmology*", "Fitted Stock*", "Kill Tracker", "Graviton Lance Catalyst*", ], "Power": 10, "ROF": 300, "Range": 90, "Rarity": "Exotic", "Recoil": 85, "Reload": 57, "Season": 1, "Shield Duration": 0, "Source": "", "Stability": 100, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 1, "Zoom": 17, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 48, "Archetype": "Wave Frame", "Blast Radius": 60, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 87, "Hash": 981718087, "Holofoil": false, "Id": ""6917529263471888577"", "Impact": 0, "Kill Tracker": 128, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 10, "Masterwork Type": "Blast Radius", "Name": "Deafening Whisper", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Wave Frame*", "Countermass", "Quick Launch*", "High-Velocity Rounds*", "Implosion Rounds", "Killing Wind*", "Auto-Loading Holster*", "Kill Tracker", "Masterworked: Blast Radius*", ], "Power": 10, "ROF": 72, "Range": 0, "Rarity": "Legendary", "Recoil": 81, "Reload": 80, "Season": 12, "Shield Duration": 0, "Source": "wrathborn", "Stability": 38, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 84, "Year": 4, "Zoom": 13, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 79, "Hash": 1690783811, "Holofoil": false, "Id": ""6917529263512296643"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 41, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "The Forward Path", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Cleanshot IS*", "Red Dot 2 MOA", "Alloy Magazine*", "Flared Magwell", "Dynamic Sway Reduction*", "Multikill Clip*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 600, "Range": 50, "Rarity": "Legendary", "Recoil": 60, "Reload": 62, "Season": 11, "Shield Duration": 0, "Source": "ironbanner", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 3, "Zoom": 17, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "special", "Ammo Generation": 49, "Archetype": "Pinpoint Slug Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 65, "Hash": 4248569242, "Holofoil": false, "Id": ""6917529265091876850"", "Impact": 70, "Kill Tracker": 1074, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Heritage", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Pinpoint Slug Frame*", "Full Bore*", "Smallbore", "Appended Mag*", "Extended Mag", "Reconstruction*", "Thresh*", "Kill Tracker", "Masterworked: Range*", ], "Power": 10, "ROF": 65, "Range": 93, "Rarity": "Legendary", "Recoil": 60, "Reload": 45, "Season": 12, "Shield Duration": 0, "Source": "deepstonecrypt", "Stability": 33, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 35, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 500, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 43, "Hash": 3075224551, "Holofoil": false, "Id": ""6917529316561158735"", "Impact": 44, "Kill Tracker": 7, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 10, "Masterwork Type": "Charge Time", "Name": "Threaded Needle", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Fluted Barrel*", "Liquid Coils*", "Clown Cartridge*", "Dragonfly*", "Kill Tracker", "Masterworked: Charge Time*", ], "Power": 10, "ROF": 0, "Range": 37, "Rarity": "Legendary", "Recoil": 65, "Reload": 20, "Season": 13, "Shield Duration": 0, "Source": "battlegrounds", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 4, "Zoom": 25, }, { "AA": 89, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 40, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 2121785039, "Holofoil": false, "Id": ""6917529331407605500"", "Impact": 56, "Kill Tracker": 73, "Loadouts": "", "Locked": true, "Mag": 28, "Masterwork Tier": 5, "Masterwork Type": "Range", "Name": "Brass Attacks", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Arrowhead Brake*", "Fluted Barrel", "Appended Mag*", "Light Mag", "Surplus*", "Frenzy*", "Kill Tracker", "Tier 5: Range*", ], "Power": 10, "ROF": 325, "Range": 46, "Rarity": "Legendary", "Recoil": 100, "Reload": 27, "Season": 13, "Shield Duration": 0, "Source": "battlegrounds", "Stability": 38, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 4, "Zoom": 13, }, { "AA": 80, "Accuracy": 28, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 500, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 73, "Hash": 3460122497, "Holofoil": false, "Id": ""6917529333054047193"", "Impact": 68, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Accuracy", "Name": "Imperial Needle", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Elastic String*", "High Tension String", "Fiberglass Arrow Shaft", "Straight Fletching*", "Impulse Amplifier*", "Frenzy*", "Kill Tracker", "Tier 1: Target Acquisition*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 52, "Reload": 60, "Season": 13, "Shield Duration": 0, "Source": "battlegrounds", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 4, "Zoom": 19, }, { "AA": 34, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 54, "Hash": 1561006927, "Holofoil": false, "Id": ""6917529339227194313"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Seventh Seraph Carbine", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Fluted Barrel*", "Polygonal Rifling", "Alloy Magazine", "Flared Magwell*", "Fourth Time's the Charm*", "Vorpal Weapon*", "Kill Tracker", "Tier 3: Range*", ], "Power": 10, "ROF": 450, "Range": 61, "Rarity": "Legendary", "Recoil": 74, "Reload": 56, "Season": 10, "Shield Duration": 0, "Source": "rasputin", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 3, "Zoom": 16, }, { "AA": 37, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 3658188704, "Holofoil": false, "Id": ""6917529339455188926"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 30, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "The Messenger", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Corkscrew Rifling*", "Hammer-Forged Rifling", "Alloy Magazine*", "Appended Mag", "Outlaw*", "Headseeker*", "Kill Tracker", "Counterbalance Stock*", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 340, "Range": 71, "Rarity": "Legendary", "Recoil": 75, "Reload": 41, "Season": 13, "Shield Duration": 0, "Source": "trials", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 4, "Zoom": 18, }, { "AA": 34, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 35, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 55, "Hash": 1097616550, "Holofoil": false, "Id": ""6917529340867495000"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 35, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Extraordinary Rendition", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Polygonal Rifling*", "Smallbore", "Tactical Mag*", "Extended Mag", "Surplus*", "Frenzy*", "Kill Tracker", "Backup Mag*", "Tier 3: Stability*", "Always North*", ], "Power": 10, "ROF": 720, "Range": 42, "Rarity": "Legendary", "Recoil": 97, "Reload": 32, "Season": 13, "Shield Duration": 0, "Source": "battlegrounds", "Stability": 26, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 4, "Zoom": 15, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 30, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 893527433, "Holofoil": false, "Id": ""6917529342154633671"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Far Future", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Polygonal Rifling", "Steady Rounds*", "Flared Magwell", "Quickdraw*", "Opening Shot*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 90, "Range": 48, "Rarity": "Legendary", "Recoil": 52, "Reload": 45, "Season": 13, "Shield Duration": 0, "Source": "battlegrounds", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 4, "Zoom": 45, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 70, "Archetype": "Lightweight Frame", "Blast Radius": 100, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 95, "Hash": 2060863616, "Holofoil": false, "Id": ""6917529344431768493"", "Impact": 0, "Kill Tracker": 673, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 10, "Masterwork Type": "Handling", "Name": "Salvager's Salvo", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Quick Launch*", "Spike Grenades*", "Ambitious Assassin*", "Demolitionist", "Vorpal Weapon", "Chain Reaction*", "Kill Tracker", "Quick Access Sling*", "Masterworked: Handling*", "Panacea*", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Legendary", "Recoil": 75, "Reload": 70, "Season": 13, "Shield Duration": 0, "Source": "gunsmith", "Stability": 36, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 80, "Year": 4, "Zoom": 13, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 47, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 4156253727, "Holofoil": false, "Id": ""6917529349535833875"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 42, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "The Third Axiom", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Hammer-Forged Rifling", "Appended Mag*", "Armor-Piercing Rounds", "Surplus*", "Vorpal Weapon*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 390, "Range": 54, "Rarity": "Legendary", "Recoil": 74, "Reload": 46, "Season": 13, "Shield Duration": 0, "Source": "strikes", "Stability": 61, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 4, "Zoom": 18, }, { "AA": 79, "Accuracy": 47, "Airborne Effectiveness": 23, "Ammo": "primary", "Ammo Generation": 65, "Archetype": "Sacred Flame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 567, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 3260753130, "Holofoil": false, "Id": ""6917529350295797827"", "Impact": 68, "Kill Tracker": 28, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Ticuu's Divination", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Sacred Flame*", "Tactile String*", "Straight Fletching*", "Causality Arrows*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Exotic", "Recoil": 60, "Reload": 60, "Season": 13, "Shield Duration": 0, "Source": "", "Stability": 62, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 4, "Zoom": 18, }, { "AA": 37, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 34, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 3197270240, "Holofoil": false, "Id": ""6917529368011950810"", "Impact": 80, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Found Verdict", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Aggressive Frame*", "Rifled Barrel*", "Barrel Shroud", "Assault Mag*", "Light Mag", "Rewind Rounds*", "Vorpal Weapon*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 60, "Range": 41, "Rarity": "Legendary", "Recoil": 75, "Reload": 36, "Season": 14, "Shield Duration": 0, "Source": "vaultofglass", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 80, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 35, "Hash": 1119734784, "Holofoil": false, "Id": ""6917529368861287826"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 50, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Chroma Rush", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Corkscrew Rifling", "Hammer-Forged Rifling*", "Accurized Rounds*", "Tactical Mag", "Feeding Frenzy*", "Kill Clip*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 720, "Range": 45, "Rarity": "Legendary", "Recoil": 45, "Reload": 48, "Season": 14, "Shield Duration": 0, "Source": "servitor", "Stability": 56, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 38, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 55, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 71, "Hash": 1644680957, "Holofoil": false, "Id": ""6917529387876451858"", "Impact": 55, "Kill Tracker": 307, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Null Composure", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Fluted Barrel*", "Projection Fuse*", "Feeding Frenzy*", "Heating Up", "Reservoir Burst*", "High-Impact Reserves", "Kill Tracker", "Masterworked: Range*", "Hoarfrost Sunrise*", ], "Power": 10, "ROF": 0, "Range": 49, "Rarity": "Legendary", "Recoil": 55, "Reload": 48, "Season": 14, "Shield Duration": 0, "Source": "gunsmith", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 86, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 39, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 2199171672, "Holofoil": false, "Id": ""6917529391080771347"", "Impact": 51, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Lonesome", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Flared Magwell*", "Light Mag", "Outlaw*", "Opening Shot*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 260, "Range": 67, "Rarity": "Legendary", "Recoil": 90, "Reload": 36, "Season": 6, "Shield Duration": 0, "Source": "gambitprime", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 2, "Zoom": 12, }, { "AA": 27, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 40, "Archetype": "Pinpoint Slug Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 70, "Hash": 3616586446, "Holofoil": false, "Id": ""6917529391089338851"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "First In, Last Out", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Pinpoint Slug Frame*", "Arrowhead Brake*", "Chambered Compensator", "Appended Mag*", "Accurized Rounds", "Auto-Loading Holster*", "Demolitionist*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 65, "Range": 58, "Rarity": "Legendary", "Recoil": 88, "Reload": 45, "Season": 11, "Shield Duration": 0, "Source": "contact", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 3, "Zoom": 12, }, { "AA": 79, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 65, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, "Hash": 1946491241, "Holofoil": false, "Id": ""6917529391923015086"", "Impact": 0, "Kill Tracker": 31, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 10, "Masterwork Type": "Blast Radius", "Name": "Truthteller", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Volatile Launch", "Linear Compensator*", "Disorienting Grenades*", "High-Velocity Rounds", "Feeding Frenzy*", "Swashbuckler*", "Kill Tracker", "Masterworked: Blast Radius*", "Hoarfrost Sunrise*", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Legendary", "Recoil": 70, "Reload": 71, "Season": 11, "Shield Duration": 0, "Source": "engram", "Stability": 23, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 79, "Year": 3, "Zoom": 13, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 28, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, "Hash": 541188001, "Holofoil": false, "Id": ""6917529402550685317"", "Impact": 43, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 17, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Farewell", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Appended Mag*", "Alloy Magazine", "Subsistence*", "Vorpal Weapon*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 360, "Range": 23, "Rarity": "Legendary", "Recoil": 96, "Reload": 50, "Season": 14, "Shield Duration": 0, "Source": "servitor", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 35, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 43, "Archetype": "Pinpoint Slug Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 599895591, "Holofoil": false, "Id": ""6917529402585194870"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Sojourner's Tale", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Pinpoint Slug Frame*", "Full Bore*", "Smallbore", "Tactical Mag*", "Accurized Rounds", "Auto-Loading Holster*", "Opening Shot*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 65, "Range": 76, "Rarity": "Legendary", "Recoil": 56, "Reload": 54, "Season": 14, "Shield Duration": 0, "Source": "seasonpass", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 3143732432, "Holofoil": false, "Id": ""6917529410122902050"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 35, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "False Promises", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Corkscrew Rifling*", "Hammer-Forged Rifling", "Appended Mag*", "Light Mag", "Subsistence*", "Rampage*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 360, "Range": 77, "Rarity": "Legendary", "Recoil": 86, "Reload": 39, "Season": 11, "Shield Duration": 0, "Source": "contact", "Stability": 34, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 3, "Zoom": 20, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 59, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 471518543, "Holofoil": false, "Id": ""6917529416218748424"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 62, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Corrective Measure", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Full Bore*", "Hammer-Forged Rifling", "Tactical Mag*", "Alloy Magazine", "Dynamic Sway Reduction*", "Firefly*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 450, "Range": 67, "Rarity": "Legendary", "Recoil": 85, "Reload": 58, "Season": 14, "Shield Duration": 0, "Source": "vaultofglass", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 4, "Zoom": 15, }, { "AA": 37, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 2222560548, "Holofoil": false, "Id": ""6917529416364724234"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "IKELOS_SMG_v1.0.2", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Extended Barrel*", "Smallbore", "Tactical Mag*", "Steady Rounds", "Subsistence*", "Demolitionist*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 720, "Range": 54, "Rarity": "Legendary", "Recoil": 100, "Reload": 34, "Season": 11, "Shield Duration": 0, "Source": "contact", "Stability": 31, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 3, "Zoom": 14, }, { "AA": 81, "Accuracy": 0, "Airborne Effectiveness": 35, "Ammo": "primary", "Ammo Generation": 57, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 1835747805, "Holofoil": false, "Id": ""6917529423153048458"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Nature of the Beast", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "SteadyHand HCS*", "HitMark HCS", "Appended Mag*", "Extended Mag", "Under Pressure*", "Dragonfly*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 180, "Range": 33, "Rarity": "Legendary", "Recoil": 80, "Reload": 25, "Season": 11, "Shield Duration": 0, "Source": "engram", "Stability": 75, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 3, "Zoom": 14, }, { "AA": 80, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 1621558458, "Holofoil": false, "Id": ""6917529431671691143"", "Impact": 23, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 42, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Gridskipper", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Full Bore", "Armor-Piercing Rounds", "Ricochet Rounds*", "Heating Up*", "Frenzy", "Snapshot Sights*", "Kill Tracker", "Backup Mag*", "Tier 4: Range*", ], "Power": 10, "ROF": 540, "Range": 42, "Rarity": "Legendary", "Recoil": 85, "Reload": 35, "Season": 14, "Shield Duration": 0, "Source": "servitor", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 4, "Zoom": 18, }, { "AA": 75, "Accuracy": 30, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 59, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 500, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 70, "Hash": 211938782, "Holofoil": false, "Id": ""6917529431757875836"", "Impact": 68, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 3, "Masterwork Type": "Accuracy", "Name": "Whispering Slab", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Elastic String*", "Polymer String", "Fiberglass Arrow Shaft*", "Natural Fletching", "Archer's Tempo*", "Swashbuckler*", "Kill Tracker", "Tier 3: Target Acquisition*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 55, "Reload": 60, "Season": 11, "Shield Duration": 0, "Source": "contact", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 3, "Zoom": 18, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "heavy", "Ammo Generation": 55, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 82, "Hash": 4230965989, "Holofoil": false, "Id": ""6917529431787619863"", "Impact": 41, "Kill Tracker": 2, "Loadouts": "", "Locked": true, "Mag": 66, "Masterwork Tier": 10, "Masterwork Type": "Handling", "Name": "Commemoration", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake", "Fluted Barrel*", "Appended Mag*", "Steady Rounds", "Feeding Frenzy*", "Dragonfly*", "Kill Tracker", "Masterworked: Handling*", ], "Power": 10, "ROF": 450, "Range": 58, "Rarity": "Legendary", "Recoil": 80, "Reload": 55, "Season": 12, "Shield Duration": 0, "Source": "deepstonecrypt", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 80, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 35, "Hash": 1119734784, "Holofoil": false, "Id": ""6917529436165087992"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 53, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Chroma Rush", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Polygonal Rifling*", "Smallbore", "Appended Mag", "Tactical Mag*", "Subsistence*", "Thresh*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 720, "Range": 25, "Rarity": "Legendary", "Recoil": 45, "Reload": 57, "Season": 14, "Shield Duration": 0, "Source": "servitor", "Stability": 72, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 16, "Ammo": "heavy", "Ammo Generation": 57, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 2434225986, "Holofoil": false, "Id": ""6917529436183238543"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 95, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Shattered Cipher", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Hammer-Forged Rifling*", "Polygonal Rifling", "Extended Mag*", "Steady Rounds", "Field Prep*", "Surrounded*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 900, "Range": 30, "Rarity": "Legendary", "Recoil": 45, "Reload": 45, "Season": 14, "Shield Duration": 0, "Source": "seasonpass", "Stability": 36, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 81, "Accuracy": 37, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 70, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 560, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 70, "Hash": 720351795, "Holofoil": false, "Id": ""6917529441312170017"", "Impact": 68, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 2, "Masterwork Type": "Draw Time", "Name": "Arsenic Bite-4b", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Agile Bowstring", "Tactile String*", "Compact Arrow Shaft*", "Helical Fletching", "Dragonfly*", "Archer's Tempo*", "Kill Tracker", "Icarus Grip*", "Tier 2: Draw Time*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 51, "Reload": 70, "Season": 4, "Shield Duration": 0, "Source": "engram", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 2, "Zoom": 18, }, { "AA": 73, "Accuracy": 73, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 667, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 70, "Hash": 3472875143, "Holofoil": false, "Id": ""6917529459355747350"", "Impact": 76, "Kill Tracker": 16, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 10, "Masterwork Type": "Accuracy", "Name": "Wolftone Draw", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Agile Bowstring*", "Tactile String", "Carbon Arrow Shaft", "Natural Fletching*", "Impulse Amplifier*", "Demolitionist*", "Kill Tracker", "Targeting Adjuster*", "Masterworked: Target Acquisition*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 73, "Reload": 40, "Season": 15, "Shield Duration": 0, "Source": "lost", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 4, "Zoom": 18, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 43, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 829330711, "Holofoil": false, "Id": ""6917529520909811363"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 48, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Peacebond", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Burst*", "Full Bore", "Polygonal Rifling*", "Accurized Rounds", "Flared Magwell*", "Subsistence*", "Swashbuckler*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 491, "Range": 33, "Rarity": "Legendary", "Recoil": 92, "Reload": 44, "Season": 15, "Shield Duration": 0, "Source": "", "Stability": 95, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 1337707096, "Holofoil": false, "Id": ""6917529534017719680"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 31, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Chrysura Melo", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Arrowhead Brake*", "Hammer-Forged Rifling", "Accurized Rounds*", "Steady Rounds", "Fourth Time's the Charm*", "Frenzy*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 360, "Range": 83, "Rarity": "Legendary", "Recoil": 100, "Reload": 30, "Season": 15, "Shield Duration": 0, "Source": "seasonpass", "Stability": 27, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 60, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 49, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 520, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 2481881293, "Holofoil": false, "Id": ""6917529534780231146"", "Impact": 50, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 4, "Masterwork Type": "Charge Time", "Name": "Cartesian Coordinate", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Cleanshot IS*", "Hitmark IS", "Accelerated Coils*", "Liquid Coils", "Feeding Frenzy*", "High-Impact Reserves*", "Kill Tracker", "Tier 4: Charge Time*", ], "Power": 10, "ROF": 0, "Range": 26, "Rarity": "Legendary", "Recoil": 52, "Reload": 46, "Season": 13, "Shield Duration": 0, "Source": "engram", "Stability": 26, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 77, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 45, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 41, "Hash": 3184681056, "Holofoil": false, "Id": ""6917529534780231590"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Fractethyst", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Rifled Barrel", "Smallbore*", "Assault Mag*", "Tactical Mag", "Quickdraw*", "Opening Shot*", "Kill Tracker", "Targeting Adjuster*", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 70, "Range": 69, "Rarity": "Legendary", "Recoil": 75, "Reload": 53, "Season": 15, "Shield Duration": 0, "Source": "seasonpass", "Stability": 67, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 49, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 26, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 1679868061, "Holofoil": false, "Id": ""6917529547585798743"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Wastelander M5", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Smoothbore*", "Barrel Shroud", "Assault Mag*", "Extended Mag", "Dual Loader*", "Adagio*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 90, "Range": 59, "Rarity": "Legendary", "Recoil": 51, "Reload": 62, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 23, "Archetype": "Adaptive Frame", "Blast Radius": 50, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 57, "Hash": 3165547384, "Holofoil": false, "Id": ""6917529575758316990"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Memory Interdict", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Adaptive Frame*", "Quick Launch*", "Smart Drift Control", "Spike Grenades*", "Sticky Grenades", "Clown Cartridge*", "Chain Reaction*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 71, "Reload": 44, "Season": 14, "Shield Duration": 0, "Source": "engram", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 38, "Year": 4, "Zoom": 13, }, { "AA": 79, "Accuracy": 0, "Airborne Effectiveness": 47, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 3281285075, "Holofoil": false, "Id": ""6917529575758321385"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 15, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Posterity", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Hammer-Forged Rifling", "Appended Mag*", "Extended Mag", "Fourth Time's the Charm*", "One for All*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 180, "Range": 39, "Rarity": "Legendary", "Recoil": 100, "Reload": 59, "Season": 12, "Shield Duration": 0, "Source": "deepstonecrypt", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 4, "Zoom": 14, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 30, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 40, "Guard Resistance": 10, "Handling": 0, "Hash": 2782325300, "Holofoil": false, "Id": ""6917529600527551469"", "Impact": 61, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 88, "Masterwork Tier": 2, "Masterwork Type": "Impact", "Name": "Quickfang", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Hungry Edge*", "Honed Edge", "Tempered Edge", "Swordmaster's Guard*", "Relentless Strikes*", "Assassin's Blade*", "Kill Tracker", "Tier 2: Impact*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 14, "Shield Duration": 0, "Source": "engram", "Stability": 0, "Swing Speed": 80, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 4, "Zoom": 0, }, { "AA": 79, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 432476743, "Holofoil": false, "Id": ""6917529600549830121"", "Impact": 84, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 10, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "The Palindrome", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Full Bore", "Smallbore*", "Appended Mag", "Ricochet Rounds*", "Killing Wind*", "Rampage*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 140, "Range": 62, "Rarity": "Legendary", "Recoil": 98, "Reload": 42, "Season": 13, "Shield Duration": 0, "Source": "nightfall", "Stability": 78, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 4, "Zoom": 14, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 77, "Hash": 602618796, "Holofoil": false, "Id": ""6917529610420022236"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 49, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Scathelocke", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Smallbore", "Extended Mag*", "Flared Magwell", "Surplus*", "Rampage*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 600, "Range": 44, "Rarity": "Legendary", "Recoil": 60, "Reload": 36, "Season": 15, "Shield Duration": 0, "Source": "engram", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 4, "Zoom": 20, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 27, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 65, "Hash": 2588048270, "Holofoil": false, "Id": ""6917529610431462699"", "Impact": 43, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Spoiler Alert", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Tactic SAS*", "FarPoint SAS", "Appended Mag*", "Ricochet Rounds", "Surplus*", "Swashbuckler*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 360, "Range": 23, "Rarity": "Legendary", "Recoil": 87, "Reload": 51, "Season": 15, "Shield Duration": 0, "Source": "engram", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 42, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 21, "Hash": 1622998472, "Holofoil": false, "Id": ""6917529611327211442"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Vulpecula", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Hammer-Forged Rifling", "Steady Rounds*", "Flared Magwell", "Outlaw*", "Multikill Clip*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 180, "Range": 37, "Rarity": "Legendary", "Recoil": 100, "Reload": 52, "Season": 15, "Shield Duration": 0, "Source": "lost", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 4, "Zoom": 14, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 30, "Hash": 1337707096, "Holofoil": false, "Id": ""6917529613963100610"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Chrysura Melo", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Chambered Compensator", "Polygonal Rifling*", "Appended Mag", "Tactical Mag*", "Dynamic Sway Reduction", "Outlaw*", "Frenzy*", "Harmony", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 360, "Range": 73, "Rarity": "Legendary", "Recoil": 86, "Reload": 43, "Season": 15, "Shield Duration": 0, "Source": "seasonpass", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 76, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 50, "Archetype": "Wolfpack Rounds", "Blast Radius": 100, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 76, "Hash": 1363886209, "Holofoil": false, "Id": ""6917529622055176263"", "Impact": 0, "Kill Tracker": 357, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Gjallarhorn", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Wolfpack Rounds*", "Volatile Launch*", "Alloy Casing*", "Pack Hunter*", "Short-Action Stock*", "Kill Tracker", "Hraesveglur*", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Exotic", "Recoil": 49, "Reload": 67, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 54, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 40, "Year": 4, "Zoom": 20, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 43, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 1572896086, "Holofoil": false, "Id": ""6917529651985770277"", "Impact": 25, "Kill Tracker": 16, "Loadouts": "", "Locked": true, "Mag": 81, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Recurrent Impact", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Smallbore*", "Tactical Mag*", "Perpetual Motion*", "Headstone*", "Land Tank*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 900, "Range": 35, "Rarity": "Legendary", "Recoil": 53, "Reload": 72, "Season": 16, "Shield Duration": 0, "Source": "psiops", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 5, "Zoom": 17, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 43, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 1572896086, "Holofoil": false, "Id": ""6917529652222016768"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 74, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Recurrent Impact", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Chambered Compensator", "Accurized Rounds*", "Flared Magwell", "Subsistence*", "Frenzy*", "Land Tank*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 900, "Range": 41, "Rarity": "Legendary", "Recoil": 83, "Reload": 62, "Season": 16, "Shield Duration": 0, "Source": "psiops", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 5, "Zoom": 17, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "special", "Ammo Generation": 45, "Archetype": "Adaptive Glaive", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 7, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 2595497736, "Holofoil": false, "Id": ""6917529669850359950"", "Impact": 80, "Kill Tracker": 500, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 10, "Masterwork Type": "Handling", "Name": "The Enigma", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Glaive*", "Tempered Truss Rod*", "Alloy Magazine*", "Tilting at Windmills*", "Kill Clip*", "Psychohack*", "Kill Tracker", ], "Power": 10, "ROF": 55, "Range": 50, "Rarity": "Legendary", "Recoil": 0, "Reload": 35, "Season": 16, "Shield Duration": 45, "Source": "campaign", "Stability": 0, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Glaive", "Velocity": 0, "Year": 5, "Zoom": 0, }, { "AA": 74, "Accuracy": 0, "Airborne Effectiveness": 35, "Ammo": "primary", "Ammo Generation": 47, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 310708513, "Holofoil": false, "Id": ""6917529673316805664"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Survivor's Epitaph", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Alloy Magazine*", "High-Caliber Rounds", "Rapid Hit*", "Kill Clip*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 180, "Range": 32, "Rarity": "Legendary", "Recoil": 93, "Reload": 45, "Season": 14, "Shield Duration": 0, "Source": "crucible", "Stability": 67, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 4, "Zoom": 14, }, { "AA": 36, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 31, "Hash": 1019000888, "Holofoil": false, "Id": ""6917529674975940136"", "Impact": 67, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 13, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Perses-D", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Hammer-Forged Rifling*", "Polygonal Rifling", "Accurized Rounds", "Flared Magwell*", "Rapid Hit*", "Headstone*", "Häkke Breach Armaments*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 150, "Range": 78, "Rarity": "Legendary", "Recoil": 74, "Reload": 48, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 31, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 5, "Zoom": 22, }, { "AA": 86, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 30, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 24, "Hash": 2607304614, "Holofoil": false, "Id": ""6917529683473657223"", "Impact": 56, "Kill Tracker": 18, "Loadouts": "", "Locked": true, "Mag": 28, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Empirical Evidence", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Extended Barrel*", "Appended Mag*", "Ensemble*", "Unrelenting*", "Psychohack*", "Kill Tracker", ], "Power": 10, "ROF": 325, "Range": 62, "Rarity": "Legendary", "Recoil": 100, "Reload": 24, "Season": 16, "Shield Duration": 0, "Source": "campaign", "Stability": 33, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 25, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 71, "Hash": 108221785, "Holofoil": false, "Id": ""6917529746534444914"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Riiswalker", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Smallbore*", "Full Choke", "Appended Mag*", "Accurized Rounds", "Hip-Fire Grip*", "Iron Reach*", "Skulking Wolf*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 80, "Range": 72, "Rarity": "Legendary", "Recoil": 54, "Reload": 63, "Season": 14, "Shield Duration": 0, "Source": "ironbanner", "Stability": 30, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 27, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "heavy", "Ammo Generation": 41, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 48, "Hash": 1967303408, "Holofoil": false, "Id": ""6917529746540305559"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 52, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Archon's Thunder", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Fluted Barrel*", "Smallbore", "Accurized Rounds", "Appended Mag*", "Surplus*", "Thresh*", "Skulking Wolf*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 360, "Range": 64, "Rarity": "Legendary", "Recoil": 76, "Reload": 33, "Season": 14, "Shield Duration": 0, "Source": "ironbanner", "Stability": 23, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 83, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 46, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 409551876, "Holofoil": false, "Id": ""6917529747629035196"", "Impact": 49, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "The Keening", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Hammer-Forged Rifling*", "Smallbore", "High-Caliber Rounds*", "Ricochet Rounds", "Subsistence*", "Multikill Clip*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 300, "Range": 53, "Rarity": "Legendary", "Recoil": 99, "Reload": 25, "Season": 13, "Shield Duration": 0, "Source": "crucible", "Stability": 61, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 48, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 55, "Hash": 4067556514, "Holofoil": false, "Id": ""6917529747633508985"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 4, "Masterwork Type": "Reload Speed", "Name": "Thoughtless", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Extended Barrel", "Appended Mag*", "Flared Magwell", "Overflow*", "Firing Line*", "Land Tank*", "Kill Tracker", "Tier 4: Reload Speed*", ], "Power": 10, "ROF": 90, "Range": 51, "Rarity": "Legendary", "Recoil": 100, "Reload": 48, "Season": 16, "Shield Duration": 0, "Source": "psiops", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 5, "Zoom": 46, }, { "AA": 32, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 34, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "Guardian Games", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 48, "Hash": 294129361, "Holofoil": false, "Id": ""6917529747665028105"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 32, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "The Title", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Polygonal Rifling", "Smallbore*", "High-Caliber Rounds*", "Light Mag", "Threat Detector*", "Swashbuckler*", "Classy Contender*", "Häkke Breach Armaments", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 720, "Range": 53, "Rarity": "Legendary", "Recoil": 85, "Reload": 23, "Season": 16, "Shield Duration": 0, "Source": "events", "Stability": 27, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 73, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 36, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 4184808992, "Holofoil": false, "Id": ""6917529747694957903"", "Impact": 70, "Kill Tracker": 22, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 10, "Masterwork Type": "Stability", "Name": "Adored", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Hammer-Forged Rifling*", "Accurized Rounds*", "Triple Tap*", "Killing Wind", "Vorpal Weapon*", "Snapshot Sights", "Kill Tracker", "Targeting Adjuster*", "Masterworked: Stability*", ], "Power": 10, "ROF": 90, "Range": 70, "Rarity": "Legendary", "Recoil": 77, "Reload": 38, "Season": 12, "Shield Duration": 0, "Source": "ritual-weapon", "Stability": 56, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 4, "Zoom": 45, }, { "AA": 50, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 26, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 75, "Hash": 3216652511, "Holofoil": false, "Id": ""6917529747781372395"", "Impact": 65, "Kill Tracker": 144, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 10, "Masterwork Type": "Handling", "Name": "Reckless Endangerment", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Full Choke*", "Assault Mag*", "Perpetual Motion*", "Steady Hands", "Swashbuckler*", "Snapshot Sights", "Kill Tracker", "Vanguard's Vindication*", "One Quiet Moment", "Gun and Run", "Masterworked: Handling*", ], "Power": 10, "ROF": 90, "Range": 42, "Rarity": "Legendary", "Recoil": 60, "Reload": 59, "Season": 16, "Shield Duration": 0, "Source": "crucible", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 44, "Ammo": "primary", "Ammo Generation": 48, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 1141547457, "Holofoil": false, "Id": ""6917529749713763110"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Frontier's Cry", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Full Bore*", "Smallbore", "Accurized Rounds*", "Tactical Mag", "Stats for All*", "Iron Grip*", "Skulking Wolf*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 180, "Range": 67, "Rarity": "Legendary", "Recoil": 94, "Reload": 29, "Season": 16, "Shield Duration": 0, "Source": "ironbanner", "Stability": 69, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 54, "Ammo": "primary", "Ammo Generation": 48, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 1141547457, "Holofoil": false, "Id": ""6917529749719173078"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Frontier's Cry", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Precision Frame*", "Full Bore*", "Hammer-Forged Rifling", "Extended Mag*", "Alloy Magazine", "Stats for All*", "One for All*", "Skulking Wolf*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 180, "Range": 57, "Rarity": "Legendary", "Recoil": 94, "Reload": 28, "Season": 16, "Shield Duration": 0, "Source": "ironbanner", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 25, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 73, "Hash": 108221785, "Holofoil": false, "Id": ""6917529749724070797"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Riiswalker", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Smoothbore*", "Full Choke", "Steady Rounds*", "Accurized Rounds", "Surplus*", "Swashbuckler*", "Skulking Wolf*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 80, "Range": 55, "Rarity": "Legendary", "Recoil": 54, "Reload": 62, "Season": 14, "Shield Duration": 0, "Source": "ironbanner", "Stability": 58, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 43, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 829330711, "Holofoil": false, "Id": ""6917529749728671499"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 54, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Peacebond", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Burst*", "Arrowhead Brake*", "Smallbore", "Appended Mag*", "Tactical Mag", "Subsistence*", "Surrounded*", "Skulking Wolf*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 491, "Range": 33, "Rarity": "Legendary", "Recoil": 100, "Reload": 29, "Season": 15, "Shield Duration": 0, "Source": "", "Stability": 80, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 32, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 34, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "Guardian Games", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 43, "Hash": 294129361, "Holofoil": false, "Id": ""6917529757351548250"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 32, "Masterwork Tier": 10, "Masterwork Type": "Stability", "Name": "The Title", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Chambered Compensator*", "Polygonal Rifling", "Armor-Piercing Rounds*", "Light Mag", "Steady Hands*", "Dynamic Sway Reduction", "Vorpal Weapon*", "Swashbuckler", "Classy Contender*", "Häkke Breach Armaments", "Kill Tracker", "Masterworked: Stability*", ], "Power": 10, "ROF": 720, "Range": 45, "Rarity": "Legendary", "Recoil": 95, "Reload": 23, "Season": 16, "Shield Duration": 0, "Source": "events", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 33, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Together Forever", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 88, "Hash": 502356570, "Holofoil": false, "Id": ""6917529760306892403"", "Impact": 49, "Kill Tracker": 20, "Loadouts": "", "Locked": true, "Mag": 20, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Drang (Baroque)", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Together Forever*", "Fluted Barrel*", "Polygonal Rifling", "Extended Mag*", "Flared Magwell", "Wellspring*", "Swashbuckler*", "To Excess*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 300, "Range": 35, "Rarity": "Legendary", "Recoil": 80, "Reload": 10, "Season": 17, "Shield Duration": 0, "Source": "haunted", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 5, "Zoom": 13, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 28, "Hash": 1019000888, "Holofoil": false, "Id": ""6917529770763902268"", "Impact": 67, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 13, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Perses-D", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Chambered Compensator*", "Full Bore", "Steady Rounds", "Flared Magwell*", "Stats for All*", "One for All*", "Häkke Breach Armaments*", "Kill Tracker", "Targeting Adjuster*", "Tier 2: Handling*", ], "Power": 10, "ROF": 150, "Range": 68, "Rarity": "Legendary", "Recoil": 84, "Reload": 48, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 5, "Zoom": 22, }, { "AA": 54, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 40, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 667, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 31, "Hash": 3258665412, "Holofoil": false, "Id": ""6917529770769142344"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Trinary System", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Extended Barrel", "Liquid Coils*", "Projection Fuse", "Surplus*", "Disruption Break*", "Gun and Run*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 65, "Reload": 39, "Season": 13, "Shield Duration": 0, "Source": "gambit", "Stability": 53, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 4, "Zoom": 15, }, { "AA": 31, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 66, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 2715240478, "Holofoil": false, "Id": ""6917529772604856034"", "Impact": 55, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "Riptide", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Chambered Compensator*", "Polygonal Rifling", "Enhanced Battery*", "Projection Fuse", "Field Prep*", "Chill Clip*", "One Quiet Moment*", "Kill Tracker", "Tier 4: Handling*", ], "Power": 10, "ROF": 0, "Range": 21, "Rarity": "Legendary", "Recoil": 63, "Reload": 45, "Season": 17, "Shield Duration": 0, "Source": "crucible", "Stability": 41, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 39, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 800, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 768621510, "Holofoil": false, "Id": ""6917529775414096701"", "Impact": 80, "Kill Tracker": 111, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Deliverance", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Full Bore", "Smallbore*", "Liquid Coils", "Projection Fuse*", "Perpetual Motion*", "Successful Warm-Up*", "Souldrinker*", "Kill Tracker", "Tier 1: Handling*", "Queensguard Valor*", ], "Power": 10, "ROF": 0, "Range": 79, "Rarity": "Legendary", "Recoil": 80, "Reload": 34, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 27, "Ammo": "primary", "Ammo Generation": 30, "Archetype": "Screaming Swarm", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 32, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 26, "Hash": 46524085, "Holofoil": false, "Id": ""6917529775632556585"", "Impact": 25, "Kill Tracker": 3157, "Loadouts": "", "Locked": true, "Mag": 29, "Masterwork Tier": 10, "Masterwork Type": undefined, "Name": "Osteo Striga", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Screaming Swarm*", "Polygonal Rifling*", "Accurized Rounds*", "Toxic Overload*", "Hand-Laid Stock*", "Kill Tracker", "Osteo Striga Catalyst*", ], "Power": 10, "ROF": 600, "Range": 90, "Rarity": "Exotic", "Recoil": 99, "Reload": 48, "Season": 16, "Shield Duration": 0, "Source": "exoticquest", "Stability": 77, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 13, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 3385326721, "Holofoil": false, "Id": ""6917529777001908779"", "Impact": 18, "Kill Tracker": 345, "Loadouts": "", "Locked": true, "Mag": 50, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Reckless Oracle", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Chambered Compensator*", "Full Bore", "High-Caliber Rounds", "Ricochet Rounds*", "Outlaw*", "Kill Clip*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 720, "Range": 31, "Rarity": "Legendary", "Recoil": 69, "Reload": 55, "Season": 8, "Shield Duration": 0, "Source": "gardenofsalvation", "Stability": 78, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 3, "Zoom": 16, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "special", "Ammo Generation": 45, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 57, "Hash": 2233545123, "Holofoil": false, "Id": ""6917529778466121226"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Fugue-55", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Hammer-Forged Rifling", "Extended Mag*", "Steady Rounds", "Lead from Gold*", "Firing Line*", "Suros Synergy*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 90, "Range": 54, "Rarity": "Legendary", "Recoil": 100, "Reload": 27, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 5, "Zoom": 50, }, { "AA": 49, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 26, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 1679868061, "Holofoil": false, "Id": ""6917529786068896442"", "Impact": 65, "Kill Tracker": 23, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Wastelander M5", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Smallbore*", "Full Choke", "Assault Mag*", "Accurized Rounds", "Lead from Gold*", "One-Two Punch*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 90, "Range": 53, "Rarity": "Legendary", "Recoil": 51, "Reload": 59, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 62, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 73, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 80, "Hash": 2342054803, "Holofoil": false, "Id": ""6917529786074406161"", "Impact": 27, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 30, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Ogma PR6", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Polygonal Rifling", "Accurized Rounds*", "Extended Mag", "Stats for All*", "One for All*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 450, "Range": 52, "Rarity": "Legendary", "Recoil": 85, "Reload": 55, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 19, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "special", "Ammo Generation": 50, "Archetype": "Adaptive Glaive", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 43, "Hash": 254636484, "Holofoil": false, "Id": ""6917529786991292741"", "Impact": 80, "Kill Tracker": 502, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Nezarec's Whisper", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Glaive*", "Ballistic Tuning*", "Low-Impedance Windings", "Extended Mag*", "Accurized Rounds", "Lead from Gold*", "Frenzy*", "Extrovert*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 55, "Range": 65, "Rarity": "Legendary", "Recoil": 0, "Reload": 5, "Season": 17, "Shield Duration": 32, "Source": "haunted", "Stability": 0, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Glaive", "Velocity": 0, "Year": 5, "Zoom": 0, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 9, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 2856514843, "Holofoil": false, "Id": ""6917529788374004947"", "Impact": 29, "Kill Tracker": 512, "Loadouts": "", "Locked": true, "Mag": 42, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Syncopation-53", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Appended Mag*", "Moving Target*", "Headstone*", "Suros Synergy*", "Kill Tracker", ], "Power": 10, "ROF": 390, "Range": 56, "Rarity": "Legendary", "Recoil": 89, "Reload": 42, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 19, }, { "AA": 38, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "special", "Ammo Generation": 37, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 4, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 30, "Hash": 4225322581, "Holofoil": false, "Id": ""6917529788385108591"", "Impact": 80, "Kill Tracker": 33, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Ragnhild-D", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Aggressive Frame*", "Smallbore*", "Steady Rounds*", "Subsistence*", "Demolitionist*", "Häkke Breach Armaments*", "Kill Tracker", ], "Power": 10, "ROF": 55, "Range": 30, "Rarity": "Legendary", "Recoil": 64, "Reload": 33, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 100, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 65, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 2323544076, "Holofoil": false, "Id": ""6917529789537942791"", "Impact": 6, "Kill Tracker": 77, "Loadouts": "", "Locked": true, "Mag": 74, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Hollow Denial", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Hammer-Forged Rifling", "Enhanced Battery*", "Tactical Battery", "Adaptive Munitions*", "Dragonfly*", "Extrovert*", "Kill Tracker", "Tier 2: Stability*", "Mind's Eye*", ], "Power": 10, "ROF": 1000, "Range": 71, "Rarity": "Legendary", "Recoil": 99, "Reload": 46, "Season": 17, "Shield Duration": 0, "Source": "haunted", "Stability": 83, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Unrepentant", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 1234150730, "Holofoil": false, "Id": ""6917529789552549322"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 48, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Trespasser", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Unrepentant*", "Full Bore*", "Ricochet Rounds*", "Be the Danger*", "Smooth Grip*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 491, "Range": 59, "Rarity": "Exotic", "Recoil": 100, "Reload": 33, "Season": 17, "Shield Duration": 0, "Source": "seasonpass", "Stability": 90, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 43, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 829330711, "Holofoil": false, "Id": ""6917529789557461465"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 48, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Peacebond", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Burst*", "Extended Barrel*", "Smallbore", "Accurized Rounds*", "Alloy Magazine", "Subsistence*", "Headstone*", "Skulking Wolf*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 491, "Range": 53, "Rarity": "Legendary", "Recoil": 100, "Reload": 32, "Season": 15, "Shield Duration": 0, "Source": "", "Stability": 79, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 60, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 56, "Hash": 1366394399, "Holofoil": false, "Id": ""6917529792324920991"", "Impact": 62, "Kill Tracker": 6, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Tears of Contrition", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Fluted Barrel", "Steady Rounds*", "Flared Magwell", "Triple Tap*", "Explosive Payload*", "Extrovert*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 180, "Range": 42, "Rarity": "Legendary", "Recoil": 100, "Reload": 40, "Season": 17, "Shield Duration": 0, "Source": "haunted", "Stability": 64, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 5, "Zoom": 19, }, { "AA": 82, "Accuracy": 42, "Airborne Effectiveness": 7, "Ammo": "primary", "Ammo Generation": 64, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 567, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 72, "Hash": 2513965917, "Holofoil": false, "Id": ""6917529805387289281"", "Impact": 68, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 2, "Masterwork Type": "Accuracy", "Name": "Lunulata-4b", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Tactile String", "Natural String*", "Helical Fletching", "Straight Fletching*", "No Distractions*", "Successful Warm-Up*", "Veist Stinger*", "Kill Tracker", "Tier 2: Target Acquisition*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 49, "Reload": 60, "Season": 17, "Shield Duration": 0, "Source": "engram", "Stability": 64, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 90, "Accuracy": 0, "Airborne Effectiveness": 26, "Ammo": "heavy", "Ammo Generation": 30, "Archetype": "Reign Havoc", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 3325463374, "Holofoil": false, "Id": ""6917529811843684090"", "Impact": 41, "Kill Tracker": 640, "Loadouts": "", "Locked": true, "Mag": 62, "Masterwork Tier": 10, "Masterwork Type": undefined, "Name": "Thunderlord", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Reign Havoc*", "Polygonal Rifling*", "Armor-Piercing Rounds*", "Lightning Rounds*", "Feeding Frenzy*", "Kill Tracker", "Thunderlord Catalyst*", ], "Power": 10, "ROF": 450, "Range": 65, "Rarity": "Exotic", "Recoil": 70, "Reload": 68, "Season": 4, "Shield Duration": 0, "Source": "", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 2, "Zoom": 15, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 49, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 3738678140, "Holofoil": false, "Id": ""6917529811851364588"", "Impact": 65, "Kill Tracker": 19, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Dead Weight", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Barrel Shroud*", "Corkscrew Rifling", "Appended Mag*", "Accurized Rounds", "Auto-Loading Holster*", "Golden Tricorn*", "Gun and Run*", "Kill Tracker", "Tier 3: Range*", ], "Power": 10, "ROF": 140, "Range": 31, "Rarity": "Legendary", "Recoil": 70, "Reload": 64, "Season": 17, "Shield Duration": 0, "Source": "gambit", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 35, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "special", "Ammo Generation": 55, "Archetype": "Traitor's Vessel", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 86, "Hash": 374573733, "Holofoil": false, "Id": ""6917529812120308548"", "Impact": 60, "Kill Tracker": 509, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Delicate Tomb", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Traitor's Vessel*", "Fluted Barrel*", "Liquid Coils*", "Tempest Cascade*", "Short-Action Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 0, "Range": 29, "Rarity": "Exotic", "Recoil": 95, "Reload": 48, "Season": 18, "Shield Duration": 0, "Source": "", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 45, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 40, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 793, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "fotc", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 41, "Hash": 253196586, "Holofoil": false, "Id": ""6917529812164773330"", "Impact": 80, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 2, "Masterwork Type": "Charge Time", "Name": "Main Ingredient", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Cleanshot IS*", "Hitmark IS", "Red Dot Micro", "Enhanced Battery*", "Particle Repeater", "Auto-Loading Holster*", "Rampage*", "Kill Tracker", "Tier 2: Charge Time*", ], "Power": 10, "ROF": 0, "Range": 64, "Rarity": "Legendary", "Recoil": 73, "Reload": 24, "Season": 4, "Shield Duration": 0, "Source": "strikes", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 2, "Zoom": 16, }, { "AA": 83, "Accuracy": 0, "Airborne Effectiveness": 26, "Ammo": "primary", "Ammo Generation": 59, "Archetype": "Missile Tracers", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 4293613902, "Holofoil": false, "Id": ""6917529812274003194"", "Impact": 18, "Kill Tracker": 5232, "Loadouts": "", "Locked": true, "Mag": 50, "Masterwork Tier": 10, "Masterwork Type": undefined, "Name": "Quicksilver Storm", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Missile Tracers*", "Corkscrew Rifling*", "High-Caliber Rounds*", "Grenade Chaser*", "Hand-Laid Stock*", "Kill Tracker", "Quicksilver Storm Catalyst*", ], "Power": 10, "ROF": 720, "Range": 50, "Rarity": "Exotic", "Recoil": 88, "Reload": 54, "Season": 18, "Shield Duration": 0, "Source": "deluxe", "Stability": 100, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 32, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 21, "Hash": 1780464822, "Holofoil": false, "Id": ""6917529812351112530"", "Impact": 33, "Kill Tracker": 25, "Loadouts": "", "Locked": true, "Mag": 27, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "New Purpose", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Full Bore*", "Hammer-Forged Rifling", "Alloy Magazine*", "Ricochet Rounds", "Heating Up*", "Desperado*", "Bitterspite*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 340, "Range": 82, "Rarity": "Legendary", "Recoil": 70, "Reload": 31, "Season": 17, "Shield Duration": 0, "Source": "duality", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 85, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 235827225, "Holofoil": false, "Id": ""6917529813191493346"", "Impact": 84, "Kill Tracker": 89, "Loadouts": "", "Locked": true, "Mag": 14, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Eyasluna", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "SteadyHand HCS*", "HitMark HCS", "Appended Mag", "High-Caliber Rounds*", "Outlaw*", "Kill Clip*", "Kill Tracker", "Backup Mag*", "Masterworked: Range*", ], "Power": 10, "ROF": 140, "Range": 66, "Rarity": "Legendary", "Recoil": 96, "Reload": 45, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 74, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 4, "Zoom": 14, }, { "AA": 82, "Accuracy": 25, "Airborne Effectiveness": 7, "Ammo": "primary", "Ammo Generation": 64, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 500, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 72, "Hash": 2513965917, "Holofoil": false, "Id": ""6917529820991284645"", "Impact": 68, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Lunulata-4b", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Agile Bowstring", "Elastic String*", "Natural Fletching", "Straight Fletching*", "No Distractions*", "Headstone*", "Veist Stinger*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 49, "Reload": 60, "Season": 17, "Shield Duration": 0, "Source": "engram", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "special", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 26, "Hash": 1280894514, "Holofoil": false, "Id": ""6917529835303788349"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 3, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Mechabre", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Smallbore*", "Accurized Rounds*", "Snapshot Sights*", "Opening Shot*", "Search Party*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 72, "Range": 92, "Rarity": "Legendary", "Recoil": 77, "Reload": 30, "Season": 18, "Shield Duration": 0, "Source": "events", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 5, "Zoom": 45, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 59, "Archetype": "Aggressive Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 3428521585, "Holofoil": false, "Id": ""6917529835455063219"", "Impact": 35, "Kill Tracker": 90, "Loadouts": "", "Locked": true, "Mag": 40, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Insidious", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Burst*", "Fluted Barrel*", "Polygonal Rifling", "Tactical Mag*", "Flared Magwell", "Dragonfly*", "Adaptive Munitions*", "Souldrinker*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 450, "Range": 74, "Rarity": "Legendary", "Recoil": 78, "Reload": 45, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 70, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 34, "Ammo": "primary", "Ammo Generation": 59, "Archetype": "Aggressive Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 3428521585, "Holofoil": false, "Id": ""6917529835466227632"", "Impact": 35, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 48, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Insidious", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Burst*", "Corkscrew Rifling*", "Full Bore", "Extended Mag*", "Alloy Magazine", "Demolitionist*", "Adrenaline Junkie*", "Souldrinker*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 450, "Range": 79, "Rarity": "Legendary", "Recoil": 78, "Reload": 14, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 59, "Archetype": "Aggressive Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 20, "Hash": 3428521585, "Holofoil": false, "Id": ""6917529835478615189"", "Impact": 35, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 36, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Insidious", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Burst*", "Extended Barrel*", "Fluted Barrel", "Alloy Magazine*", "Flared Magwell", "Rapid Hit*", "Rampage*", "Souldrinker*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 450, "Range": 84, "Rarity": "Legendary", "Recoil": 88, "Reload": 34, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 79, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 67, "Archetype": "Wave Frame", "Blast Radius": 61, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 70, "Hash": 613334176, "Holofoil": false, "Id": ""6917529835478618130"", "Impact": 0, "Kill Tracker": 125, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 1, "Masterwork Type": "Blast Radius", "Name": "Forbearance", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Wave Frame*", "Confined Launch*", "Linear Compensator", "High-Velocity Rounds*", "Implosion Rounds", "Surplus*", "Rampage*", "Souldrinker*", "Kill Tracker", "Tier 1: Blast Radius*", ], "Power": 10, "ROF": 72, "Range": 0, "Rarity": "Legendary", "Recoil": 73, "Reload": 81, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 74, "Year": 5, "Zoom": 13, }, { "AA": 78, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": true, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 2852052802, "Holofoil": false, "Id": ""6917529842979650617"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 53, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Krait", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Rapid-Fire Frame*", "Corkscrew Rifling*", "Smallbore", "Armor-Piercing Rounds*", "Ricochet Rounds", "Subsistence*", "Headstone*", "Veist Stinger*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 720, "Range": 43, "Rarity": "Legendary", "Recoil": 56, "Reload": 56, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 63, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "fotc", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 2742838700, "Holofoil": false, "Id": ""6917529844798000250"", "Impact": 92, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 10, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "True Prophecy", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "SteadyHand HCS*", "Crossfire HCS", "Tactical Mag*", "Steady Rounds", "Rangefinder*", "Demolitionist*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 120, "Range": 58, "Rarity": "Legendary", "Recoil": 93, "Reload": 34, "Season": 10, "Shield Duration": 0, "Source": "engram", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 3, "Zoom": 14, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "special", "Ammo Generation": 38, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 800, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 4114929480, "Holofoil": false, "Id": ""6917529848793845847"", "Impact": 80, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Snorri FR5", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Smallbore", "Enhanced Battery*", "Ionized Battery", "Surplus*", "Reservoir Burst*", "Omolon Fluid Dynamics*", "Kill Tracker", "Counterbalance Stock*", "Masterworked: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 66, "Rarity": "Legendary", "Recoil": 94, "Reload": 43, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 17, }, { "AA": 38, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "special", "Ammo Generation": 41, "Archetype": "Pinpoint Slug Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 2, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 55, "Hash": 2531963421, "Holofoil": false, "Id": ""6917529848793849238"", "Impact": 70, "Kill Tracker": 41, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "No Reprieve", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Pinpoint Slug Frame*", "Extended Barrel*", "Extended Mag*", "Feeding Frenzy*", "Wellspring*", "Right Hook*", "Kill Tracker", ], "Power": 10, "ROF": 65, "Range": 72, "Rarity": "Legendary", "Recoil": 56, "Reload": 26, "Season": 18, "Shield Duration": 0, "Source": "plunder", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 67, "Archetype": "Lightweight Frame", "Blast Radius": 100, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 78, "Hash": 3849810018, "Holofoil": false, "Id": ""6917529848801242708"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 1, "Masterwork Type": "Blast Radius", "Name": "Pardon Our Dust", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Confined Launch", "Smart Drift Control*", "Proximity Grenades", "Spike Grenades*", "Auto-Loading Holster*", "Adrenaline Junkie*", "Hot Swap*", "Kill Tracker", "Tier 1: Blast Radius*", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Legendary", "Recoil": 92, "Reload": 69, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 78, "Year": 4, "Zoom": 13, }, { "AA": 70, "Accuracy": 74, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 8, "Draw Time": 667, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 70, "Hash": 232928045, "Holofoil": false, "Id": ""6917529848801244804"", "Impact": 76, "Kill Tracker": 813, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Under Your Skin", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Agile Bowstring*", "Carbon Arrow Shaft*", "Unrelenting*", "Explosive Head*", "Land Tank*", "Kill Tracker", "Nectar Dynamo*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 75, "Reload": 40, "Season": 16, "Shield Duration": 0, "Source": "psiops", "Stability": 62, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 19, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 967, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 24, "Hash": 963710795, "Holofoil": false, "Id": ""6917529849150569570"", "Impact": 95, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Aurvandil FR6", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Corkscrew Rifling*", "Smallbore", "Particle Repeater*", "Projection Fuse", "Reconstruction*", "Chill Clip*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 0, "Range": 52, "Rarity": "Legendary", "Recoil": 67, "Reload": 18, "Season": 19, "Shield Duration": 0, "Source": "engram", "Stability": 39, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 64, "Accuracy": 89, "Airborne Effectiveness": 5, "Ammo": "primary", "Ammo Generation": 59, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 633, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 2591241074, "Holofoil": false, "Id": ""6917529849183577444"", "Impact": 76, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Strident Whistle", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Flexible String", "Polymer String*", "Carbon Arrow Shaft", "Straight Fletching*", "Killing Wind*", "Explosive Head*", "Vanguard's Vindication*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 76, "Reload": 40, "Season": 17, "Shield Duration": 0, "Source": "strikes", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 83, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 46, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 409551876, "Holofoil": false, "Id": ""6917529849183579854"", "Impact": 49, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "The Keening", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Fluted Barrel", "Alloy Magazine*", "High-Caliber Rounds", "Surplus*", "Unrelenting*", "One Quiet Moment*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 300, "Range": 48, "Rarity": "Legendary", "Recoil": 100, "Reload": 26, "Season": 13, "Shield Duration": 0, "Source": "crucible", "Stability": 61, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 1141927949, "Holofoil": false, "Id": ""6917529849190692403"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Yesteryear", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Smallbore", "Alloy Magazine", "Light Mag*", "Outlaw*", "Desperado*", "Gun and Run*", "Suros Synergy", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 390, "Range": 48, "Rarity": "Legendary", "Recoil": 60, "Reload": 52, "Season": 18, "Shield Duration": 0, "Source": "gambit", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 17, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "special", "Ammo Generation": 35, "Archetype": "Aggressive Glaive", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 2978226043, "Holofoil": false, "Id": ""6917529850970210336"", "Impact": 95, "Kill Tracker": 568, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Shield Duration", "Name": "Judgment of Kelgorath", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Glaive*", "Lightweight Emitter*", "Light Mag*", "Overflow*", "Close to Melee*", "Ambush*", "Häkke Breach Armaments", "Kill Tracker", "Tier 1: Shield Duration*", ], "Power": 10, "ROF": 45, "Range": 76, "Rarity": "Legendary", "Recoil": 0, "Reload": 55, "Season": 19, "Shield Duration": 16, "Source": "seasonpass", "Stability": 0, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Glaive", "Velocity": 0, "Year": 5, "Zoom": 0, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 27, "Ammo": "primary", "Ammo Generation": 65, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 20, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 72, "Hash": 3886416794, "Holofoil": false, "Id": ""6917529850994732338"", "Impact": 15, "Kill Tracker": 923, "Loadouts": "", "Locked": true, "Mag": 47, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Submission", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Hammer-Forged Rifling*", "Tactical Mag*", "Subsistence*", "Frenzy*", "Souldrinker*", "Kill Tracker", "Backup Mag*", ], "Power": 10, "ROF": 900, "Range": 49, "Rarity": "Legendary", "Recoil": 100, "Reload": 41, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 30, "Ammo": "primary", "Ammo Generation": 57, "Archetype": "Soaring Fang", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 73, "Hash": 219145368, "Holofoil": false, "Id": ""6917529850994736130"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 44, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "The Manticore", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Soaring Fang*", "Arrowhead Brake*", "Ricochet Rounds*", "Swooping Talons*", "Fitted Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 900, "Range": 46, "Rarity": "Exotic", "Recoil": 100, "Reload": 30, "Season": 19, "Shield Duration": 0, "Source": "seasonpass", "Stability": 78, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 29, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 34, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "The Dawning", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, "Hash": 2814093983, "Holofoil": false, "Id": ""6917529851116536489"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 29, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Cold Front", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Fluted Barrel*", "Full Bore", "Steady Rounds*", "Alloy Magazine", "Unrelenting*", "Subsistence", "One for All*", "Dawning Surprise*", "Häkke Breach Armaments", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 720, "Range": 35, "Rarity": "Legendary", "Recoil": 92, "Reload": 19, "Season": 19, "Shield Duration": 0, "Source": "", "Stability": 27, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 26, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 967, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "The Dawning", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 3573686365, "Holofoil": false, "Id": ""6917529851121982045"", "Impact": 95, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Glacioclasm", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Extended Barrel", "Polygonal Rifling*", "Enhanced Battery", "Projection Fuse*", "Killing Wind*", "Offhand Strike", "Fragile Focus", "Kickstart*", "Dawning Surprise*", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 0, "Range": 75, "Rarity": "Legendary", "Recoil": 78, "Reload": 15, "Season": 19, "Shield Duration": 0, "Source": "", "Stability": 39, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "The Dawning", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 77, "Hash": 2812100428, "Holofoil": false, "Id": ""6917529851133493489"", "Impact": 27, "Kill Tracker": 168, "Loadouts": "", "Locked": true, "Mag": 30, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Stay Frosty", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Polygonal Rifling", "Extended Mag", "High-Caliber Rounds*", "Killing Wind*", "Encore", "Kill Clip*", "Desperado", "Dawning Surprise*", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 450, "Range": 43, "Rarity": "Legendary", "Recoil": 85, "Reload": 58, "Season": 19, "Shield Duration": 0, "Source": "", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 45, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Aggressive Burst", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 1751893422, "Holofoil": false, "Id": ""6917529853792664987"", "Impact": 35, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 36, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Disparity", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Burst*", "Chambered Compensator*", "Smallbore", "Accurized Rounds*", "Extended Mag", "Heating Up*", "Kill Clip*", "Ambush*", "Häkke Breach Armaments", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 450, "Range": 75, "Rarity": "Legendary", "Recoil": 77, "Reload": 28, "Season": 19, "Shield Duration": 0, "Source": "rasputin", "Stability": 73, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 19, }, { "AA": 73, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 30, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 68, "Hash": 2218569744, "Holofoil": false, "Id": ""6917529855341690372"", "Impact": 60, "Kill Tracker": 1937, "Loadouts": "", "Locked": true, "Mag": 15, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Tarnished Mettle", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Smallbore*", "Flared Magwell*", "Fourth Time's the Charm*", "Voltshot*", "Right Hook*", "Kill Tracker", "Counterbalance Stock*", ], "Power": 10, "ROF": 200, "Range": 47, "Rarity": "Legendary", "Recoil": 67, "Reload": 70, "Season": 18, "Shield Duration": 0, "Source": "plunder", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 5, "Zoom": 20, }, { "AA": 30, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 31, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 2, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 1509167284, "Holofoil": false, "Id": ""6917529855357192445"", "Impact": 22, "Kill Tracker": 149, "Loadouts": "", "Locked": true, "Mag": 30, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Blood Feud", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Extended Barrel*", "Alloy Magazine*", "Ensemble*", "Elemental Capacitor*", "Right Hook*", "Kill Tracker", ], "Power": 10, "ROF": 720, "Range": 47, "Rarity": "Legendary", "Recoil": 100, "Reload": 21, "Season": 18, "Shield Duration": 0, "Source": "plunder", "Stability": 11, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 81, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 32, "Hash": 1248372789, "Holofoil": false, "Id": ""6917529855357196290"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 50, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Sweet Sorrow", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Extended Barrel*", "Alloy Magazine*", "Pulse Monitor*", "Tap the Trigger*", "Land Tank*", "Kill Tracker", ], "Power": 10, "ROF": 720, "Range": 33, "Rarity": "Legendary", "Recoil": 67, "Reload": 47, "Season": 16, "Shield Duration": 0, "Source": "psiops", "Stability": 54, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 5, "Zoom": 17, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "heavy", "Ammo Generation": 36, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 14, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 820890091, "Holofoil": false, "Id": ""6917529855361216193"", "Impact": 25, "Kill Tracker": 294, "Loadouts": "", "Locked": true, "Mag": 74, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Planck's Stride", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Extended Barrel*", "Alloy Magazine*", "Perpetual Motion*", "Tap the Trigger*", "Right Hook*", "Kill Tracker", ], "Power": 10, "ROF": 900, "Range": 36, "Rarity": "Legendary", "Recoil": 66, "Reload": 57, "Season": 18, "Shield Duration": 0, "Source": "plunder", "Stability": 33, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 85, "Accuracy": 0, "Airborne Effectiveness": 32, "Ammo": "primary", "Ammo Generation": 33, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 3138208275, "Holofoil": false, "Id": ""6917529855413099866"", "Impact": 56, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 32, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Liminal Vigil", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Arrowhead Brake*", "Smallbore", "Extended Mag*", "High-Caliber Rounds", "Perpetual Motion*", "Kill Clip*", "Tex Balanced Stock*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 325, "Range": 58, "Rarity": "Legendary", "Recoil": 100, "Reload": 0, "Season": 19, "Shield Duration": 0, "Source": "dungeon", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 54, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 86, "Hash": 3341893443, "Holofoil": false, "Id": ""6917529857744228017"", "Impact": 15, "Kill Tracker": 22, "Loadouts": "", "Locked": true, "Mag": 43, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Funnelweb", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake", "Fluted Barrel*", "Tactical Mag*", "Steady Rounds", "Subsistence*", "Frenzy*", "Veist Stinger*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 900, "Range": 39, "Rarity": "Legendary", "Recoil": 96, "Reload": 41, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 54, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 85, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 34, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 23, "Hash": 1916287826, "Holofoil": false, "Id": ""6917529859842442532"", "Impact": 51, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Boudica-C", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Precision Frame*", "Extended Barrel*", "Smallbore", "Alloy Magazine*", "Armor-Piercing Rounds", "Pugilist*", "Frenzy*", "Häkke Breach Armaments*", "Kill Tracker", "Tier 3: Range*", ], "Power": 10, "ROF": 260, "Range": 76, "Rarity": "Legendary", "Recoil": 100, "Reload": 25, "Season": 18, "Shield Duration": 0, "Source": "engram", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Vortex Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 20, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 80, "Handling": 0, "Hash": 1796949035, "Holofoil": false, "Id": ""6917529861824572740"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 62, "Masterwork Tier": 2, "Masterwork Type": "Impact", "Name": "Razor's Edge", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Vortex Frame*", "Honed Edge", "Jagged Edge*", "Tempered Edge", "Burst Guard*", "Enduring Guard", "Unrelenting*", "Chain Reaction*", "Skulking Wolf*", "Kill Tracker", "Tier 2: Impact*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 16, "Shield Duration": 0, "Source": "ironbanner", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 5, "Zoom": 0, }, { "AA": 85, "Accuracy": 0, "Airborne Effectiveness": 23, "Ammo": "primary", "Ammo Generation": 44, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 1532276803, "Holofoil": false, "Id": ""6917529861850959518"", "Impact": 49, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Allied Demand", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Full Bore", "Tactical Mag*", "Alloy Magazine", "Rapid Hit*", "Multikill Clip*", "Skulking Wolf*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 300, "Range": 34, "Rarity": "Legendary", "Recoil": 97, "Reload": 40, "Season": 18, "Shield Duration": 0, "Source": "", "Stability": 81, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 54, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 3105930175, "Holofoil": false, "Id": ""6917529865884356070"", "Impact": 41, "Kill Tracker": 37, "Loadouts": "", "Locked": true, "Mag": 48, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Chain of Command", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "High-Caliber Rounds*", "Adrenaline Junkie*", "Osmosis", "Demolitionist*", "Adaptive Munitions", "Kill Tracker", "Vanguard's Vindication*", "One Quiet Moment", "Gun and Run", "Masterworked: Range*", ], "Power": 10, "ROF": 450, "Range": 58, "Rarity": "Legendary", "Recoil": 100, "Reload": 46, "Season": 17, "Shield Duration": 0, "Source": "crucible", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 19, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 967, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 13, "Hash": 963710795, "Holofoil": false, "Id": ""6917529866534581923"", "Impact": 95, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Aurvandil FR6", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Chambered Compensator*", "Fluted Barrel", "Enhanced Battery*", "Particle Repeater", "Subsistence*", "Golden Tricorn*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 0, "Range": 47, "Rarity": "Legendary", "Recoil": 77, "Reload": 18, "Season": 19, "Shield Duration": 0, "Source": "engram", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 24, "Hash": 1851521408, "Holofoil": false, "Id": ""6917529867705406462"", "Impact": 45, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Jararaca-3sr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Corkscrew Rifling*", "Hammer-Forged Rifling", "Alloy Magazine*", "Ricochet Rounds", "Fourth Time's the Charm*", "Golden Tricorn*", "Veist Stinger*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 260, "Range": 29, "Rarity": "Legendary", "Recoil": 57, "Reload": 29, "Season": 19, "Shield Duration": 0, "Source": "engram", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 5, "Zoom": 20, }, { "AA": 45, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 40, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 76, "Hash": 4100775158, "Holofoil": false, "Id": ""6917529868122113316"", "Impact": 20, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 38, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Pizzicato-22", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator", "Fluted Barrel*", "Alloy Magazine", "Light Mag*", "Perpetual Motion*", "Swashbuckler*", "Suros Synergy*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 900, "Range": 48, "Rarity": "Legendary", "Recoil": 90, "Reload": 60, "Season": 18, "Shield Duration": 0, "Source": "engram", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Aggressive Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 28, "Hash": 2276328320, "Holofoil": false, "Id": ""6917529868489383313"", "Impact": 35, "Kill Tracker": 177, "Loadouts": "", "Locked": true, "Mag": 36, "Masterwork Tier": 10, "Masterwork Type": "Handling", "Name": "Veles-X", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Burst*", "Chambered Compensator*", "Flared Magwell*", "Repulsor Brace*", "Tunnel Vision", "Golden Tricorn*", "Kill Clip", "Kill Tracker", "Häkke Breach Armaments", "Vanguard's Vindication*", "One Quiet Moment", "Gun and Run", "Masterworked: Handling*", "Goldjay*", ], "Power": 10, "ROF": 450, "Range": 73, "Rarity": "Legendary", "Recoil": 74, "Reload": 53, "Season": 19, "Shield Duration": 0, "Source": "crucible", "Stability": 72, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 1141927949, "Holofoil": false, "Id": ""6917529868648971540"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "Yesteryear", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Hammer-Forged Rifling", "Polygonal Rifling*", "Ricochet Rounds*", "Flared Magwell", "Perpetual Motion", "Heating Up*", "Multikill Clip*", "Gun and Run*", "Suros Synergy", "Kill Tracker", "Tier 4: Handling*", ], "Power": 10, "ROF": 390, "Range": 48, "Rarity": "Legendary", "Recoil": 50, "Reload": 42, "Season": 18, "Shield Duration": 0, "Source": "gambit", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 17, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 39, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 1731355324, "Holofoil": false, "Id": ""6917529869087090040"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 15, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "IKELOS_HC_v1.0.3", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel", "Smallbore*", "Accurized Rounds", "Appended Mag*", "Triple Tap*", "Frenzy*", "Rasputin's Arsenal*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 180, "Range": 40, "Rarity": "Legendary", "Recoil": 91, "Reload": 33, "Season": 19, "Shield Duration": 0, "Source": "rasputin", "Stability": 62, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 50, "Accuracy": 0, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 58, "Archetype": "Hunter's Trace IV", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 18, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 30, "Hash": 1473821207, "Holofoil": false, "Id": ""6917529869185337512"", "Impact": 35, "Kill Tracker": 1553, "Loadouts": "", "Locked": true, "Mag": 40, "Masterwork Tier": 10, "Masterwork Type": "Stability, Handling, Range, Aim Assistance, Airborne Effectiveness, Recoil Direction, Magazine, Reload Speed", "Name": "Revision Zero", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Hunter's Trace IV*", "Chambered Compensator*", "Flared Magwell*", "Perpetual Motion*", "Hand-Laid Stock*", "Häkke Light Burst Fire", "Häkke Heavy Burst Fire*", "Kill Tracker", "4-Timer Refit*", "Version: Prime*", ], "Power": 10, "ROF": 324, "Range": 96, "Rarity": "Exotic", "Recoil": 95, "Reload": 58, "Season": 19, "Shield Duration": 0, "Source": "rasputin", "Stability": 75, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "heavy", "Ammo Generation": 42, "Archetype": "Precision Frame", "Blast Radius": 100, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 33, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 3489657138, "Holofoil": false, "Id": ""6917529869192515031"", "Impact": 0, "Kill Tracker": 323, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Palmyra-B", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Volatile Launch*", "Impact Casing*", "Impulse Amplifier*", "Lasting Impression*", "Häkke Breach Armaments*", "Kill Tracker", "Targeting Adjuster*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Legendary", "Recoil": 65, "Reload": 32, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 46, "Year": 5, "Zoom": 20, }, { "AA": 43, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "special", "Ammo Generation": 28, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 14, "Hash": 3920310144, "Holofoil": false, "Id": ""6917529869760979131"", "Impact": 90, "Kill Tracker": 19, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Volta Bracket", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Extended Barrel*", "Fluted Barrel", "Extended Mag*", "Flared Magwell", "Firmly Planted*", "Under Pressure*", "Nanotech Tracer Missiles*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 72, "Range": 83, "Rarity": "Legendary", "Recoil": 86, "Reload": 8, "Season": 20, "Shield Duration": 0, "Source": "campaign", "Stability": 23, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 6, "Zoom": 50, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 76, "Hash": 2099894368, "Holofoil": false, "Id": ""6917529870298123302"", "Impact": 27, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Battle Scar", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Polygonal Rifling", "Tactical Mag*", "Steady Rounds", "Perpetual Motion*", "Osmosis*", "Field-Tested*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 450, "Range": 28, "Rarity": "Legendary", "Recoil": 84, "Reload": 66, "Season": 20, "Shield Duration": 0, "Source": "engram", "Stability": 53, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 6, "Zoom": 17, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 43, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 20, "Hash": 1555959830, "Holofoil": false, "Id": ""6917529871352775959"", "Impact": 78, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 10, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Seventh Seraph Officer Revolver", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Fluted Barrel", "Seraph Rounds*", "Flared Magwell", "Reconstruction*", "Redirection*", "Rasputin's Arsenal*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 180, "Range": 45, "Rarity": "Legendary", "Recoil": 94, "Reload": 49, "Season": 19, "Shield Duration": 0, "Source": "dungeon", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 34, "Accuracy": 0, "Airborne Effectiveness": 8, "Ammo": "special", "Ammo Generation": 50, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 1289796511, "Holofoil": false, "Id": ""6917529875526130336"", "Impact": 55, "Kill Tracker": 46, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Iterative Loop", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Chambered Compensator*", "Fluted Barrel", "Ionized Battery*", "Projection Fuse", "Lead from Gold*", "Voltshot*", "Nanotech Tracer Missiles*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 23, "Rarity": "Legendary", "Recoil": 57, "Reload": 25, "Season": 20, "Shield Duration": 0, "Source": "neomuna", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 82, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 57, "Hash": 372697604, "Holofoil": false, "Id": ""6917529875532160623"", "Impact": 84, "Kill Tracker": 16, "Loadouts": "", "Locked": true, "Mag": 10, "Masterwork Tier": 5, "Masterwork Type": "Stability", "Name": "Cantata-57", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator", "Corkscrew Rifling*", "Accurized Rounds*", "Flared Magwell", "Eye of the Storm*", "Timed Payload*", "Suros Synergy*", "Kill Tracker", "Tier 5: Stability*", ], "Power": 10, "ROF": 140, "Range": 61, "Rarity": "Legendary", "Recoil": 85, "Reload": 45, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 61, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 57, "Archetype": "Wave Frame", "Blast Radius": 52, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": true, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 81, "Hash": 1606497639, "Holofoil": false, "Id": ""6917529883630545315"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 2, "Masterwork Type": "Blast Radius", "Name": "Harsh Language", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Wave Frame*", "Quick Launch*", "Smart Drift Control", "High-Velocity Rounds*", "Implosion Rounds", "Envious Assassin*", "Repulsor Brace*", "Field-Tested*", "Kill Tracker", "Tier 2: Blast Radius*", ], "Power": 10, "ROF": 72, "Range": 0, "Rarity": "Legendary", "Recoil": 75, "Reload": 75, "Season": 20, "Shield Duration": 0, "Source": "engram", "Stability": 20, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 93, "Year": 6, "Zoom": 13, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 50, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 2573900604, "Holofoil": false, "Id": ""6917529890584112726"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Basso Ostinato", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Corkscrew Rifling*", "Full Choke", "Tactical Mag*", "Extended Mag", "Envious Assassin*", "One-Two Punch*", "Nanotech Tracer Missiles*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 140, "Range": 32, "Rarity": "Legendary", "Recoil": 70, "Reload": 73, "Season": 20, "Shield Duration": 0, "Source": "neomuna", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "heavy", "Ammo Generation": 40, "Archetype": "Vexadecimal", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 449318888, "Holofoil": false, "Id": ""6917529890604281439"", "Impact": 70, "Kill Tracker": 217, "Loadouts": "", "Locked": true, "Mag": 48, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Deterministic Chaos", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Vexadecimal*", "Polygonal Rifling*", "Ricochet Rounds*", "Heavy Metal*", "Composite Stock*", "Kill Tracker", ], "Power": 10, "ROF": 360, "Range": 49, "Rarity": "Exotic", "Recoil": 80, "Reload": 38, "Season": 20, "Shield Duration": 0, "Source": "exoticquest", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 67, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 68, "Hash": 3849810018, "Holofoil": false, "Id": ""6917529891010789138"", "Impact": 0, "Kill Tracker": 25, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 2, "Masterwork Type": "Velocity", "Name": "Pardon Our Dust", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Lightweight Frame*", "Confined Launch", "Hard Launch*", "Disorienting Grenades*", "Spike Grenades", "Ambitious Assassin*", "Demolitionist*", "Hot Swap*", "Kill Tracker", "Tier 2: Velocity*", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Legendary", "Recoil": 77, "Reload": 69, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 18, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 90, "Year": 4, "Zoom": 13, }, { "AA": 83, "Accuracy": 53, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 70, "Archetype": "Hail Barrage", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 567, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 72, "Hash": 3659414143, "Holofoil": false, "Id": ""6917529892142418495"", "Impact": 68, "Kill Tracker": 166, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Verglas Curve", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Hail Barrage*", "Natural String*", "Straight Fletching*", "Hail Storm*", "Kill Tracker", "Heritance of the Thaw*", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Exotic", "Recoil": 60, "Reload": 60, "Season": 20, "Shield Duration": 0, "Source": "seasonpass", "Stability": 63, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 6, "Zoom": 18, }, { "AA": 48, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 50, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 2990047042, "Holofoil": false, "Id": ""6917529895492076768"", "Impact": 90, "Kill Tracker": 1, "Loadouts": "", "Locked": true, "Mag": 3, "Masterwork Tier": 5, "Masterwork Type": "Reload Speed", "Name": "Succession", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Tactical Mag*", "Steady Rounds", "Demolitionist*", "Firing Line*", "Bray Inheritance*", "Kill Tracker", "Tier 5: Reload Speed*", ], "Power": 10, "ROF": 72, "Range": 76, "Rarity": "Legendary", "Recoil": 80, "Reload": 50, "Season": 12, "Shield Duration": 0, "Source": "deepstonecrypt", "Stability": 33, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 4, "Zoom": 50, }, { "AA": 74, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "special", "Ammo Generation": 55, "Archetype": "M1R Distribution Matrix", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 4, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 82, "Hash": 3118061005, "Holofoil": false, "Id": ""6917529897686604573"", "Impact": 80, "Kill Tracker": 335, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 10, "Masterwork Type": undefined, "Name": "Vexcalibur", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "M1R Distribution Matrix*", "Ballistic Tuning*", "Extended Mag*", "Perpetual Loophole*", "Hand-Laid Stock*", "Kill Tracker", "Expert Authorization Override*", ], "Power": 10, "ROF": 80, "Range": 95, "Rarity": "Exotic", "Recoil": 0, "Reload": 62, "Season": 20, "Shield Duration": 0, "Source": "avalon", "Stability": 0, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Glaive", "Velocity": 0, "Year": 6, "Zoom": 0, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 46, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 39, "Hash": 996109059, "Holofoil": false, "Id": ""6917529897740803351"", "Impact": 62, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 17, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Nameless Midnight", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Tactical Mag*", "Extended Mag", "Perpetual Motion*", "Explosive Payload*", "Vanguard's Vindication*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 180, "Range": 40, "Rarity": "Legendary", "Recoil": 89, "Reload": 54, "Season": 20, "Shield Duration": 0, "Source": "strikes", "Stability": 53, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 20, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "Guardian Games", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 81, "Hash": 1389546626, "Holofoil": false, "Id": ""6917529897876443127"", "Impact": 60, "Kill Tracker": 47, "Loadouts": "", "Locked": true, "Mag": 17, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Taraxippos", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Smallbore", "Appended Mag*", "Alloy Magazine", "Fourth Time's the Charm*", "Hatchling*", "Classy Contender*", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 200, "Range": 30, "Rarity": "Legendary", "Recoil": 84, "Reload": 59, "Season": 20, "Shield Duration": 0, "Source": "events", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 19, }, { "AA": 34, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 29, "Hash": 4070357005, "Holofoil": false, "Id": ""6917529897881274051"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 5, "Masterwork Type": "Stability", "Name": "Seventh Seraph Carbine", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Full Bore", "Accurized Rounds*", "Flared Magwell", "Reconstruction*", "Target Lock*", "Rasputin's Arsenal*", "Kill Tracker", "Tier 5: Stability*", ], "Power": 10, "ROF": 450, "Range": 78, "Rarity": "Legendary", "Recoil": 84, "Reload": 41, "Season": 19, "Shield Duration": 0, "Source": "dungeon", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 24, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 811403305, "Holofoil": false, "Id": ""6917529898125626960"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 29, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Synchronic Roulette", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Fluted Barrel*", "Polygonal Rifling", "Extended Mag", "High-Caliber Rounds*", "Pulse Monitor*", "Target Lock*", "Nanotech Tracer Missiles*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 600, "Range": 53, "Rarity": "Legendary", "Recoil": 93, "Reload": 19, "Season": 20, "Shield Duration": 0, "Source": "neomuna", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 52, "Archetype": "Compressed Wave Frame", "Blast Radius": 56, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 1311684613, "Holofoil": false, "Id": ""6917529898134657462"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Blast Radius", "Name": "Dimensional Hypotrochoid", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Compressed Wave Frame*", "Linear Compensator*", "Smart Drift Control", "High-Velocity Rounds*", "Implosion Rounds", "Field Prep*", "One for All*", "Nanotech Tracer Missiles*", "Kill Tracker", "Tier 1: Blast Radius*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 63, "Reload": 52, "Season": 20, "Shield Duration": 0, "Source": "campaign", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 48, "Year": 6, "Zoom": 13, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 60, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 54, "Hash": 1168625549, "Holofoil": false, "Id": ""6917529898502298073"", "Impact": 65, "Kill Tracker": 17, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "IKELOS_SG_v1.0.3", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Barrel Shroud*", "Corkscrew Rifling", "Tactical Mag*", "Light Mag", "Grave Robber*", "Trench Barrel*", "Rasputin's Arsenal*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 140, "Range": 36, "Rarity": "Legendary", "Recoil": 69, "Reload": 71, "Season": 19, "Shield Duration": 0, "Source": "rasputin", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 355382946, "Holofoil": false, "Id": ""6917529899256085382"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 44, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Coronach-22", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Alloy Magazine*", "Armor-Piercing Rounds", "Envious Assassin*", "Cascade Point*", "Suros Synergy*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 600, "Range": 42, "Rarity": "Legendary", "Recoil": 73, "Reload": 43, "Season": 20, "Shield Duration": 0, "Source": "engram", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 65, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 31, "Hash": 676270382, "Holofoil": false, "Id": ""6917529902938888539"", "Impact": 90, "Kill Tracker": 6, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Albruna-D", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Appended Mag*", "Flared Magwell", "Field Prep*", "Firing Line*", "Gun and Run*", "Häkke Breach Armaments", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 72, "Range": 76, "Rarity": "Legendary", "Recoil": 75, "Reload": 31, "Season": 19, "Shield Duration": 0, "Source": "gambit", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 5, "Zoom": 45, }, { "AA": 31, "Accuracy": 0, "Airborne Effectiveness": 16, "Ammo": "primary", "Ammo Generation": 48, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 16, "Hash": 3635821806, "Holofoil": false, "Id": ""6917529902969834120"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Phyllotactic Spiral", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Extended Barrel*", "Polygonal Rifling", "Appended Mag*", "High-Caliber Rounds", "Keep Away*", "Voltshot*", "Nanotech Tracer Missiles*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 340, "Range": 70, "Rarity": "Legendary", "Recoil": 90, "Reload": 32, "Season": 20, "Shield Duration": 0, "Source": "campaign", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 6, "Zoom": 18, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 26, "Ammo": "heavy", "Ammo Generation": 51, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 2187717691, "Holofoil": false, "Id": ""6917529902984276627"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 53, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Circular Logic", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Adaptive Frame*", "Full Bore*", "Smallbore", "High-Caliber Rounds*", "Flared Magwell", "Feeding Frenzy*", "Golden Tricorn*", "Nanotech Tracer Missiles*", "Kill Tracker", "Icarus Grip*", "Tier 3: Range*", ], "Power": 10, "ROF": 450, "Range": 63, "Rarity": "Legendary", "Recoil": 80, "Reload": 50, "Season": 20, "Shield Duration": 0, "Source": "neomuna", "Stability": 23, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 43, "Hash": 355382946, "Holofoil": false, "Id": ""6917529906748710911"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 44, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Coronach-22", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Full Bore*", "Smallbore", "High-Caliber Rounds*", "Flared Magwell", "Shot Swap*", "Target Lock*", "Suros Synergy*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 600, "Range": 63, "Rarity": "Legendary", "Recoil": 73, "Reload": 41, "Season": 20, "Shield Duration": 0, "Source": "engram", "Stability": 28, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 424291879, "Holofoil": false, "Id": ""6917529906826161434"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 35, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Age-Old Bond", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Fluted Barrel*", "Polygonal Rifling", "Appended Mag*", "Extended Mag", "Stats for All*", "Harmony*", "Explosive Pact*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 360, "Range": 82, "Rarity": "Legendary", "Recoil": 79, "Reload": 36, "Season": 21, "Shield Duration": 0, "Source": "lastwish", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 29, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 26, "Hash": 3885259140, "Holofoil": false, "Id": ""6917529906841606051"", "Impact": 67, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Transfiguration", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Chambered Compensator*", "Extended Barrel", "Appended Mag*", "Extended Mag", "Keep Away*", "Kinetic Tremors*", "Explosive Pact*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 150, "Range": 74, "Rarity": "Legendary", "Recoil": 88, "Reload": 25, "Season": 21, "Shield Duration": 0, "Source": "lastwish", "Stability": 32, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 21, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 1123421440, "Holofoil": false, "Id": ""6917529907275914673"", "Impact": 84, "Kill Tracker": 23, "Loadouts": "", "Locked": true, "Mag": 11, "Masterwork Tier": 10, "Masterwork Type": "Handling", "Name": "Epochal Integration", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Smallbore*", "Ricochet Rounds*", "Keep Away", "Stats for All*", "Eye of the Storm", "Incandescent*", "Harmonic Resonance*", "Nanotech Tracer Missiles", "Kill Tracker", "Masterworked: Handling*", ], "Power": 10, "ROF": 140, "Range": 54, "Rarity": "Legendary", "Recoil": 89, "Reload": 41, "Season": 21, "Shield Duration": 0, "Source": "neomuna", "Stability": 82, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 50, "Accuracy": 0, "Airborne Effectiveness": 63, "Ammo": "primary", "Ammo Generation": 57, "Archetype": "Explosive Shadow", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 24, "Hash": 204878059, "Holofoil": false, "Id": ""6917529908784615228"", "Impact": 78, "Kill Tracker": 690, "Loadouts": "", "Locked": true, "Mag": 14, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Malfeasance", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Explosive Shadow*", "Corkscrew Rifling*", "Extended Mag*", "Taken Predator*", "Heavy Grip*", "Kill Tracker", "Empty Catalyst Socket*", "Aim to Misbehave*", ], "Power": 10, "ROF": 180, "Range": 45, "Rarity": "Exotic", "Recoil": 98, "Reload": 60, "Season": 4, "Shield Duration": 0, "Source": "exoticquest", "Stability": 95, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 2, "Zoom": 14, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 29, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 667, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": true, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 17, "Hash": 3591141932, "Holofoil": false, "Id": ""6917529908802335936"", "Impact": 70, "Kill Tracker": 3, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Techeun Force", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Polygonal Rifling", "Enhanced Battery*", "Liquid Coils", "Rewind Rounds*", "High-Impact Reserves*", "Explosive Pact*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 0, "Range": 53, "Rarity": "Legendary", "Recoil": 70, "Reload": 34, "Season": 21, "Shield Duration": 0, "Source": "lastwish", "Stability": 39, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 35, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 43, "Hash": 3228096719, "Holofoil": false, "Id": ""6917529908830411416"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Defiance of Yasmin", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Hammer-Forged Rifling", "Accurized Rounds*", "Tactical Mag", "Firefly*", "Opening Shot*", "Runneth Over*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 90, "Range": 64, "Rarity": "Legendary", "Recoil": 72, "Reload": 42, "Season": 18, "Shield Duration": 0, "Source": "kingsfall", "Stability": 56, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 5, "Zoom": 40, }, { "AA": 36, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 50, "Archetype": "Rapid-Fire Frame", "Blast Radius": 40, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 2972949637, "Holofoil": false, "Id": ""6917529910434373836"", "Impact": 0, "Kill Tracker": 54, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 10, "Masterwork Type": "Blast Radius", "Name": "Koraxis's Distress", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Confined Launch*", "Smart Drift Control", "Spike Grenades*", "High-Explosive Ordnance", "Reconstruction*", "Full Court*", "Harmonic Resonance*", "Kill Tracker", "Masterworked: Blast Radius*", ], "Power": 10, "ROF": 150, "Range": 0, "Rarity": "Legendary", "Recoil": 75, "Reload": 28, "Season": 20, "Shield Duration": 0, "Source": "rootofnightmares", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 48, "Year": 6, "Zoom": 13, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 20, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 13, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 90, "Guard Resistance": 0, "Handling": 0, "Hash": 3257091167, "Holofoil": false, "Id": ""6917529919724772603"", "Impact": 61, "Kill Tracker": 96, "Loadouts": "", "Locked": true, "Mag": 70, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "The Other Half", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Tempered Edge*", "Enduring Guard*", "Duelist's Trance*", "Frenzy*", "Hot Swap*", "Kill Tracker", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 4, "Zoom": 0, }, { "AA": 42, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "primary", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 3, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 2149683300, "Holofoil": false, "Id": ""6917529919733335267"", "Impact": 22, "Kill Tracker": 93, "Loadouts": "", "Locked": true, "Mag": 32, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "IKELOS_SMG_v1.0.3", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Extended Barrel*", "Alloy Magazine*", "Feeding Frenzy*", "Frenzy*", "Rasputin's Arsenal*", "Kill Tracker", "Targeting Adjuster*", ], "Power": 10, "ROF": 720, "Range": 54, "Rarity": "Legendary", "Recoil": 100, "Reload": 24, "Season": 19, "Shield Duration": 0, "Source": "rasputin", "Stability": 25, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 60, "Accuracy": 0, "Airborne Effectiveness": 7, "Ammo": "heavy", "Ammo Generation": 33, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": "crafted", "Crafted Level": 20, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 17, "Hash": 2272041093, "Holofoil": false, "Id": ""6917529919743326028"", "Impact": 41, "Kill Tracker": 77, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Fire and Forget", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Burst*", "Extended Barrel*", "Enhanced Battery*", "Rangefinder*", "Frenzy*", "Ambush*", "Veist Stinger", "Kill Tracker", ], "Power": 10, "ROF": 0, "Range": 45, "Rarity": "Legendary", "Recoil": 85, "Reload": 22, "Season": 19, "Shield Duration": 0, "Source": "rasputin", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 25, }, { "AA": 60, "Accuracy": 0, "Airborne Effectiveness": 31, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 2221264583, "Holofoil": false, "Id": ""6917529920140528503"", "Impact": 29, "Kill Tracker": 173, "Loadouts": "", "Locked": true, "Mag": 45, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Smite of Merain", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Polygonal Rifling*", "Smallbore", "Extended Mag*", "Alloy Magazine", "Moving Target*", "Adrenaline Junkie*", "Runneth Over*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 390, "Range": 45, "Rarity": "Legendary", "Recoil": 70, "Reload": 27, "Season": 18, "Shield Duration": 0, "Source": "kingsfall", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 17, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 7, "Ammo": "heavy", "Ammo Generation": 20, "Archetype": "Eyes on All", "Blast Radius": 50, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 2399110176, "Holofoil": false, "Id": ""6917529920176006759"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Eyes of Tomorrow", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Eyes on All*", "Smart Drift Control*", "Alloy Casing*", "Adaptive Ordnance*", "Fitted Stock*", "Kill Tracker", ], "Power": 10, "ROF": 20, "Range": 0, "Rarity": "Exotic", "Recoil": 100, "Reload": 76, "Season": 12, "Shield Duration": 0, "Source": "deepstonecrypt", "Stability": 82, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 57, "Year": 4, "Zoom": 20, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Overcharge Capacitor", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 82, "Hash": 1912669214, "Holofoil": false, "Id": ""6917529920189669828"", "Impact": 29, "Kill Tracker": 19, "Loadouts": "", "Locked": true, "Mag": 45, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Centrifuse", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Overcharge Capacitor*", "Corkscrew Rifling*", "Appended Mag*", "Regenerative Motion*", "Hand-Laid Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 450, "Range": 47, "Rarity": "Exotic", "Recoil": 90, "Reload": 40, "Season": 21, "Shield Duration": 0, "Source": "seasonpass", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "heavy", "Ammo Generation": 33, "Archetype": "Adaptive Frame", "Blast Radius": 50, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 1851777734, "Holofoil": false, "Id": ""6917529920275516726"", "Impact": 0, "Kill Tracker": 7, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 1, "Masterwork Type": "Velocity", "Name": "Apex Predator", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Countermass", "Quick Launch*", "Alloy Casing*", "Implosion Rounds", "Reconstruction*", "Frenzy*", "Explosive Pact*", "Kill Tracker", "Counterbalance Stock*", "Tier 1: Velocity*", ], "Power": 10, "ROF": 20, "Range": 0, "Rarity": "Legendary", "Recoil": 76, "Reload": 74, "Season": 21, "Shield Duration": 0, "Source": "lastwish", "Stability": 33, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 60, "Year": 6, "Zoom": 20, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 47, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 65, "Hash": 2414141462, "Holofoil": false, "Id": ""6917529920445805726"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 45, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "The Vision", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Burst*", "QuickDot SAS*", "Control SAS", "Alloy Magazine*", "Flared Magwell", "Surplus*", "Disruption Break*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 491, "Range": 30, "Rarity": "Legendary", "Recoil": 90, "Reload": 33, "Season": 14, "Shield Duration": 0, "Source": "engram", "Stability": 85, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "heavy", "Ammo Generation": 37, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 530, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 999767358, "Holofoil": false, "Id": ""6917529920506933431"", "Impact": 41, "Kill Tracker": 5, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Charge Time", "Name": "Cataclysmic", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Ionized Battery*", "Projection Fuse", "Surplus*", "Clown Cartridge*", "Souldrinker*", "Kill Tracker", "Tier 1: Charge Time*", ], "Power": 10, "ROF": 0, "Range": 44, "Rarity": "Legendary", "Recoil": 70, "Reload": 5, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 53, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 25, }, { "AA": 60, "Accuracy": 0, "Airborne Effectiveness": 23, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "Solstice", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 35, "Hash": 3240434620, "Holofoil": false, "Id": ""6917529921012873395"", "Impact": 92, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Something New", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Corkscrew Rifling*", "Polygonal Rifling", "Alloy Magazine*", "Light Mag", "Rapid Hit*", "Frenzy*", "Dream Work*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 120, "Range": 64, "Rarity": "Legendary", "Recoil": 98, "Reload": 25, "Season": 21, "Shield Duration": 0, "Source": "", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 19, "Hash": 2034215657, "Holofoil": false, "Id": ""6917529921046073995"", "Impact": 92, "Kill Tracker": 15, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Round Robin", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Aggressive Frame*", "Full Bore*", "Polygonal Rifling", "Alloy Magazine*", "Appended Mag", "Keep Away*", "Hatchling*", "Nanotech Tracer Missiles*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 120, "Range": 71, "Rarity": "Legendary", "Recoil": 97, "Reload": 22, "Season": 20, "Shield Duration": 0, "Source": "neomuna", "Stability": 11, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 48, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 15, "Hash": 3292795429, "Holofoil": false, "Id": ""6917529923842253867"", "Impact": 45, "Kill Tracker": 1, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Randy's Throwing Knife", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Rapid-Fire Frame*", "Chambered Compensator*", "Corkscrew Rifling", "High-Caliber Rounds*", "Flared Magwell", "Perpetual Motion*", "Kill Clip*", "One Quiet Moment*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 260, "Range": 28, "Rarity": "Legendary", "Recoil": 69, "Reload": 29, "Season": 21, "Shield Duration": 0, "Source": "crucible", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 20, }, { "AA": 31, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 46, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 67, "Hash": 2715240478, "Holofoil": false, "Id": ""6917529923845906796"", "Impact": 50, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Riptide", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Fluted Barrel*", "Polygonal Rifling", "Accelerated Coils*", "Liquid Coils", "Well-Rounded*", "Frenzy*", "One Quiet Moment*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 0, "Range": 21, "Rarity": "Legendary", "Recoil": 53, "Reload": 45, "Season": 17, "Shield Duration": 0, "Source": "crucible", "Stability": 36, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 1107446438, "Holofoil": false, "Id": ""6917529923875728627"", "Impact": 45, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 18, "Masterwork Tier": 4, "Masterwork Type": "Reload Speed", "Name": "Servant Leader", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Fluted Barrel*", "Smallbore", "Tactical Mag*", "Extended Mag", "Pulse Monitor*", "Frenzy*", "Gun and Run*", "Field-Tested", "Kill Tracker", "Tier 4: Reload Speed*", ], "Power": 10, "ROF": 260, "Range": 24, "Rarity": "Legendary", "Recoil": 54, "Reload": 43, "Season": 15, "Shield Duration": 0, "Source": "gambit", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 4, "Zoom": 26, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 16, "Ammo": "heavy", "Ammo Generation": 43, "Archetype": "Adaptive Frame", "Blast Radius": 65, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 3915197957, "Holofoil": false, "Id": ""6917529923888960132"", "Impact": 0, "Kill Tracker": 2, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Wendigo GL3 (Adept)", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Volatile Launch*", "Confined Launch", "Alloy Casing*", "High-Explosive Ordnance", "Impulse Amplifier*", "Clown Cartridge", "Frenzy*", "Stunning Recovery*", "Vanguard's Vindication", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 78, "Reload": 76, "Season": 20, "Shield Duration": 0, "Source": "nightfall", "Stability": 28, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 24, "Year": 6, "Zoom": 13, }, { "AA": 88, "Accuracy": 0, "Airborne Effectiveness": 27, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "All at Once", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 1000, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 3121540812, "Holofoil": false, "Id": ""6917529923888961430"", "Impact": 35, "Kill Tracker": 35, "Loadouts": "", "Locked": true, "Mag": 21, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Final Warning", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "All at Once*", "Hammer-Forged Rifling*", "Flared Magwell*", "Pick Your Poison*", "Combat Grip*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 450, "Range": 45, "Rarity": "Exotic", "Recoil": 60, "Reload": 48, "Season": 20, "Shield Duration": 0, "Source": "exoticquest", "Stability": 66, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 67, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 24, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 1298815317, "Holofoil": false, "Id": ""6917529924564705554"", "Impact": 35, "Kill Tracker": 1250, "Loadouts": "", "Locked": true, "Mag": 18, "Masterwork Tier": 10, "Masterwork Type": "Stability", "Name": "Brigand's Law", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Polygonal Rifling*", "Steady Rounds*", "Threat Detector*", "Voltshot*", "Right Hook*", "Kill Tracker", "Backup Mag*", "Gambit Memento*", "Seraphim Cloak*", ], "Power": 10, "ROF": 450, "Range": 17, "Rarity": "Legendary", "Recoil": 95, "Reload": 43, "Season": 18, "Shield Duration": 0, "Source": "plunder", "Stability": 98, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "heavy", "Ammo Generation": 33, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 500, "Crafted": "crafted", "Crafted Level": 20, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 48, "Hash": 1911060537, "Holofoil": false, "Id": ""6917529925211026181"", "Impact": 38, "Kill Tracker": 103, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": 10, "Masterwork Type": "Charge Time", "Name": "Taipan-4fr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Enhanced Battery*", "Triple Tap*", "Firing Line*", "Veist Stinger*", "Kill Tracker", "Envious Touch*", ], "Power": 10, "ROF": 0, "Range": 50, "Rarity": "Legendary", "Recoil": 99, "Reload": 31, "Season": 18, "Shield Duration": 0, "Source": "engram", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 25, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 18, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 72, "Hash": 392008588, "Holofoil": false, "Id": ""6917529925216730409"", "Impact": 21, "Kill Tracker": 1099, "Loadouts": "", "Locked": false, "Mag": 44, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Perpetualis", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Tactical Mag*", "Elemental Capacitor*", "Hatchling*", "Noble Deeds*", "Kill Tracker", "Counterbalance Stock*", "Testudo*", ], "Power": 10, "ROF": 600, "Range": 53, "Rarity": "Legendary", "Recoil": 98, "Reload": 68, "Season": 20, "Shield Duration": 0, "Source": "seasonpass", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 26, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 59, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 491078457, "Holofoil": false, "Id": ""6917529925232147135"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 31, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Positive Outlook", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Fluted Barrel", "Armor-Piercing Rounds*", "Light Mag", "Surplus*", "Adaptive Munitions*", "Vanguard's Vindication*", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 450, "Range": 69, "Rarity": "Legendary", "Recoil": 96, "Reload": 43, "Season": 21, "Shield Duration": 0, "Source": "strikes", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 26, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 59, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 56, "Hash": 491078457, "Holofoil": false, "Id": ""6917529925450059378"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 31, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Positive Outlook", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Hammer-Forged Rifling", "Alloy Magazine*", "Ricochet Rounds", "Shot Swap*", "Destabilizing Rounds*", "Vanguard's Vindication*", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 450, "Range": 64, "Rarity": "Legendary", "Recoil": 96, "Reload": 45, "Season": 21, "Shield Duration": 0, "Source": "strikes", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "heavy", "Ammo Generation": 45, "Archetype": "Precision Frame", "Blast Radius": 55, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "Solstice", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 35, "Hash": 3256453690, "Holofoil": false, "Id": ""6917529925454246312"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 5, "Masterwork Type": "Blast Radius", "Name": "Crowning Duologue", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Volatile Launch*", "Smart Drift Control", "Black Powder*", "High-Velocity Rounds", "Auto-Loading Holster*", "Chain Reaction*", "Dream Work*", "Kill Tracker", "Tier 5: Blast Radius*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Legendary", "Recoil": 65, "Reload": 29, "Season": 21, "Shield Duration": 0, "Source": "events", "Stability": 29, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 46, "Year": 6, "Zoom": 20, }, { "AA": 100, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "special", "Ammo Generation": 60, "Archetype": "Cold Fusion", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 1345867571, "Holofoil": false, "Id": ""6917529928184181406"", "Impact": 6, "Kill Tracker": 29, "Loadouts": "", "Locked": true, "Mag": 74, "Masterwork Tier": 10, "Masterwork Type": undefined, "Name": "Coldheart", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Cold Fusion*", "Extended Barrel*", "Enhanced Battery*", "Longest Winter*", "Hand-Laid Stock*", "Kill Tracker", ], "Power": 10, "ROF": 1000, "Range": 70, "Rarity": "Exotic", "Recoil": 100, "Reload": 30, "Season": 1, "Shield Duration": 0, "Source": "", "Stability": 70, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 1, "Zoom": 16, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Creeping Attrition", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 44, "Hash": 940371471, "Holofoil": false, "Id": ""6917529928209111412"", "Impact": 45, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 20, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Wicked Implement", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Creeping Attrition*", "Corkscrew Rifling*", "Accurized Rounds*", "Tithing Harvest*", "Fitted Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 260, "Range": 62, "Rarity": "Exotic", "Recoil": 95, "Reload": 41, "Season": 21, "Shield Duration": 0, "Source": "seasonpass", "Stability": 70, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 20, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 3832743906, "Holofoil": false, "Id": ""6917529928407668540"", "Impact": 62, "Kill Tracker": 52, "Loadouts": "", "Locked": true, "Mag": 18, "Masterwork Tier": 5, "Masterwork Type": "Range", "Name": "Hung Jury SR4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Fluted Barrel", "Appended Mag*", "Flared Magwell", "Rapid Hit*", "Firefly*", "Stunning Recovery*", "Vanguard's Vindication", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 5: Range*", ], "Power": 10, "ROF": 180, "Range": 54, "Rarity": "Legendary", "Recoil": 100, "Reload": 46, "Season": 20, "Shield Duration": 0, "Source": "nightfall", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 22, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 32, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 3832743906, "Holofoil": false, "Id": ""6917529928408652877"", "Impact": 62, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 19, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Hung Jury SR4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator*", "Smallbore", "Extended Mag*", "Alloy Magazine", "Subsistence*", "Frenzy*", "Stunning Recovery*", "Vanguard's Vindication", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 180, "Range": 49, "Rarity": "Legendary", "Recoil": 85, "Reload": 26, "Season": 20, "Shield Duration": 0, "Source": "nightfall", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 22, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 3832743906, "Holofoil": false, "Id": ""6917529928410549273"", "Impact": 62, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 17, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Hung Jury SR4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Smallbore", "Tactical Mag*", "Extended Mag", "Rapid Hit*", "Kinetic Tremors*", "Stunning Recovery*", "Vanguard's Vindication", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 180, "Range": 59, "Rarity": "Legendary", "Recoil": 85, "Reload": 56, "Season": 20, "Shield Duration": 0, "Source": "nightfall", "Stability": 63, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 22, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 31, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 2349907931, "Holofoil": false, "Id": ""6917529928410551356"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 45, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Prolonged Engagement", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Extended Mag*", "Flared Magwell", "Subsistence*", "Headstone*", "Vanguard's Vindication*", "Veist Stinger", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 900, "Range": 25, "Rarity": "Legendary", "Recoil": 95, "Reload": 12, "Season": 19, "Shield Duration": 0, "Source": "strikes", "Stability": 56, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 64, "Accuracy": 60, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 6, "Draw Time": 667, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 71, "Hash": 45643573, "Holofoil": false, "Id": ""6917529929109126948"", "Impact": 76, "Kill Tracker": 345, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Raconteur", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Agile Bowstring*", "Natural Fletching*", "Surplus*", "Eye of the Storm*", "Noble Deeds*", "Kill Tracker", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 72, "Reload": 40, "Season": 20, "Shield Duration": 0, "Source": "seasonpass", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 6, "Zoom": 18, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 2, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 18, "Hash": 3016891299, "Holofoil": false, "Id": ""6917529929109132120"", "Impact": 23, "Kill Tracker": 166, "Loadouts": "", "Locked": false, "Mag": 39, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Different Times", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Polygonal Rifling*", "Appended Mag*", "Subsistence*", "Focused Fury*", "Kill Tracker", "Unsated Hunger*", ], "Power": 10, "ROF": 540, "Range": 28, "Rarity": "Legendary", "Recoil": 52, "Reload": 28, "Season": 21, "Shield Duration": 0, "Source": "sonar", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 6, "Zoom": 17, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "special", "Ammo Generation": 54, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 2883484461, "Holofoil": false, "Id": ""6917529929117194404"", "Impact": 65, "Kill Tracker": 12, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Until Its Return", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Smallbore*", "Steady Rounds*", "Well-Rounded*", "Collective Action*", "Kill Tracker", "Unsated Hunger*", ], "Power": 10, "ROF": 140, "Range": 30, "Rarity": "Legendary", "Recoil": 70, "Reload": 61, "Season": 21, "Shield Duration": 0, "Source": "sonar", "Stability": 54, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 70, "Archetype": "Corrupted Nucleosynthesis", "Blast Radius": 22, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 3821409356, "Holofoil": false, "Id": ""6917529929117195489"", "Impact": 0, "Kill Tracker": 279, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Ex Diris", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Corrupted Nucleosynthesis*", "Volatile Launch*", "Nucleosynthetic Magazine*", "Loyal Moths*", "Hand-Laid Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Exotic", "Recoil": 58, "Reload": 69, "Season": 22, "Shield Duration": 0, "Source": "seasonpass", "Stability": 36, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 10, "Year": 6, "Zoom": 13, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 32, "Ammo": "special", "Ammo Generation": 61, "Archetype": "Micro-Missile Frame", "Blast Radius": 95, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, "Hash": 3993415705, "Holofoil": false, "Id": ""6917529929124638109"", "Impact": 0, "Kill Tracker": 12, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 10, "Masterwork Type": "Velocity", "Name": "The Mountaintop", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Hard Launch*", "Volatile Launch", "Spike Grenades*", "Sticky Grenades", "Rangefinder*", "Rampage*", "Kill Tracker", "Masterworked: Velocity*", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Legendary", "Recoil": 62, "Reload": 63, "Season": 5, "Shield Duration": 0, "Source": "crucible", "Stability": 23, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 100, "Year": 2, "Zoom": 13, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 33, "Archetype": "Property: Undecidable", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 667, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 3561203890, "Holofoil": false, "Id": ""6917529932869770669"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Tessellation", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Property: Undecidable*", "Polygonal Rifling*", "Projection Fuse*", "Property: Irreducible*", "Composite Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 0, "Range": 51, "Rarity": "Exotic", "Recoil": 75, "Reload": 32, "Season": 22, "Shield Duration": 0, "Source": "deluxe", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 67, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 231031173, "Holofoil": false, "Id": ""6917529933746196956"", "Impact": 35, "Kill Tracker": 91, "Loadouts": "", "Locked": true, "Mag": 17, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Mykel's Reverence", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Corkscrew Rifling*", "Hammer-Forged Rifling", "Alloy Magazine*", "Light Mag", "Pugilist*", "Swashbuckler*", "Harmonic Resonance*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 450, "Range": 38, "Rarity": "Legendary", "Recoil": 96, "Reload": 47, "Season": 20, "Shield Duration": 0, "Source": "rootofnightmares", "Stability": 75, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 100, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "special", "Ammo Generation": 0, "Archetype": "Judgment", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 56, "Hash": 4103414242, "Holofoil": false, "Id": ""6917529933753873229"", "Impact": 6, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 194, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Divinity", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Judgment*", "Polygonal Rifling*", "Particle Repeater*", "Penance*", "Composite Stock*", "Kill Tracker", ], "Power": 10, "ROF": 1000, "Range": 67, "Rarity": "Exotic", "Recoil": 100, "Reload": 45, "Season": 8, "Shield Duration": 0, "Source": "exoticquest", "Stability": 100, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 3, "Zoom": 16, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 43, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": true, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 4132072834, "Holofoil": false, "Id": ""6917529940136011573"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Locus Locutus", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Extended Barrel", "Accurized Rounds*", "Alloy Magazine", "Surplus*", "Headstone*", "Head Rush*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 90, "Range": 63, "Rarity": "Legendary", "Recoil": 63, "Reload": 43, "Season": 22, "Shield Duration": 0, "Source": "seasonpass", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 6, "Zoom": 40, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Worm's Hunger", "Blast Radius": 100, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 2812324400, "Holofoil": false, "Id": ""6917529944441251712"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Parasite", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Worm's Hunger*", "Volatile Launch*", "High-Explosive Ordnance*", "Worm Byproduct*", "Composite Stock*", "Kill Tracker", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Exotic", "Recoil": 77, "Reload": 44, "Season": 16, "Shield Duration": 0, "Source": "exoticquest", "Stability": 53, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 0, "Year": 5, "Zoom": 13, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "heavy", "Ammo Generation": 43, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 105306149, "Holofoil": false, "Id": ""6917529946625267555"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 74, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Eleatic Principle", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Polygonal Rifling", "High-Caliber Rounds*", "Light Mag", "Well-Rounded*", "Target Lock*", "Head Rush*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 900, "Range": 31, "Rarity": "Legendary", "Recoil": 90, "Reload": 64, "Season": 22, "Shield Duration": 0, "Source": "seasonpass", "Stability": 38, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 36, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "heavy", "Ammo Generation": 70, "Archetype": "Rapid-Fire Frame", "Blast Radius": 15, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "Festival of the Lost", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 413901114, "Holofoil": false, "Id": ""6917529947931466139"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Acosmic", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Hard Launch*", "Quick Launch", "Augmented Drum*", "High-Explosive Ordnance", "Field Prep*", "High Ground*", "Search Party*", "Nadir Focus", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 150, "Range": 0, "Rarity": "Legendary", "Recoil": 64, "Reload": 8, "Season": 22, "Shield Duration": 0, "Source": "events", "Stability": 13, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 73, "Year": 6, "Zoom": 13, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 32, "Ammo": "primary", "Ammo Generation": 46, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 5159537, "Holofoil": false, "Id": ""6917529948470154887"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 60, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Senuna SI6", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Adaptive Burst*", "Arrowhead Brake*", "Polygonal Rifling", "Extended Mag*", "Appended Mag", "Perfect Float*", "Headstone*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 491, "Range": 30, "Rarity": "Legendary", "Recoil": 100, "Reload": 11, "Season": 20, "Shield Duration": 0, "Source": "engram", "Stability": 82, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 51, "Accuracy": 85, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Queen's Wrath", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 766, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 814876684, "Holofoil": false, "Id": ""6917529948475330972"", "Impact": 92, "Kill Tracker": 92, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Wish-Ender", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Queen's Wrath*", "High Tension String*", "Anti-Taken Fletching*", "Broadhead*", "Kill Tracker", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Exotic", "Recoil": 60, "Reload": 30, "Season": 4, "Shield Duration": 0, "Source": "dungeon", "Stability": 85, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 2, "Zoom": 18, }, { "AA": 29, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "heavy", "Ammo Generation": 32, "Archetype": "Aggressive Frame", "Blast Radius": 20, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": true, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 3067821200, "Holofoil": false, "Id": ""6917529950743999872"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Heretic", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Aggressive Frame*", "Volatile Launch", "Smart Drift Control*", "Black Powder", "Impact Casing*", "Ambitious Assassin*", "Cluster Bomb*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 25, "Range": 0, "Rarity": "Legendary", "Recoil": 83, "Reload": 61, "Season": 8, "Shield Duration": 0, "Source": "moon", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 84, "Year": 3, "Zoom": 20, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 31, "Ammo": "primary", "Ammo Generation": 29, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 65, "Hash": 3998080529, "Holofoil": false, "Id": ""6917529951136302558"", "Impact": 43, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 17, "Masterwork Tier": 5, "Masterwork Type": "Reload Speed", "Name": "Heliocentric QSc", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Hammer-Forged Rifling*", "Smallbore", "Extended Mag*", "Alloy Magazine", "Heal Clip*", "Frenzy*", "Nadir Focus*", "Kill Tracker", "Tier 5: Reload Speed*", ], "Power": 10, "ROF": 360, "Range": 37, "Rarity": "Legendary", "Recoil": 94, "Reload": 36, "Season": 22, "Shield Duration": 0, "Source": "engram", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 36, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "heavy", "Ammo Generation": 50, "Archetype": "Rapid-Fire Frame", "Blast Radius": 25, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "Festival of the Lost", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 26, "Hash": 413901114, "Holofoil": false, "Id": ""6917529951169162931"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Acosmic", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Rapid-Fire Frame*", "Linear Compensator*", "Quick Launch", "Spike Grenades*", "Alloy Casing", "Surplus*", "Explosive Light*", "Search Party*", "Nadir Focus", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 150, "Range": 0, "Rarity": "Legendary", "Recoil": 64, "Reload": 26, "Season": 22, "Shield Duration": 0, "Source": "events", "Stability": 38, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 63, "Year": 6, "Zoom": 13, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 3319810952, "Holofoil": false, "Id": ""6917529953259998501"", "Impact": 18, "Kill Tracker": 283, "Loadouts": "", "Locked": true, "Mag": 50, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Husk of the Pit", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Kill Tracker", ], "Power": 10, "ROF": 720, "Range": 20, "Rarity": "Common", "Recoil": 75, "Reload": 40, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "heavy", "Ammo Generation": 53, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": true, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 51, "Hash": 2828278545, "Holofoil": false, "Id": ""6917529953263119726"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 71, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Song of Ir Yût", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Smallbore", "Extended Mag*", "High-Caliber Rounds", "Rewind Rounds*", "Sword Logic*", "Cursed Thrall*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 450, "Range": 59, "Rarity": "Legendary", "Recoil": 90, "Reload": 37, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 66, "Accuracy": 84, "Airborne Effectiveness": 17, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Big-Game Hunter", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 1328, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 0, "Hash": 2591746970, "Holofoil": false, "Id": ""6917529953266658450"", "Impact": 60, "Kill Tracker": 10, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Leviathan's Breath", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Big-Game Hunter*", "Chain Bowstring*", "Fiberglass Arrow Shaft*", "Leviathan's Sigh*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Exotic", "Recoil": 75, "Reload": 0, "Season": 8, "Shield Duration": 0, "Source": "", "Stability": 5, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 3, "Zoom": 20, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "heavy", "Ammo Generation": 39, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 1491665733, "Holofoil": false, "Id": ""6917529953268332123"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Briar's Contempt", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Adaptive Burst*", "Extended Barrel", "Fluted Barrel*", "Accelerated Coils", "Ionized Battery*", "Demolitionist*", "Frenzy*", "Harmonic Resonance*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 0, "Range": 53, "Rarity": "Legendary", "Recoil": 74, "Reload": 5, "Season": 20, "Shield Duration": 0, "Source": "rootofnightmares", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 23, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "special", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 17, "Hash": 3871226707, "Holofoil": false, "Id": ""6917529953534853436"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Mechabre", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Extended Barrel*", "Smallbore", "Appended Mag*", "Tactical Mag", "Demolitionist*", "Voltshot*", "Search Party*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 72, "Range": 84, "Rarity": "Legendary", "Recoil": 87, "Reload": 30, "Season": 22, "Shield Duration": 0, "Source": "events", "Stability": 28, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 6, "Zoom": 45, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "heavy", "Ammo Generation": 53, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 2828278545, "Holofoil": false, "Id": ""6917529954740819635"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 71, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Song of Ir Yût", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Smallbore", "Extended Mag*", "Armor-Piercing Rounds", "Reconstruction*", "Target Lock*", "Cursed Thrall*", "Kill Tracker", "Tier 3: Range*", ], "Power": 10, "ROF": 450, "Range": 62, "Rarity": "Legendary", "Recoil": 90, "Reload": 37, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 100, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "special", "Ammo Generation": 49, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 1471212226, "Holofoil": false, "Id": ""6917529954747192952"", "Impact": 6, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 74, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Acasia's Dejection", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Polygonal Rifling*", "Enhanced Battery*", "Hip-Fire Grip*", "Frenzy*", "Harmonic Resonance*", "Kill Tracker", ], "Power": 10, "ROF": 1000, "Range": 69, "Rarity": "Legendary", "Recoil": 96, "Reload": 53, "Season": 20, "Shield Duration": 0, "Source": "rootofnightmares", "Stability": 90, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 8, "Ammo": "special", "Ammo Generation": 21, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 967, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 26, "Hash": 3347946548, "Holofoil": false, "Id": ""6917529954749865955"", "Impact": 95, "Kill Tracker": 19, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "The Eremite", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Polygonal Rifling*", "Enhanced Battery*", "Compulsive Reloader*", "High Ground*", "Head Rush*", "Kill Tracker", ], "Power": 10, "ROF": 0, "Range": 51, "Rarity": "Legendary", "Recoil": 75, "Reload": 22, "Season": 22, "Shield Duration": 0, "Source": "seasonpass", "Stability": 32, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 27, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 34, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 43, "Hash": 1081724548, "Holofoil": false, "Id": ""6917529954749867270"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 32, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Rapacious Appetite", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Extended Barrel*", "Appended Mag*", "Envious Assassin*", "Headstone*", "Kill Tracker", "Unsated Hunger*", ], "Power": 10, "ROF": 720, "Range": 53, "Rarity": "Legendary", "Recoil": 100, "Reload": 22, "Season": 21, "Shield Duration": 0, "Source": "dungeon", "Stability": 6, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 64, "Archetype": "Lightweight Frame", "Blast Radius": 100, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 268260373, "Holofoil": false, "Id": ""6917529954749867467"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Prodigal Return", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Confined Launch*", "Implosion Rounds*", "Turnabout*", "Danger Zone*", "Noble Deeds*", "Kill Tracker", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Legendary", "Recoil": 78, "Reload": 62, "Season": 20, "Shield Duration": 0, "Source": "seasonpass", "Stability": 53, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 73, "Year": 6, "Zoom": 13, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "special", "Ammo Generation": 39, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 2, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 55, "Hash": 1769847435, "Holofoil": false, "Id": ""6917529954749869357"", "Impact": 55, "Kill Tracker": 10, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "A Distant Pull", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Rapid-Fire Frame*", "Extended Barrel*", "Steady Rounds*", "Keep Away*", "Headstone*", "Kill Tracker", "Unsated Hunger*", "Counterbalance Stock*", ], "Power": 10, "ROF": 140, "Range": 35, "Rarity": "Legendary", "Recoil": 83, "Reload": 56, "Season": 21, "Shield Duration": 0, "Source": "sonar", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 6, "Zoom": 35, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 70, "Archetype": "Edge of Concurrence", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 55, "Hash": 542203595, "Holofoil": false, "Id": ""6917529954752810314"", "Impact": 55, "Kill Tracker": 9, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 10, "Masterwork Type": undefined, "Name": "Edge of Concurrence", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Edge of Concurrence*", "Ballistic Tuning*", "Alloy Magazine*", "Lightning Seeker*", "Hand-Laid Stock*", "Kill Tracker", ], "Power": 10, "ROF": 80, "Range": 70, "Rarity": "Exotic", "Recoil": 0, "Reload": 80, "Season": 16, "Shield Duration": 10, "Source": "evidenceboard", "Stability": 0, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Glaive", "Velocity": 0, "Year": 5, "Zoom": 0, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 40, "Archetype": "High-Impact Frame", "Blast Radius": 90, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 84, "Hash": 2922749929, "Holofoil": false, "Id": ""6917529955052633126"", "Impact": 0, "Kill Tracker": 7, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Semiotician", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "High-Impact Frame*", "Hard Launch", "Quick Launch*", "Implosion Rounds", "Impact Casing*", "Impulse Amplifier*", "Explosive Light*", "Head Rush*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Legendary", "Recoil": 60, "Reload": 39, "Season": 22, "Shield Duration": 0, "Source": "seasonpass", "Stability": 66, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 54, "Year": 6, "Zoom": 20, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 42, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 64, "Hash": 781498181, "Holofoil": false, "Id": ""6917529955062931641"", "Impact": 55, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Persuader", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Rapid-Fire Frame*", "Hammer-Forged Rifling*", "Smallbore", "Appended Mag*", "Steady Rounds", "Rapid Hit*", "Triple Tap*", "Vanguard's Vindication*", "Nadir Focus", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 140, "Range": 42, "Rarity": "Legendary", "Recoil": 60, "Reload": 57, "Season": 22, "Shield Duration": 0, "Source": "seasonpass", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 6, "Zoom": 35, }, { "AA": 85, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 34, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 1916287826, "Holofoil": false, "Id": ""6917529955065803140"", "Impact": 51, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Boudica-C", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Extended Barrel", "Ricochet Rounds*", "Flared Magwell", "Threat Detector*", "Osmosis*", "Häkke Breach Armaments*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 260, "Range": 73, "Rarity": "Legendary", "Recoil": 98, "Reload": 26, "Season": 18, "Shield Duration": 0, "Source": "engram", "Stability": 58, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 43, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 667, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 1084190509, "Holofoil": false, "Id": ""6917529955672363645"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "Pressurized Precision", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Smallbore", "Accelerated Coils*", "Enhanced Battery", "Discord*", "High-Impact Reserves*", "Skulking Wolf*", "Kill Tracker", "Tier 4: Handling*", ], "Power": 10, "ROF": 0, "Range": 40, "Rarity": "Legendary", "Recoil": 85, "Reload": 40, "Season": 21, "Shield Duration": 0, "Source": "ironbanner", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 26, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 42, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 29, "Hash": 1975125963, "Holofoil": false, "Id": ""6917529955693329647"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 44, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Qua Xaphan V", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Chambered Compensator", "Full Bore*", "Alloy Magazine*", "High-Caliber Rounds", "Dynamic Sway Reduction*", "High Ground*", "Gun and Run", "Wild Card*", "Kill Tracker", "Tier 3: Range*", ], "Power": 10, "ROF": 360, "Range": 83, "Rarity": "Legendary", "Recoil": 85, "Reload": 35, "Season": 22, "Shield Duration": 0, "Source": "gambit", "Stability": 10, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 82, "Accuracy": 25, "Airborne Effectiveness": 7, "Ammo": "primary", "Ammo Generation": 64, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 567, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 92, "Hash": 2513965917, "Holofoil": false, "Id": ""6917529958124753876"", "Impact": 68, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Lunulata-4b", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Lightweight Frame*", "Agile Bowstring*", "Elastic String", "Compact Arrow Shaft*", "Fiberglass Arrow Shaft", "Shoot to Loot*", "Wellspring*", "Veist Stinger*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 49, "Reload": 70, "Season": 17, "Shield Duration": 0, "Source": "engram", "Stability": 67, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 50, "Archetype": "Composite Propellant", "Blast Radius": 100, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 92, "Hash": 17096506, "Holofoil": false, "Id": ""6917529960093172744"", "Impact": 0, "Kill Tracker": 88, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Dragon's Breath", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Composite Propellant*", "Volatile Launch*", "Black Powder*", "High Octane*", "Short-Action Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Exotic", "Recoil": 55, "Reload": 37, "Season": 23, "Shield Duration": 0, "Source": "seasonpass", "Stability": 27, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 67, "Year": 6, "Zoom": 20, }, { "AA": 100, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 43, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 4153087276, "Holofoil": false, "Id": ""6917529960417819579"", "Impact": 6, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 74, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Appetence", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Fluted Barrel", "Enhanced Battery*", "Light Battery", "Clown Cartridge*", "One for All*", "Dragon's Vengeance*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 1000, "Range": 70, "Rarity": "Legendary", "Recoil": 100, "Reload": 49, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 75, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 67, "Hash": 3583275737, "Holofoil": false, "Id": ""6917529964620541925"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 39, "Masterwork Tier": 5, "Masterwork Type": "Reload Speed", "Name": "Ros Arago IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Extended Barrel", "Accurized Rounds*", "Alloy Magazine", "Subsistence*", "Onslaught*", "Wild Card*", "Kill Tracker", "Tier 5: Reload Speed*", ], "Power": 10, "ROF": 600, "Range": 52, "Rarity": "Legendary", "Recoil": 77, "Reload": 61, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 77, "Accuracy": 28, "Airborne Effectiveness": 16, "Ammo": "primary", "Ammo Generation": 63, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 500, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 68, "Hash": 3710082365, "Holofoil": false, "Id": ""6917529964630997314"", "Impact": 68, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Lethophobia", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Elastic String*", "Fiberglass Arrow Shaft*", "Repulsor Brace*", "Golden Tricorn*", "Dragon's Vengeance*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 52, "Reload": 60, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 6, "Zoom": 18, }, { "AA": 72, "Accuracy": 66, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 667, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 71, "Hash": 839344841, "Holofoil": false, "Id": ""6917529966164749712"", "Impact": 76, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Vengeful Whisper", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Precision Frame*", "Agile Bowstring*", "Elastic String", "Natural Fletching*", "Straight Fletching", "Explosive Head*", "Offhand Strike*", "Sundering*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 79, "Reload": 40, "Season": 23, "Shield Duration": 0, "Source": "dungeon", "Stability": 70, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 6, "Zoom": 19, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 60, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 4233375372, "Holofoil": false, "Id": ""6917529966179685123"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 57, "Masterwork Tier": 5, "Masterwork Type": "Stability", "Name": "Marcato-45", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Polygonal Rifling", "Appended Mag*", "Steady Rounds", "Slice*", "Golden Tricorn*", "Suros Synergy*", "Kill Tracker", "Tier 5: Stability*", ], "Power": 10, "ROF": 450, "Range": 45, "Rarity": "Legendary", "Recoil": 90, "Reload": 49, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 72, "Accuracy": 28, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 78, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 567, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 83, "Hash": 3849444474, "Holofoil": false, "Id": ""6917529966363854538"", "Impact": 68, "Kill Tracker": 33, "Loadouts": "", "Locked": false, "Mag": 0, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Tripwire Canary", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Agile Bowstring*", "Carbon Arrow Shaft*", "Sneak Bow*", "Explosive Head*", "Ambush*", "Veist Stinger", "Kill Tracker", "Dusted Tome*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 55, "Reload": 60, "Season": 19, "Shield Duration": 0, "Source": "rasputin", "Stability": 69, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 15, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 29, "Hash": 1937552980, "Holofoil": false, "Id": ""6917529966363855101"", "Impact": 62, "Kill Tracker": 1245, "Loadouts": "", "Locked": false, "Mag": 16, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Doom of Chelchis", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Alloy Magazine*", "Rangefinder*", "Unrelenting*", "Runneth Over*", "Kill Tracker", "Perfectly Balanced*", ], "Power": 10, "ROF": 180, "Range": 59, "Rarity": "Legendary", "Recoil": 86, "Reload": 44, "Season": 18, "Shield Duration": 0, "Source": "kingsfall", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 5, "Zoom": 20, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "heavy", "Ammo Generation": 34, "Archetype": "Adaptive Frame", "Blast Radius": 50, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 268260372, "Holofoil": false, "Id": ""6917529966363858797"", "Impact": 0, "Kill Tracker": 8, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Regnant", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Countermass*", "Augmented Drum*", "Rangefinder*", "Unrelenting*", "Noble Deeds*", "Kill Tracker", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 100, "Reload": 19, "Season": 20, "Shield Duration": 0, "Source": "seasonpass", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 28, "Year": 6, "Zoom": 13, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 56, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 24, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 32, "Hash": 1392919471, "Holofoil": false, "Id": ""6917529966368736121"", "Impact": 45, "Kill Tracker": 1520, "Loadouts": "", "Locked": true, "Mag": 17, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Trustee", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Hammer-Forged Rifling*", "Flared Magwell*", "Surplus*", "High-Impact Reserves*", "Bray Inheritance*", "Kill Tracker", "Counterbalance Stock*", "Nightfall Memento*", ], "Power": 10, "ROF": 260, "Range": 47, "Rarity": "Legendary", "Recoil": 74, "Reload": 56, "Season": 12, "Shield Duration": 0, "Source": "deepstonecrypt", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 4, "Zoom": 19, }, { "AA": 60, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 35, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 28, "Hash": 3890055324, "Holofoil": false, "Id": ""6917529966368739437"", "Impact": 92, "Kill Tracker": 2463, "Loadouts": "", "Locked": true, "Mag": 11, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Targeted Redaction", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Smallbore*", "Flared Magwell*", "Outlaw*", "Destabilizing Rounds*", "Kill Tracker", "Unsated Hunger*", "Backup Mag*", "Valor at Dusk*", ], "Power": 10, "ROF": 120, "Range": 65, "Rarity": "Legendary", "Recoil": 99, "Reload": 50, "Season": 21, "Shield Duration": 0, "Source": "sonar", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 100, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "special", "Ammo Generation": 47, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 6, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 548958835, "Holofoil": false, "Id": ""6917529966368740894"", "Impact": 6, "Kill Tracker": 212, "Loadouts": "", "Locked": false, "Mag": 104, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Retraced Path", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Enhanced Battery*", "Killing Wind*", "One for All*", "Hot Swap*", "Kill Tracker", "Backup Mag*", "Gambit Memento*", "Mind's Eye*", ], "Power": 10, "ROF": 1000, "Range": 74, "Rarity": "Legendary", "Recoil": 100, "Reload": 45, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 75, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 4, "Zoom": 16, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 2, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 2779821308, "Holofoil": false, "Id": ""6917529966371114341"", "Impact": 62, "Kill Tracker": 95, "Loadouts": "", "Locked": false, "Mag": 16, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Brya's Love", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Alloy Magazine*", "Keep Away*", "Precision Instrument*", "Head Rush*", "Kill Tracker", ], "Power": 10, "ROF": 180, "Range": 58, "Rarity": "Legendary", "Recoil": 88, "Reload": 40, "Season": 22, "Shield Duration": 0, "Source": "seasonpass", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 20, }, { "AA": 87, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 25, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 484515708, "Holofoil": false, "Id": ""6917529966376040699"", "Impact": 18, "Kill Tracker": 1639, "Loadouts": "", "Locked": false, "Mag": 53, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Rufus's Fury", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Flared Magwell*", "Rewind Rounds*", "Target Lock*", "Harmonic Resonance*", "Kill Tracker", "Targeting Adjuster*", "Dark Omolon*", ], "Power": 10, "ROF": 720, "Range": 36, "Rarity": "Legendary", "Recoil": 86, "Reload": 75, "Season": 20, "Shield Duration": 0, "Source": "rootofnightmares", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 45, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 61, "Hash": 1874424704, "Holofoil": false, "Id": ""6917529966400176064"", "Impact": 55, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Twilight Oath", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Full Bore*", "Polygonal Rifling", "Tactical Mag*", "Alloy Magazine", "Heal Clip*", "Kill Clip*", "Advanced Reflexes*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 140, "Range": 42, "Rarity": "Legendary", "Recoil": 59, "Reload": 68, "Season": 23, "Shield Duration": 0, "Source": "dreaming", "Stability": 31, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 6, "Zoom": 35, }, { "AA": 100, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 43, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 67, "Hash": 4153087276, "Holofoil": false, "Id": ""6917529966428255247"", "Impact": 6, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 104, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Appetence", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Smallbore", "Ionized Battery*", "Light Battery", "Overflow*", "Killing Tally*", "Dragon's Vengeance*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 1000, "Range": 70, "Rarity": "Legendary", "Recoil": 93, "Reload": 26, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 80, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "special", "Ammo Generation": 21, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 967, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 8, "Hash": 2767393525, "Holofoil": false, "Id": ""6917529966432741427"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Nox Perennial V", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Extended Barrel*", "Polygonal Rifling", "Accelerated Coils*", "Particle Repeater", "Fragile Focus*", "Hatchling*", "Wild Card*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 55, "Rarity": "Legendary", "Recoil": 75, "Reload": 18, "Season": 22, "Shield Duration": 0, "Source": "engram", "Stability": 24, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 49, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 26, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 51, "Hash": 1679868061, "Holofoil": false, "Id": ""6917529966446564281"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Wastelander M5", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Rifled Barrel*", "Corkscrew Rifling", "Tactical Mag*", "Extended Mag", "Subsistence*", "One-Two Punch*", "Hot Swap*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 80, "Range": 56, "Rarity": "Legendary", "Recoil": 51, "Reload": 69, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 4, "Zoom": 12, }, { "AA": 73, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 75, "Hash": 355922321, "Holofoil": false, "Id": ""6917529967273735687"", "Impact": 60, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 15, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Vouchsafe", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Polygonal Rifling", "Accurized Rounds*", "Extended Mag", "Outlaw*", "Attrition Orbs*", "Advanced Reflexes*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 200, "Range": 43, "Rarity": "Legendary", "Recoil": 77, "Reload": 55, "Season": 23, "Shield Duration": 0, "Source": "dreaming", "Stability": 32, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 20, }, { "AA": 90, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 40, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "The Dawning", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 4220529694, "Holofoil": false, "Id": ""6917529967294387435"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 57, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Avalanche", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Alloy Magazine*", "Flared Magwell", "Subsistence*", "Cascade Point*", "Dawning Surprise*", "Suros Synergy", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 450, "Range": 55, "Rarity": "Legendary", "Recoil": 70, "Reload": 60, "Season": 23, "Shield Duration": 0, "Source": "events", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 19, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 20, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "The Dawning", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 80, "Handling": 0, "Hash": 1911078836, "Holofoil": false, "Id": ""6917529967305968227"", "Impact": 59, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 74, "Masterwork Tier": 2, "Masterwork Type": "Impact", "Name": "Zephyr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Enduring Blade*", "Tempered Edge", "Burst Guard*", "Enduring Guard", "Turnabout*", "Whirlwind Blade*", "Dawning Surprise*", "Kill Tracker", "Tier 2: Impact*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 23, "Shield Duration": 0, "Source": "events", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 6, "Zoom": 0, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "primary", "Ammo Generation": 44, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 2510526114, "Holofoil": false, "Id": ""6917529968237528203"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Unending Tempest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Extended Mag*", "Steady Rounds", "Dynamic Sway Reduction*", "Target Lock*", "One Quiet Moment*", "Nadir Focus", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 600, "Range": 53, "Rarity": "Legendary", "Recoil": 85, "Reload": 0, "Season": 22, "Shield Duration": 0, "Source": "crucible", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "special", "Ammo Generation": 21, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 967, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 13, "Hash": 2767393525, "Holofoil": false, "Id": ""6917529968262762438"", "Impact": 95, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Nox Perennial V", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Full Bore*", "Hammer-Forged Rifling", "Enhanced Battery*", "Liquid Coils", "Fragile Focus*", "Controlled Burst*", "Wild Card*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 60, "Rarity": "Legendary", "Recoil": 65, "Reload": 17, "Season": 22, "Shield Duration": 0, "Source": "engram", "Stability": 14, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 26, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 967, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "The Dawning", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 2728851518, "Holofoil": false, "Id": ""6917529968269102566"", "Impact": 95, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Glacioclasm", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Hammer-Forged Rifling*", "Polygonal Rifling", "Enhanced Battery*", "Liquid Coils", "Heating Up*", "Overflow", "Eye of the Storm*", "Successful Warm-Up", "Dawning Surprise*", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 0, "Range": 73, "Rarity": "Legendary", "Recoil": 78, "Reload": 15, "Season": 23, "Shield Duration": 0, "Source": "events", "Stability": 30, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 73, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "special", "Ammo Generation": 51, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 80, "Hash": 2164448701, "Holofoil": false, "Id": ""6917529968274755447"", "Impact": 55, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Apostate", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Fluted Barrel*", "Extended Mag*", "No Distractions*", "Explosive Payload*", "Kill Tracker", "Masterworked: Range*", ], "Power": 10, "ROF": 140, "Range": 46, "Rarity": "Legendary", "Recoil": 49, "Reload": 41, "Season": 8, "Shield Duration": 0, "Source": "moon", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 3, "Zoom": 40, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 26, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 967, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "The Dawning", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 48, "Hash": 2728851518, "Holofoil": false, "Id": ""6917529968542364035"", "Impact": 95, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 4, "Masterwork Type": "Reload Speed", "Name": "Glacioclasm", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Enhanced Battery*", "Liquid Coils", "Subsistence*", "Repulsor Brace", "High-Impact Reserves*", "Dawning Surprise*", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 4: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 63, "Rarity": "Legendary", "Recoil": 78, "Reload": 19, "Season": 23, "Shield Duration": 0, "Source": "events", "Stability": 34, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 31, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "heavy", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 15, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 17, "Hash": 1719687748, "Holofoil": false, "Id": ""6917529968544628174"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 2, "Masterwork Type": "Velocity", "Name": "Crux Termination IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Hard Launch*", "Smart Drift Control", "Alloy Casing*", "Implosion Rounds", "Eddy Current*", "Tracking Module*", "Wild Card*", "Kill Tracker", "Tier 2: Velocity*", ], "Power": 10, "ROF": 25, "Range": 0, "Rarity": "Legendary", "Recoil": 80, "Reload": 87, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 3, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 89, "Year": 6, "Zoom": 20, }, { "AA": 33, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 32, "Hash": 2045811635, "Holofoil": false, "Id": ""6917529993277119010"", "Impact": 67, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 15, "Masterwork Tier": 5, "Masterwork Type": "Stability", "Name": "Imperative", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Arrowhead Brake*", "Corkscrew Rifling", "Appended Mag*", "Steady Rounds", "Keep Away*", "Opening Shot*", "Nano-Munitions*", "Kill Tracker", "Tier 5: Stability*", ], "Power": 10, "ROF": 150, "Range": 65, "Rarity": "Legendary", "Recoil": 100, "Reload": 30, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 25, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 21, }, { "AA": 70, "Accuracy": 67, "Airborne Effectiveness": 14, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Snareweaver", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 4, "Draw Time": 667, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 2910326942, "Holofoil": false, "Id": ""6917529993278864107"", "Impact": 76, "Kill Tracker": 251, "Loadouts": "", "Locked": false, "Mag": 0, "Masterwork Tier": 10, "Masterwork Type": undefined, "Name": "Wish-Keeper", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Snareweaver*", "Agile Bowstring*", "Carbon Arrow Shaft*", "Silkbound Slayer*", "Heavy Grip*", "Kill Tracker", "Immaterial Messenger*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Exotic", "Recoil": 80, "Reload": 40, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 72, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 6, "Zoom": 18, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "Guardian Games", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 84, "Hash": 3007479950, "Holofoil": false, "Id": ""6917529993280923730"", "Impact": 60, "Kill Tracker": 49, "Loadouts": "", "Locked": true, "Mag": 15, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Taraxippos", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Fluted Barrel*", "Full Bore", "Accurized Rounds*", "Flared Magwell", "Enlightened Action*", "Precision Instrument*", "Classy Contender*", "Omolon Fluid Dynamics", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 200, "Range": 40, "Rarity": "Legendary", "Recoil": 54, "Reload": 59, "Season": 23, "Shield Duration": 0, "Source": "events", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 19, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 27, "Archetype": "Compressed Wave Frame", "Blast Radius": 50, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Guardian Games", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 51, "Hash": 657927352, "Holofoil": false, "Id": ""6917529993575217880"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Hullabaloo", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Compressed Wave Frame*", "Countermass*", "Hard Launch", "High-Velocity Rounds*", "Implosion Rounds", "Voltshot*", "Chain Reaction*", "Classy Contender*", "Kill Tracker", "Freehand Grip*", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 100, "Reload": 50, "Season": 23, "Shield Duration": 0, "Source": "events", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 38, "Year": 6, "Zoom": 13, }, { "AA": 32, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 34, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "Guardian Games", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 655712834, "Holofoil": false, "Id": ""6917529993578402634"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 32, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "The Title", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Armor-Piercing Rounds*", "Light Mag", "Threat Detector*", "Destabilizing Rounds*", "Classy Contender*", "Häkke Breach Armaments", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 720, "Range": 51, "Rarity": "Legendary", "Recoil": 85, "Reload": 23, "Season": 23, "Shield Duration": 0, "Source": "events", "Stability": 25, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 81, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Big Frigid Glaive", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 0, "Hash": 3118061004, "Holofoil": false, "Id": ""6917529993587809115"", "Impact": 95, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Winterbite", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Big Frigid Glaive*", "Supercooled Accelerator*", "Alloy Magazine*", "Weighted Edge*", "Tilting at Windmills*", "Kill Tracker", ], "Power": 10, "ROF": 45, "Range": 85, "Rarity": "Exotic", "Recoil": 0, "Reload": 5, "Season": 20, "Shield Duration": 39, "Source": "exoticquest", "Stability": 0, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Glaive", "Velocity": 0, "Year": 6, "Zoom": 0, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 26, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 1331824604, "Holofoil": false, "Id": ""6917529993591232476"", "Impact": 92, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 10, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Combined Action", "New Gear": false, "Notes": undefined, "Owner": "Warlock(10)", "Perks": [ "Aggressive Frame*", "Arrowhead Brake*", "Corkscrew Rifling", "Extended Mag*", "Alloy Magazine", "Eddy Current*", "Voltshot*", "Field-Tested*", "Kill Tracker", "Tier 3: Range*", ], "Power": 10, "ROF": 120, "Range": 57, "Rarity": "Legendary", "Recoil": 100, "Reload": 12, "Season": 21, "Shield Duration": 0, "Source": "engram", "Stability": 26, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 20, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 80, "Handling": 0, "Hash": 105164264, "Holofoil": false, "Id": ""6917529993595353501"", "Impact": 58, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 72, "Masterwork Tier": 1, "Masterwork Type": "Impact", "Name": "Double-Edged Answer", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Enduring Blade*", "Honed Edge", "Burst Guard*", "Swordmaster's Guard", "Wellspring*", "Destabilizing Rounds*", "Vanguard's Vindication*", "Nadir Focus", "Kill Tracker", "Tier 1: Impact*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 23, "Shield Duration": 0, "Source": "strikes", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 6, "Zoom": 0, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 30, "Hash": 2465372924, "Holofoil": false, "Id": ""6917530007102646435"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 31, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Tigerspite", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Full Bore", "Ricochet Rounds*", "Flared Magwell", "Subsistence*", "Eye of the Storm*", "Advanced Reflexes*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 450, "Range": 77, "Rarity": "Legendary", "Recoil": 78, "Reload": 41, "Season": 23, "Shield Duration": 0, "Source": "dreaming", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 43, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 29, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 1106635211, "Holofoil": false, "Id": ""6917530008180509146"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 3, "Masterwork Tier": 5, "Masterwork Type": "Reload Speed", "Name": "Last Foray", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Arrowhead Brake*", "Extended Barrel", "Accurized Rounds*", "Tactical Mag", "Lead from Gold*", "Incandescent*", "Field-Tested*", "Kill Tracker", "Tier 5: Reload Speed*", ], "Power": 10, "ROF": 72, "Range": 80, "Rarity": "Legendary", "Recoil": 100, "Reload": 32, "Season": 21, "Shield Duration": 0, "Source": "engram", "Stability": 27, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 6, "Zoom": 55, }, { "AA": 76, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 1875512595, "Holofoil": false, "Id": ""6917530009485550530"", "Impact": 84, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Kept Confidence", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Smallbore", "Appended Mag*", "Flared Magwell", "Loose Change*", "Harmony*", "Head Rush*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 140, "Range": 46, "Rarity": "Legendary", "Recoil": 91, "Reload": 41, "Season": 22, "Shield Duration": 0, "Source": "seasonpass", "Stability": 61, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 63, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 61, "Hash": 1447836603, "Holofoil": false, "Id": ""6917530009485550990"", "Impact": 15, "Kill Tracker": 4, "Loadouts": "", "Locked": true, "Mag": 42, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Subjunctive", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Chambered Compensator*", "Extended Barrel", "Tactical Mag*", "Flared Magwell", "Threat Detector*", "Voltshot*", "Nano-Munitions*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 900, "Range": 28, "Rarity": "Legendary", "Recoil": 100, "Reload": 38, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 13, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 62, "Hash": 3678653083, "Holofoil": false, "Id": ""6917530009500030632"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 41, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "Old Sterling", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Steady Rounds*", "Alloy Magazine", "Discord*", "Demolitionist*", "Field-Tested*", "Kill Tracker", "Tier 4: Handling*", ], "Power": 10, "ROF": 600, "Range": 38, "Rarity": "Legendary", "Recoil": 73, "Reload": 55, "Season": 21, "Shield Duration": 0, "Source": "engram", "Stability": 67, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 29, "Accuracy": 0, "Airborne Effectiveness": 7, "Ammo": "heavy", "Ammo Generation": 53, "Archetype": "Rapid-Fire Frame", "Blast Radius": 16, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 16, "Hash": 2721157927, "Holofoil": false, "Id": ""6917530009511078656"", "Impact": 0, "Kill Tracker": 6, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Blast Radius", "Name": "Tarnation", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Volatile Launch*", "Proximity Grenades*", "High-Velocity Rounds", "Field Prep*", "Chain Reaction*", "Psychohack*", "Kill Tracker", "Tier 1: Blast Radius*", ], "Power": 10, "ROF": 150, "Range": 0, "Rarity": "Legendary", "Recoil": 63, "Reload": 24, "Season": 16, "Shield Duration": 0, "Source": "throneworld", "Stability": 23, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 48, "Year": 5, "Zoom": 13, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 34, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 30, "Hash": 1501688142, "Holofoil": false, "Id": ""6917530015026434735"", "Impact": 44, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Doomed Petitioner", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Burst*", "Corkscrew Rifling*", "Liquid Coils*", "Reconstruction*", "Golden Tricorn*", "Dragon's Vengeance*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 50, "Rarity": "Legendary", "Recoil": 71, "Reload": 25, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 25, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "special", "Ammo Generation": 46, "Archetype": "Pinpoint Slug Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 61, "Hash": 92459755, "Holofoil": false, "Id": ""6917530015055614129"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Supercluster", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Pinpoint Slug Frame*", "Extended Barrel*", "Smallbore", "Steady Rounds*", "Light Mag", "Threat Detector*", "Surrounded*", "Dragon's Vengeance*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 65, "Range": 74, "Rarity": "Legendary", "Recoil": 69, "Reload": 45, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 64, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 82, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 46, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 78, "Hash": 3723679465, "Holofoil": false, "Id": ""6917530015059343202"", "Impact": 84, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 14, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Waking Vigil", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Polygonal Rifling", "Appended Mag*", "Flared Magwell", "Perpetual Motion*", "Voltshot*", "Advanced Reflexes*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 140, "Range": 37, "Rarity": "Legendary", "Recoil": 98, "Reload": 56, "Season": 23, "Shield Duration": 0, "Source": "dreaming", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 85, "Accuracy": 0, "Airborne Effectiveness": 28, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Legacy PR-55 Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 61, "Hash": 2708806099, "Holofoil": false, "Id": ""6917530017567596221"", "Impact": 23, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 33, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "BxR-55 Battler", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Legacy PR-55 Frame*", "Extended Barrel*", "Steady Rounds*", "Heating Up*", "Eye of the Storm*", "Hot Swap*", "Kill Tracker", "Counterbalance Stock*", "Dawning Memento*", ], "Power": 10, "ROF": 450, "Range": 55, "Rarity": "Legendary", "Recoil": 95, "Reload": 63, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 74, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 4, "Zoom": 20, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 20, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 90, "Guard Resistance": 0, "Handling": 0, "Hash": 3257091166, "Holofoil": false, "Id": ""6917530017567598347"", "Impact": 61, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 70, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Half-Truths", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Tempered Edge*", "Enduring Guard*", "Unrelenting*", "Assassin's Blade*", "Hot Swap*", "Kill Tracker", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 15, "Shield Duration": 0, "Source": "30th", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 4, "Zoom": 0, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "special", "Ammo Generation": 38, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 21, "Hash": 871900124, "Holofoil": false, "Id": ""6917530017571193705"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Retold Tale", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Rifled Barrel*", "Barrel Shroud", "Extended Mag*", "Light Mag", "Repulsor Brace*", "Destabilizing Rounds*", "Advanced Reflexes*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 65, "Range": 75, "Rarity": "Legendary", "Recoil": 64, "Reload": 21, "Season": 23, "Shield Duration": 0, "Source": "", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 39, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 52, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 533, "Crafted": "crafted", "Crafted Level": 20, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 2558925366, "Holofoil": false, "Id": ""6917530018775417223"", "Impact": 55, "Kill Tracker": 366, "Loadouts": "", "Locked": false, "Mag": 8, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Scatter Signal", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Rapid-Fire Frame*", "Extended Barrel*", "Enhanced Battery*", "Perpetual Motion*", "Under-Over*", "Dragon's Vengeance*", "Kill Tracker", ], "Power": 10, "ROF": 0, "Range": 33, "Rarity": "Legendary", "Recoil": 69, "Reload": 46, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 36, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 76, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 47, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 4, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 2563668388, "Holofoil": false, "Id": ""6917530018784852940"", "Impact": 23, "Kill Tracker": 317, "Loadouts": "", "Locked": false, "Mag": 33, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Scalar Potential", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Polygonal Rifling*", "Steady Rounds*", "Loose Change*", "Under-Over*", "Dragon's Vengeance*", "Kill Tracker", ], "Power": 10, "ROF": 540, "Range": 23, "Rarity": "Legendary", "Recoil": 53, "Reload": 35, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 69, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 6, "Zoom": 17, }, { "AA": 100, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "special", "Ammo Generation": 41, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 2827764482, "Holofoil": false, "Id": ""6917530018787453125"", "Impact": 6, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 74, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Path of Least Resistance", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Polygonal Rifling*", "Enhanced Battery*", "Hip-Fire Grip*", "Harmony*", "Ambush*", "Omolon Fluid Dynamics", "Kill Tracker", ], "Power": 10, "ROF": 1000, "Range": 69, "Rarity": "Legendary", "Recoil": 95, "Reload": 40, "Season": 19, "Shield Duration": 0, "Source": "rasputin", "Stability": 83, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 48, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "special", "Ammo Generation": 49, "Archetype": "Pinpoint Slug Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 2, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 135029084, "Holofoil": false, "Id": ""6917530018787458483"", "Impact": 70, "Kill Tracker": 24, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Nessa's Oblation", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Pinpoint Slug Frame*", "Extended Barrel*", "Steady Rounds*", "Compulsive Reloader*", "Frenzy*", "Harmonic Resonance*", "Kill Tracker", ], "Power": 10, "ROF": 65, "Range": 68, "Rarity": "Legendary", "Recoil": 69, "Reload": 49, "Season": 20, "Shield Duration": 0, "Source": "rootofnightmares", "Stability": 62, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "heavy", "Ammo Generation": 29, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 3103325054, "Holofoil": false, "Id": ""6917530018787460587"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 74, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Retrofit Escapade", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Extended Barrel*", "Steady Rounds*", "Zen Moment*", "Frenzy*", "Ambush*", "Suros Synergy", "Kill Tracker", ], "Power": 10, "ROF": 900, "Range": 28, "Rarity": "Legendary", "Recoil": 65, "Reload": 67, "Season": 19, "Shield Duration": 0, "Source": "seasonpass", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 45, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 40, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 4100775158, "Holofoil": false, "Id": ""6917530018804513592"", "Impact": 20, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 41, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Pizzicato-22", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Smallbore", "Appended Mag*", "Light Mag", "Perpetual Motion*", "Multikill Clip*", "Suros Synergy*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 900, "Range": 48, "Rarity": "Legendary", "Recoil": 90, "Reload": 51, "Season": 18, "Shield Duration": 0, "Source": "engram", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 14, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "special", "Ammo Generation": 38, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 800, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 44, "Hash": 4114929480, "Holofoil": false, "Id": ""6917530018807533950"", "Impact": 85, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Snorri FR5", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Full Bore", "Liquid Coils*", "Projection Fuse", "Heating Up*", "High-Impact Reserves*", "Omolon Fluid Dynamics*", "Kill Tracker", "Targeting Adjuster*", "Tier 2: Handling*", ], "Power": 10, "ROF": 0, "Range": 66, "Rarity": "Legendary", "Recoil": 79, "Reload": 33, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 17, }, { "AA": 78, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 63, "Archetype": "Lightweight Frame", "Blast Radius": 100, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 4255586669, "Holofoil": false, "Id": ""6917530018807536207"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Empty Vessel", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Volatile Launch*", "Confined Launch", "High-Velocity Rounds*", "Implosion Rounds", "Quickdraw*", "Swashbuckler*", "Vanguard's Vindication*", "Field-Tested", "Kill Tracker", "Targeting Adjuster*", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Legendary", "Recoil": 76, "Reload": 77, "Season": 14, "Shield Duration": 0, "Source": "strikes", "Stability": 24, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 77, "Year": 4, "Zoom": 13, }, { "AA": 69, "Accuracy": 65, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 660, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 192784503, "Holofoil": false, "Id": ""6917530018811248664"", "Impact": 76, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 2, "Masterwork Type": "Draw Time", "Name": "Pre Astyanax IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Tactile String*", "Elastic String", "Natural Fletching*", "Straight Fletching", "Enlightened Action*", "Successful Warm-Up*", "Stunning Recovery*", "Vanguard's Vindication", "Wild Card", "Kill Tracker", "Tier 2: Draw Time*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 69, "Reload": 40, "Season": 22, "Shield Duration": 0, "Source": "nightfall", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 6, "Zoom": 18, }, { "AA": 80, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 6857689, "Holofoil": false, "Id": ""6917530018814180221"", "Impact": 84, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Annual Skate", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fastdraw HCS*", "Sureshot HCS", "Appended Mag*", "Alloy Magazine", "No Distractions*", "Opening Shot*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 140, "Range": 40, "Rarity": "Legendary", "Recoil": 88, "Reload": 38, "Season": 15, "Shield Duration": 0, "Source": "engram", "Stability": 54, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 4, "Zoom": 14, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 38, "Archetype": "Adaptive Frame", "Blast Radius": 46, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 568611923, "Holofoil": false, "Id": ""6917530018820741887"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Blast Radius", "Name": "Edge Transit", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Linear Compensator*", "Smart Drift Control", "Mini Frags*", "Sticky Grenades", "Impulse Amplifier*", "Full Court*", "Indomitability*", "Kill Tracker", "Tier 1: Blast Radius*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 70, "Reload": 49, "Season": 23, "Shield Duration": 0, "Source": "brave", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 37, "Year": 6, "Zoom": 13, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 38, "Archetype": "Adaptive Frame", "Blast Radius": 65, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 568611923, "Holofoil": false, "Id": ""6917530018825412333"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Edge Transit", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Volatile Launch*", "Smart Drift Control", "Sticky Grenades*", "High-Velocity Rounds", "Repulsor Brace*", "Explosive Light*", "Indomitability*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 70, "Reload": 40, "Season": 23, "Shield Duration": 0, "Source": "brave", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 27, "Year": 6, "Zoom": 13, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 67, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 73, "Hash": 1050806815, "Holofoil": false, "Id": ""6917530018829313904"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 41, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "The Recluse", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Corkscrew Rifling", "High-Caliber Rounds*", "Flared Magwell", "Subsistence*", "Master of Arms*", "Indomitability*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 900, "Range": 45, "Rarity": "Legendary", "Recoil": 100, "Reload": 28, "Season": 23, "Shield Duration": 0, "Source": "brave", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 13, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 1453235079, "Holofoil": false, "Id": ""6917530018839856018"", "Impact": 62, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 18, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Hung Jury SR4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Full Bore", "Appended Mag*", "Steady Rounds", "Rewind Rounds*", "Desperate Measures*", "Indomitability*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 180, "Range": 51, "Rarity": "Legendary", "Recoil": 100, "Reload": 46, "Season": 23, "Shield Duration": 0, "Source": "brave", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 22, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "special", "Ammo Generation": 64, "Archetype": "Rapid-Fire Glaive", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 61, "Hash": 1757202961, "Holofoil": false, "Id": ""6917530018892744862"", "Impact": 55, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 4, "Masterwork Type": "Reload Speed", "Name": "Greasy Luck", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Glaive*", "Ballistic Tuning*", "Tempered Truss Rod", "Swap Mag*", "Light Mag", "Lead from Gold*", "Close to Melee*", "Restoration Ritual*", "Kill Tracker", "Tier 4: Reload Speed*", ], "Power": 10, "ROF": 80, "Range": 67, "Rarity": "Legendary", "Recoil": 0, "Reload": 67, "Season": 21, "Shield Duration": 8, "Source": "dungeon", "Stability": 0, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Glaive", "Velocity": 0, "Year": 6, "Zoom": 0, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 63, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 76, "Hash": 1447836603, "Holofoil": false, "Id": ""6917530018896079421"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 45, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Subjunctive", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Fluted Barrel", "Extended Mag*", "Steady Rounds", "Threat Detector*", "Swashbuckler*", "Nano-Munitions*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 900, "Range": 27, "Rarity": "Legendary", "Recoil": 100, "Reload": 8, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 13, }, { "AA": 33, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 27, "Hash": 2045811635, "Holofoil": false, "Id": ""6917530018909852655"", "Impact": 67, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 13, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Imperative", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Steady Rounds*", "Alloy Magazine", "Well-Rounded*", "Osmosis*", "Nano-Munitions*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 150, "Range": 65, "Rarity": "Legendary", "Recoil": 80, "Reload": 30, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 21, }, { "AA": 33, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 17, "Hash": 2045811635, "Holofoil": false, "Id": ""6917530018909860287"", "Impact": 67, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 14, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Imperative", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Chambered Compensator*", "Polygonal Rifling", "Tactical Mag*", "Flared Magwell", "Well-Rounded*", "Explosive Payload*", "Nano-Munitions*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 150, "Range": 66, "Rarity": "Legendary", "Recoil": 90, "Reload": 40, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 21, }, { "AA": 100, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 43, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 4153087276, "Holofoil": false, "Id": ""6917530018919842462"", "Impact": 6, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 74, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Appetence", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Polygonal Rifling", "Enhanced Battery*", "Ionized Battery", "Enlightened Action*", "High Ground*", "Dragon's Vengeance*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 1000, "Range": 74, "Rarity": "Legendary", "Recoil": 100, "Reload": 46, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 85, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 32, "Hash": 2993554824, "Holofoil": false, "Id": ""6917530018919850578"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 36, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Adhortative", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Polygonal Rifling", "Armor-Piercing Rounds*", "Light Mag", "Attrition Orbs*", "Headseeker*", "Nano-Munitions*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 390, "Range": 56, "Rarity": "Legendary", "Recoil": 71, "Reload": 39, "Season": 23, "Shield Duration": 0, "Source": "rivenslair", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 6, "Zoom": 17, }, { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 34, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 3851176026, "Holofoil": true, "Id": ""6917530018927518205"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 36, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Elsie's Rifle", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Corkscrew Rifling*", "Hammer-Forged Rifling", "Extended Mag*", "Light Mag", "Zen Moment*", "Under-Over", "Frenzy*", "Desperado", "Indomitability*", "Kill Tracker", "Masterworked: Reload Speed*", "BRAVE Elsie's Rifle*", ], "Power": 10, "ROF": 340, "Range": 65, "Rarity": "Legendary", "Recoil": 73, "Reload": 50, "Season": 23, "Shield Duration": 0, "Source": "brave", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 6, "Zoom": 17, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 67, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 1050806815, "Holofoil": false, "Id": ""6917530018927521512"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 44, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "The Recluse", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Extended Barrel*", "Polygonal Rifling", "Appended Mag*", "Flared Magwell", "Threat Detector*", "Desperate Measures*", "Indomitability*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 900, "Range": 51, "Rarity": "Legendary", "Recoil": 100, "Reload": 28, "Season": 23, "Shield Duration": 0, "Source": "brave", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 13, }, { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 57, "Hash": 3851176026, "Holofoil": true, "Id": ""6917530018930514200"", "Impact": 33, "Kill Tracker": 6, "Loadouts": "", "Locked": true, "Mag": 33, "Masterwork Tier": 10, "Masterwork Type": "Stability", "Name": "Elsie's Rifle", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "High-Impact Frame*", "Extended Barrel", "Fluted Barrel*", "Appended Mag*", "High-Caliber Rounds", "Repulsor Brace*", "Under-Over", "Desperate Measures*", "Adrenaline Junkie", "Indomitability*", "Kill Tracker", "Masterworked: Stability*", "BRAVE Elsie's Rifle*", ], "Power": 10, "ROF": 340, "Range": 60, "Rarity": "Legendary", "Recoil": 73, "Reload": 60, "Season": 23, "Shield Duration": 0, "Source": "brave", "Stability": 70, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 6, "Zoom": 17, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "special", "Ammo Generation": 25, "Archetype": "Micro-Missile Frame", "Blast Radius": 32, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 55, "Hash": 3947966653, "Holofoil": false, "Id": ""6917530020105555535"", "Impact": 0, "Kill Tracker": 85, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 1, "Masterwork Type": "Blast Radius", "Name": "The Call", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Quick Launch*", "Smart Drift Control", "Tactical Mag*", "Alloy Magazine", "Demolitionist*", "Golden Tricorn*", "Dealer's Choice*", "Kill Tracker", "Tier 1: Blast Radius*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 75, "Reload": 45, "Season": 24, "Shield Duration": 0, "Source": "paleheart", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 70, "Year": 7, "Zoom": 18, }, { "AA": 46, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "primary", "Ammo Generation": 44, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 3926811686, "Holofoil": false, "Id": ""6917530020123421232"", "Impact": 20, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 42, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Parabellum", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Full Bore", "Extended Mag*", "Flared Magwell", "Permeability*", "Dynamic Sway Reduction*", "Field-Tested*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 900, "Range": 53, "Rarity": "Legendary", "Recoil": 92, "Reload": 34, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 32, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 58, "Archetype": "Redemption", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 29, "Hash": 427899681, "Holofoil": false, "Id": ""6917530020153900440"", "Impact": 33, "Kill Tracker": 486, "Loadouts": "", "Locked": true, "Mag": 27, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Red Death Reformed", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Redemption*", "Full Bore*", "High-Caliber Rounds*", "Inverse Relationship*", "Fitted Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 340, "Range": 85, "Rarity": "Exotic", "Recoil": 86, "Reload": 46, "Season": 24, "Shield Duration": 0, "Source": "seasonpass", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 7, "Zoom": 17, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "special", "Ammo Generation": 25, "Archetype": "Micro-Missile Frame", "Blast Radius": 31, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 54, "Hash": 3947966653, "Holofoil": false, "Id": ""6917530024965497358"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 9, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "The Call", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Countermass*", "Quick Launch", "Flared Magwell*", "High-Velocity Rounds", "Strategist*", "Adrenaline Junkie*", "Dealer's Choice*", "Kill Tracker", "Tier 4: Handling*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 100, "Reload": 50, "Season": 24, "Shield Duration": 0, "Source": "paleheart", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 60, "Year": 7, "Zoom": 18, }, { "AA": 64, "Accuracy": 70, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 663, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 3962575203, "Holofoil": false, "Id": ""6917530025505121887"", "Impact": 76, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Draw Time", "Name": "Hush", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Tactile String*", "Polymer String", "Natural Fletching*", "Straight Fletching", "Attrition Orbs*", "Wellspring*", "Kill Tracker", "Gun and Run*", "Tier 1: Draw Time*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 78, "Reload": 40, "Season": 24, "Shield Duration": 0, "Source": "gambit", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 26, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 41, "Hash": 3221722018, "Holofoil": false, "Id": ""6917530025510662802"", "Impact": 38, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Laser Painter", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Fluted Barrel*", "Full Bore", "Accelerated Coils*", "Ionized Battery", "Fragile Focus*", "Vorpal Weapon*", "Gun and Run*", "Veist Stinger", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 0, "Range": 34, "Rarity": "Legendary", "Recoil": 70, "Reload": 25, "Season": 21, "Shield Duration": 0, "Source": "gambit", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 25, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 32, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 3615421669, "Holofoil": false, "Id": ""6917530026001529019"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Suspectum-4fr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Chambered Compensator", "Enhanced Battery*", "Particle Repeater", "Headstone*", "Chill Clip*", "Veist Stinger*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 97, "Reload": 25, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 41, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "special", "Ammo Generation": 25, "Archetype": "Micro-Missile Frame", "Blast Radius": 41, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 3947966653, "Holofoil": false, "Id": ""6917530026888997052"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "The Call", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Confined Launch*", "Linear Compensator", "Tactical Mag*", "Extended Mag", "Stats for All*", "Hatchling*", "Dealer's Choice*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 75, "Reload": 45, "Season": 24, "Shield Duration": 0, "Source": "paleheart", "Stability": 56, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 50, "Year": 7, "Zoom": 18, }, { "AA": 31, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "special", "Ammo Generation": 34, "Archetype": "Compounding Force", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 20, "Hash": 2130065553, "Holofoil": false, "Id": ""6917530027397247736"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Arbalest", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Compounding Force*", "Extended Barrel*", "Projection Fuse*", "Disruption Break*", "Composite Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 0, "Range": 56, "Rarity": "Exotic", "Recoil": 87, "Reload": 28, "Season": 6, "Shield Duration": 0, "Source": "", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 2, "Zoom": 25, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 60, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, "Hash": 4233375372, "Holofoil": false, "Id": ""6917530027400152408"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 57, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Marcato-45", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Smallbore", "Appended Mag*", "Extended Mag", "Triple Tap*", "Golden Tricorn*", "Suros Synergy*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 450, "Range": 45, "Rarity": "Legendary", "Recoil": 100, "Reload": 49, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 47, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "special", "Ammo Generation": 19, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 51, "Hash": 499245245, "Holofoil": false, "Id": ""6917530027844006029"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Ded Gramarye IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Rifled Barrel*", "Barrel Shroud", "Extended Mag*", "Accurized Rounds", "Threat Detector*", "Voltshot*", "Wild Card*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 80, "Range": 51, "Rarity": "Legendary", "Recoil": 60, "Reload": 34, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 67, "Hash": 3583275737, "Holofoil": false, "Id": ""6917530027846485344"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 44, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Ros Arago IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Fluted Barrel", "Appended Mag*", "Alloy Magazine", "Rewind Rounds*", "Onslaught*", "Wild Card*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 600, "Range": 42, "Rarity": "Legendary", "Recoil": 77, "Reload": 57, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 46, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 44, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 3926811686, "Holofoil": false, "Id": ""6917530027853680091"", "Impact": 20, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 39, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Parabellum", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Full Bore", "Tactical Mag*", "Flared Magwell", "Heal Clip*", "Dynamic Sway Reduction*", "Field-Tested*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 900, "Range": 43, "Rarity": "Legendary", "Recoil": 92, "Reload": 64, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 58, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 82, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 17, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 2097055732, "Holofoil": false, "Id": ""6917530031418496234"", "Impact": 23, "Kill Tracker": 813, "Loadouts": "", "Locked": true, "Mag": 39, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Piece of Mind", "New Gear": false, "Notes": undefined, "Owner": "Titan(10)", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Appended Mag*", "Overflow*", "Adrenaline Junkie*", "Land Tank*", "Kill Tracker", "Full Auto Retrofit*", "Calus's Shadow*", ], "Power": 10, "ROF": 540, "Range": 31, "Rarity": "Legendary", "Recoil": 88, "Reload": 42, "Season": 16, "Shield Duration": 0, "Source": "psiops", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 48, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": "crafted", "Crafted Level": 33, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 2119346509, "Holofoil": false, "Id": ""6917530031418499476"", "Impact": 29, "Kill Tracker": 2994, "Loadouts": "", "Locked": true, "Mag": 37, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Ammit AR2", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Flared Magwell*", "Ambitious Assassin*", "Incandescent*", "Omolon Fluid Dynamics*", "Kill Tracker", "Backup Mag*", "Skitchpaint*", ], "Power": 10, "ROF": 450, "Range": 63, "Rarity": "Legendary", "Recoil": 100, "Reload": 68, "Season": 18, "Shield Duration": 0, "Source": "engram", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 43, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "special", "Ammo Generation": 34, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 633, "Crafted": "crafted", "Crafted Level": 17, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 1720503118, "Holofoil": false, "Id": ""6917530031423136781"", "Impact": 60, "Kill Tracker": 175, "Loadouts": "", "Locked": true, "Mag": 8, "Masterwork Tier": 10, "Masterwork Type": "Charge Time", "Name": "Royal Executioner", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Accelerated Coils*", "Envious Assassin*", "Reservoir Burst*", "Noble Deeds*", "Kill Tracker", "Backup Mag*", ], "Power": 10, "ROF": 0, "Range": 31, "Rarity": "Legendary", "Recoil": 88, "Reload": 32, "Season": 20, "Shield Duration": 0, "Source": "seasonpass", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 57, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 36, "Archetype": "Precision Frame", "Blast Radius": 47, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 4195186942, "Holofoil": false, "Id": ""6917530039520439071"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 2, "Masterwork Type": "Blast Radius", "Name": "Faith-Keeper", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Volatile Launch*", "Linear Compensator", "High-Velocity Rounds*", "Implosion Rounds", "Clown Cartridge*", "Reverberation*", "Radiolaria Transposer*", "Kill Tracker", "Tier 2: Blast Radius*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Legendary", "Recoil": 66, "Reload": 33, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 57, "Year": 7, "Zoom": 20, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 63, "Archetype": "Area Denial Frame", "Blast Radius": 100, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 1197771438, "Holofoil": false, "Id": ""6917530039520440567"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 3, "Masterwork Type": "Velocity", "Name": "Lost Signal", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Area Denial Frame*", "Linear Compensator*", "Smart Drift Control", "High-Velocity Rounds*", "Implosion Rounds", "Lead from Gold*", "One for All*", "Radiolaria Transposer*", "Kill Tracker", "Tier 3: Velocity*", ], "Power": 10, "ROF": 72, "Range": 0, "Rarity": "Legendary", "Recoil": 79, "Reload": 75, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 26, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 90, "Year": 7, "Zoom": 13, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 32, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 3615421669, "Holofoil": false, "Id": ""6917530039539579913"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Suspectum-4fr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Ionized Battery*", "Liquid Coils", "Enlightened Action*", "Firing Line*", "Veist Stinger*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 67, "Reload": 8, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 49, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 51, "Hash": 3959549446, "Holofoil": false, "Id": ""6917530039546539403"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 41, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Yarovit MG4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Chambered Compensator", "Hammer-Forged Rifling*", "Extended Mag", "Ricochet Rounds*", "Enlightened Action*", "Zen Moment*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 900, "Range": 37, "Rarity": "Legendary", "Recoil": 98, "Reload": 25, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 423343404, "Holofoil": false, "Id": ""6917530039551111553"", "Impact": 49, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 16, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Controlling Vision", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Chambered Compensator", "Ricochet Rounds*", "Flared Magwell", "Surplus*", "High Ground*", "Field-Tested*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 300, "Range": 35, "Rarity": "Legendary", "Recoil": 100, "Reload": 33, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 71, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 38, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 2800870005, "Holofoil": false, "Id": ""6917530039553752445"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "The Domino", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Full Bore*", "Hammer-Forged Rifling", "Steady Rounds*", "Alloy Magazine", "Overflow*", "Mulligan*", "Nadir Focus*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 90, "Range": 58, "Rarity": "Legendary", "Recoil": 74, "Reload": 40, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 45, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "special", "Ammo Generation": 25, "Archetype": "Micro-Missile Frame", "Blast Radius": 31, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 3947966653, "Holofoil": false, "Id": ""6917530039555869568"", "Impact": 0, "Kill Tracker": 159, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "The Call", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Countermass*", "Smart Drift Control", "Appended Mag*", "Flared Magwell", "Lead from Gold*", "Vorpal Weapon*", "Dealer's Choice*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 100, "Reload": 38, "Season": 24, "Shield Duration": 0, "Source": "paleheart", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 60, "Year": 7, "Zoom": 18, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 70, "Archetype": "Cayde's Retribution", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 62, "Hash": 2905188646, "Holofoil": false, "Id": ""6917530039558282883"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Still Hunt", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Cayde's Retribution*", "Polygonal Rifling*", "Golden Munitions*", "Sharpshooter*", "Short-Action Stock*", "Kill Tracker", ], "Power": 10, "ROF": 90, "Range": 64, "Rarity": "Exotic", "Recoil": 69, "Reload": 44, "Season": 24, "Shield Duration": 0, "Source": "exoticquest", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 40, }, { "AA": 46, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "heavy", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 35, "Hash": 3605603507, "Holofoil": false, "Id": ""6917530039565289668"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 64, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Pro Memoria", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Extended Barrel*", "Polygonal Rifling", "Flared Magwell*", "Light Mag", "Hatchling*", "Dragonfly*", "Dealer's Choice*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 600, "Range": 56, "Rarity": "Legendary", "Recoil": 89, "Reload": 60, "Season": 24, "Shield Duration": 0, "Source": "paleheart", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 7, "Zoom": 17, }, { "AA": 46, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "heavy", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 3605603507, "Holofoil": false, "Id": ""6917530039570033921"", "Impact": 33, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 64, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Pro Memoria", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Full Bore", "Smallbore*", "High-Caliber Rounds", "Ricochet Rounds*", "Strategist*", "Tap the Trigger*", "Dealer's Choice*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 600, "Range": 57, "Rarity": "Legendary", "Recoil": 79, "Reload": 49, "Season": 24, "Shield Duration": 0, "Source": "paleheart", "Stability": 67, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 7, "Zoom": 17, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "special", "Ammo Generation": 25, "Archetype": "Micro-Missile Frame", "Blast Radius": 46, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 3947966653, "Holofoil": false, "Id": ""6917530039600184973"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "The Call", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Volatile Launch*", "Hard Launch", "Tactical Mag*", "Flared Magwell", "Slice*", "Adrenaline Junkie*", "Dealer's Choice*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 75, "Reload": 45, "Season": 24, "Shield Duration": 0, "Source": "paleheart", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 55, "Year": 7, "Zoom": 18, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Support Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 77, "Hash": 1801007332, "Holofoil": false, "Id": ""6917530039619237070"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 52, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "No Hesitation", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Support Frame*", "Full Bore*", "Polygonal Rifling", "Extended Mag*", "Steady Rounds", "Strategist*", "Incandescent*", "Dealer's Choice*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 600, "Range": 60, "Rarity": "Legendary", "Recoil": 85, "Reload": 50, "Season": 24, "Shield Duration": 0, "Source": "paleheart", "Stability": 38, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 7, "Zoom": 16, }, { "AA": 51, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 1058098236, "Holofoil": false, "Id": ""6917530039628571992"", "Impact": 100, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 12, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Timeworn Wayfarer", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Chambered Compensator*", "Steady Rounds*", "Dual Loader*", "Precision Instrument*", "Radiolaria Transposer*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 120, "Range": 53, "Rarity": "Legendary", "Recoil": 68, "Reload": 44, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 71, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 29, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 82, "Hash": 3998080529, "Holofoil": false, "Id": ""6917530039633329230"", "Impact": 43, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 14, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Heliocentric QSc", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Fluted Barrel*", "Smallbore", "Accurized Rounds*", "Steady Rounds", "Moving Target*", "Frenzy*", "Nadir Focus*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 360, "Range": 37, "Rarity": "Legendary", "Recoil": 94, "Reload": 51, "Season": 22, "Shield Duration": 0, "Source": "engram", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 30, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 3685470415, "Holofoil": false, "Id": ""6917530039633330516"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 56, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Veiled Threat", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Chambered Compensator*", "Smallbore", "Extended Mag*", "Alloy Magazine", "Fragile Focus*", "Encore*", "Radiolaria Transposer*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 720, "Range": 27, "Rarity": "Legendary", "Recoil": 55, "Reload": 29, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 62, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 7, "Zoom": 16, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "special", "Ammo Generation": 0, "Archetype": "Wolfpack Rounds", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 30, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 40, "Guard Resistance": 40, "Handling": 0, "Hash": 1681583613, "Holofoil": false, "Id": ""6917530039637863620"", "Impact": 60, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 48, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Ergo Sum", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Wolfpack Rounds*", "Hungry Edge*", "Enduring Blade", "Jagged Edge", "Balanced Guard*", "Swordmaster's Guard", "Vortex Frame*", "Transcendent Duelist*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Exotic", "Recoil": 0, "Reload": 0, "Season": 24, "Shield Duration": 0, "Source": "paleheart", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 7, "Zoom": 0, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 27, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 57, "Hash": 3685470415, "Holofoil": false, "Id": ""6917530042913585142"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 46, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Veiled Threat", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Fluted Barrel*", "Steady Rounds*", "Loose Change*", "Headstone*", "Radiolaria Transposer*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 720, "Range": 22, "Rarity": "Legendary", "Recoil": 45, "Reload": 49, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 73, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 7, "Zoom": 16, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "heavy", "Ammo Generation": 45, "Archetype": "Precision Frame", "Blast Radius": 31, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "Solstice", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 4106757302, "Holofoil": false, "Id": ""6917530045773513295"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 1, "Masterwork Type": "Blast Radius", "Name": "Crowning Duologue", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Countermass*", "Quick Launch", "High-Velocity Rounds*", "Implosion Rounds", "Hatchling*", "Cluster Bomb*", "Dream Work*", "Häkke Breach Armaments", "Kill Tracker", "Tier 1: Blast Radius*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Legendary", "Recoil": 95, "Reload": 39, "Season": 24, "Shield Duration": 0, "Source": "events", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 61, "Year": 7, "Zoom": 20, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 60, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 4233375372, "Holofoil": false, "Id": ""6917530045775634093"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 57, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Marcato-45", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Fluted Barrel", "Appended Mag*", "Tactical Mag", "Triple Tap*", "Onslaught*", "Suros Synergy*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 450, "Range": 45, "Rarity": "Legendary", "Recoil": 90, "Reload": 49, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 75, "Accuracy": 39, "Airborne Effectiveness": 13, "Ammo": "primary", "Ammo Generation": 58, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 500, "Element": "Void", "Equipped": false, "Event": "Solstice", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 70, "Hash": 2326578623, "Holofoil": false, "Id": ""6917530045777823316"", "Impact": 68, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Draw Time", "Name": "Fortunate Star", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Elastic String*", "Flexible String", "Fiberglass Arrow Shaft*", "Natural Fletching", "Archer's Tempo*", "Destabilizing Rounds*", "Dream Work*", "Veist Stinger", "Kill Tracker", "Counterbalance Stock*", "Tier 1: Draw Time*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 74, "Reload": 60, "Season": 24, "Shield Duration": 0, "Source": "events", "Stability": 54, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 75, "Accuracy": 39, "Airborne Effectiveness": 13, "Ammo": "primary", "Ammo Generation": 58, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 567, "Element": "Void", "Equipped": false, "Event": "Solstice", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 65, "Hash": 2326578623, "Holofoil": false, "Id": ""6917530045780076956"", "Impact": 68, "Kill Tracker": 72, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Fortunate Star", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Tactile String*", "Elastic String", "Carbon Arrow Shaft*", "Compact Arrow Shaft", "Hip-Fire Grip*", "Archer's Gambit*", "Dream Work*", "Veist Stinger", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 59, "Reload": 61, "Season": 24, "Shield Duration": 0, "Source": "events", "Stability": 79, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 47, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 80, "Handling": 0, "Hash": 3637669759, "Holofoil": false, "Id": ""6917530045782352017"", "Impact": 62, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 74, "Masterwork Tier": 4, "Masterwork Type": "Impact", "Name": "Geodetic HSm", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Enduring Blade*", "Honed Edge", "Jagged Edge", "Burst Guard*", "Heavy Guard", "Repulsor Brace*", "Destabilizing Rounds*", "Nadir Focus*", "Kill Tracker", "Tier 5: Impact*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 22, "Shield Duration": 0, "Source": "engram", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 6, "Zoom": 0, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 29, "Archetype": "Adaptive Frame", "Blast Radius": 55, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 39, "Hash": 2599338625, "Holofoil": false, "Id": ""6917530061676926827"", "Impact": 0, "Kill Tracker": 6, "Loadouts": "", "Locked": true, "Mag": 4, "Masterwork Tier": 1, "Masterwork Type": "Velocity", "Name": "Bitter/Sweet", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Hard Launch*", "High-Explosive Ordnance*", "Loose Change*", "Jolting Feedback*", "Dark Ether Reaper*", "Kill Tracker", "Tier 1: Velocity*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 58, "Reload": 42, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 32, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 51, "Year": 7, "Zoom": 13, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "primary", "Ammo Generation": 70, "Archetype": "Harvester Spike", "Blast Radius": 100, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 91, "Hash": 2350354266, "Holofoil": false, "Id": ""6917530061681279977"", "Impact": 0, "Kill Tracker": 199, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Alethonym", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Harvester Spike*", "Countermass*", "High-Velocity Rounds*", "Vestigial Alchemy*", "Short-Action Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 90, "Range": 0, "Rarity": "Exotic", "Recoil": 100, "Reload": 80, "Season": 25, "Shield Duration": 0, "Source": "seasonpass", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 84, "Year": 7, "Zoom": 13, }, { "AA": 80, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "special", "Ammo Generation": 57, "Archetype": "Micro-Missile Frame", "Blast Radius": 42, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 2198166292, "Holofoil": false, "Id": ""6917530064199989451"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 9, "Masterwork Tier": 4, "Masterwork Type": "Reload Speed", "Name": "Aberrant Action", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Confined Launch*", "Countermass", "Tactical Mag*", "High-Explosive Ordnance", "Field Prep*", "Demolitionist*", "Radiolaria Transposer*", "Kill Tracker", "Tier 4: Reload Speed*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 60, "Reload": 50, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 43, "Year": 7, "Zoom": 18, }, { "AA": 80, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "special", "Ammo Generation": 37, "Archetype": "Micro-Missile Frame", "Blast Radius": 47, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 2198166292, "Holofoil": false, "Id": ""6917530064209063173"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 9, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Aberrant Action", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Volatile Launch*", "Confined Launch", "High-Velocity Rounds*", "High-Explosive Ordnance", "Beacon Rounds*", "Demolitionist*", "Radiolaria Transposer*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 60, "Reload": 46, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 58, "Year": 7, "Zoom": 18, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 2823644677, "Holofoil": false, "Id": ""6917530066323758215"", "Impact": 92, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 9, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Exuviae", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Corkscrew Rifling*", "Polygonal Rifling", "Tactical Mag*", "Extended Mag", "Triple Tap*", "Desperate Measures*", "Dark Ether Reaper*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 120, "Range": 66, "Rarity": "Legendary", "Recoil": 93, "Reload": 40, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 32, "Hash": 2823644677, "Holofoil": false, "Id": ""6917530067028759695"", "Impact": 92, "Kill Tracker": 15, "Loadouts": "", "Locked": false, "Mag": 8, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Exuviae", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Polygonal Rifling*", "Flared Magwell*", "Triple Tap*", "Headstone*", "Dark Ether Reaper*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 120, "Range": 61, "Rarity": "Legendary", "Recoil": 93, "Reload": 45, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 33, "Archetype": "Double Fire", "Blast Radius": 35, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 2599338624, "Holofoil": false, "Id": ""6917530067033717375"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 2, "Masterwork Type": "Velocity", "Name": "Liturgy", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Double Fire*", "Volatile Launch*", "Quick Launch", "Proximity Grenades*", "High-Velocity Rounds", "Slideways*", "One for All*", "Dark Ether Reaper*", "Kill Tracker", "Tier 2: Velocity*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 66, "Reload": 32, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 22, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 86, "Year": 7, "Zoom": 13, }, { "AA": 57, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 45, "Hash": 3830941962, "Holofoil": false, "Id": ""6917530067033718037"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 36, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Vantage Point", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Steady Rounds*", "Stats for All*", "Jolting Feedback*", "Dark Ether Reaper*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 390, "Range": 43, "Rarity": "Legendary", "Recoil": 54, "Reload": 42, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 69, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 7, "Zoom": 17, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "heavy", "Ammo Generation": 53, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 74, "Hash": 2828278545, "Holofoil": false, "Id": ""6917530068068900349"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 66, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Song of Ir Yût", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Full Bore", "Appended Mag*", "High-Caliber Rounds", "Keep Away*", "Bait and Switch*", "Cursed Thrall*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 450, "Range": 49, "Rarity": "Legendary", "Recoil": 80, "Reload": 55, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 33, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 38, "Archetype": "Pinpoint Slug Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 3753063346, "Holofoil": false, "Id": ""6917530068073553416"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Legato-11", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Pinpoint Slug Frame*", "Full Bore*", "Smallbore", "Tactical Mag*", "Accurized Rounds", "Killing Wind*", "Closing Time*", "Suros Synergy*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 65, "Range": 82, "Rarity": "Legendary", "Recoil": 55, "Reload": 58, "Season": 25, "Shield Duration": 0, "Source": "engram", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 31, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "heavy", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 19, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 17, "Hash": 1719687748, "Holofoil": false, "Id": ""6917530068073554992"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 1, "Masterwork Tier": 4, "Masterwork Type": "Blast Radius", "Name": "Crux Termination IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Hard Launch*", "Linear Compensator", "High-Velocity Rounds*", "Implosion Rounds", "Reconstruction*", "Demolitionist*", "Wild Card*", "Kill Tracker", "Tier 4: Blast Radius*", ], "Power": 10, "ROF": 25, "Range": 0, "Rarity": "Legendary", "Recoil": 80, "Reload": 67, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 13, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 97, "Year": 6, "Zoom": 20, }, { "AA": 47, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 19, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 499245245, "Holofoil": false, "Id": ""6917530068073557536"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Ded Gramarye IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Smoothbore*", "Smallbore", "Tactical Mag*", "Light Mag", "Eddy Current*", "Vorpal Weapon*", "Wild Card*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 80, "Range": 57, "Rarity": "Legendary", "Recoil": 60, "Reload": 64, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 41, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 37, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 52, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 2720651699, "Holofoil": false, "Id": ""6917530068091501141"", "Impact": 55, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 8, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Zealot's Reward", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Extended Barrel*", "Polygonal Rifling", "Ionized Battery*", "Particle Repeater", "Feeding Frenzy*", "Successful Warm-Up*", "Photoinhibition*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 0, "Range": 32, "Rarity": "Legendary", "Recoil": 59, "Reload": 28, "Season": 25, "Shield Duration": 0, "Source": "gardenofsalvation", "Stability": 28, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 15, }, { "AA": 48, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Aggressive Burst", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 29, "Hash": 2241507890, "Holofoil": false, "Id": ""6917530068095906047"", "Impact": 35, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 40, "Masterwork Tier": 5, "Masterwork Type": "Reload Speed", "Name": "Sacred Provenance", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Burst*", "Corkscrew Rifling*", "Hammer-Forged Rifling", "Alloy Magazine*", "Armor-Piercing Rounds", "Killing Wind*", "Kill Clip*", "Photoinhibition*", "Kill Tracker", "Tier 5: Reload Speed*", ], "Power": 10, "ROF": 450, "Range": 74, "Rarity": "Legendary", "Recoil": 70, "Reload": 41, "Season": 25, "Shield Duration": 0, "Source": "gardenofsalvation", "Stability": 62, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 3612142623, "Holofoil": false, "Id": ""6917530068098279960"", "Impact": 62, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 15, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Live Fire", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Hammer-Forged Rifling*", "Smallbore", "Armor-Piercing Rounds*", "Flared Magwell", "Rimestealer*", "Headstone*", "Field-Tested*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 180, "Range": 62, "Rarity": "Legendary", "Recoil": 54, "Reload": 40, "Season": 25, "Shield Duration": 0, "Source": "engram", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 39, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 800, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 768621510, "Holofoil": false, "Id": ""6917530068342191291"", "Impact": 75, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Deliverance", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Full Bore", "Accelerated Coils*", "Enhanced Battery", "Compulsive Reloader*", "Surrounded*", "Souldrinker*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 67, "Rarity": "Legendary", "Recoil": 80, "Reload": 35, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 37, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 52, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 2720651699, "Holofoil": false, "Id": ""6917530068344589808"", "Impact": 50, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Zealot's Reward", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Full Bore", "Accelerated Coils*", "Projection Fuse", "Attrition Orbs*", "One for All*", "Photoinhibition*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 0, "Range": 22, "Rarity": "Legendary", "Recoil": 79, "Reload": 48, "Season": 25, "Shield Duration": 0, "Source": "gardenofsalvation", "Stability": 29, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 15, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 60, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 62, "Hash": 4233375372, "Holofoil": false, "Id": ""6917530068346984304"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 48, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Marcato-45", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Alloy Magazine*", "Flared Magwell", "Slice*", "Adagio*", "Suros Synergy*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 450, "Range": 50, "Rarity": "Legendary", "Recoil": 80, "Reload": 51, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 37, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 52, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 51, "Hash": 2720651699, "Holofoil": false, "Id": ""6917530068346984428"", "Impact": 50, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Zealot's Reward", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Chambered Compensator*", "Polygonal Rifling", "Accelerated Coils*", "Liquid Coils", "Attrition Orbs*", "Closing Time*", "Photoinhibition*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 0, "Range": 22, "Rarity": "Legendary", "Recoil": 59, "Reload": 48, "Season": 25, "Shield Duration": 0, "Source": "gardenofsalvation", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 15, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "heavy", "Ammo Generation": 37, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 999767358, "Holofoil": false, "Id": ""6917530068352204306"", "Impact": 38, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Cataclysmic", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Fluted Barrel*", "Smallbore", "Accelerated Coils*", "Particle Repeater", "Successful Warm-Up*", "Focused Fury*", "Souldrinker*", "Kill Tracker", "Counterbalance Stock*", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 85, "Reload": 28, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 53, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 25, }, { "AA": 68, "Accuracy": 85, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 74, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 633, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 2848549302, "Holofoil": false, "Id": ""6917530068354924519"", "Impact": 76, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Neoptolemus II", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "High Tension String", "Polymer String*", "Fiberglass Arrow Shaft*", "Straight Fletching", "Air Trigger*", "Wellspring*", "Wild Card*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 69, "Reload": 35, "Season": 25, "Shield Duration": 0, "Source": "engram", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 7, "Zoom": 20, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 32, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 32, "Hash": 3615421669, "Holofoil": false, "Id": ""6917530068365870009"", "Impact": 38, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 5, "Masterwork Tier": 5, "Masterwork Type": "Stability", "Name": "Suspectum-4fr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Accelerated Coils*", "Enhanced Battery", "Envious Assassin*", "Precision Instrument*", "Veist Stinger*", "Kill Tracker", "Tier 5: Stability*", ], "Power": 10, "ROF": 0, "Range": 44, "Rarity": "Legendary", "Recoil": 67, "Reload": 25, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 39, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 800, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 768621510, "Holofoil": false, "Id": ""6917530068365872290"", "Impact": 80, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Deliverance", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Extended Barrel", "Ionized Battery*", "Particle Repeater", "Demolitionist*", "Bait and Switch*", "Souldrinker*", "Kill Tracker", "Tier 3: Range*", ], "Power": 10, "ROF": 0, "Range": 70, "Rarity": "Legendary", "Recoil": 80, "Reload": 14, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 27, "Ammo": "primary", "Ammo Generation": 65, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 82, "Hash": 3886416794, "Holofoil": false, "Id": ""6917530068368677393"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 44, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Submission", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Corkscrew Rifling", "Tactical Mag*", "Alloy Magazine", "Steady Hands*", "Demolitionist*", "Souldrinker*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 900, "Range": 29, "Rarity": "Legendary", "Recoil": 100, "Reload": 39, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "heavy", "Ammo Generation": 37, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 26, "Hash": 999767358, "Holofoil": false, "Id": ""6917530068371274933"", "Impact": 44, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Cataclysmic", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator*", "Extended Barrel", "Liquid Coils*", "Particle Repeater", "Fourth Time's the Charm*", "Turnabout*", "Souldrinker*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 80, "Reload": 26, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 58, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 25, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 59, "Archetype": "Aggressive Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 3428521585, "Holofoil": false, "Id": ""6917530068371274941"", "Impact": 35, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 36, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Insidious", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Burst*", "Fluted Barrel*", "Full Bore", "Accurized Rounds*", "Alloy Magazine", "Dragonfly*", "Adaptive Munitions*", "Souldrinker*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 450, "Range": 84, "Rarity": "Legendary", "Recoil": 78, "Reload": 35, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 49, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 44, "Hash": 3959549446, "Holofoil": false, "Id": ""6917530068371275591"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 41, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Yarovit MG4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Chambered Compensator*", "Hammer-Forged Rifling", "High-Caliber Rounds*", "Ricochet Rounds", "Enlightened Action*", "Desperate Measures*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 900, "Range": 31, "Rarity": "Legendary", "Recoil": 100, "Reload": 25, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 39, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 800, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 768621510, "Holofoil": false, "Id": ""6917530068373918104"", "Impact": 80, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Deliverance", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Ionized Battery*", "Projection Fuse", "Perpetual Motion*", "Surrounded*", "Souldrinker*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 67, "Rarity": "Legendary", "Recoil": 80, "Reload": 16, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 32, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 22, "Hash": 3615421669, "Holofoil": false, "Id": ""6917530068379229732"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Suspectum-4fr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator*", "Extended Barrel", "Ionized Battery*", "Liquid Coils", "Ensemble*", "Firing Line*", "Veist Stinger*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 77, "Reload": 6, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "heavy", "Ammo Generation": 37, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 999767358, "Holofoil": false, "Id": ""6917530068379230676"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Cataclysmic", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Fluted Barrel*", "Polygonal Rifling", "Particle Repeater*", "Projection Fuse", "Compulsive Reloader*", "Clown Cartridge*", "Souldrinker*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 70, "Reload": 28, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 63, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 25, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 1890332078, "Holofoil": false, "Id": ""6917530069400900718"", "Impact": 62, "Kill Tracker": 41, "Loadouts": "", "Locked": false, "Mag": 16, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Adverse Possession IX", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator*", "Fluted Barrel", "Tactical Mag*", "Alloy Magazine", "Discord*", "Keep Away*", "Suros Synergy*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 180, "Range": 41, "Rarity": "Legendary", "Recoil": 86, "Reload": 50, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 20, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "heavy", "Ammo Generation": 33, "Archetype": "Adaptive Frame", "Blast Radius": 45, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 1851777734, "Holofoil": false, "Id": ""6917530069402975995"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 5, "Masterwork Type": "Blast Radius", "Name": "Apex Predator", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Quick Launch*", "Smart Drift Control", "Implosion Rounds*", "Impact Casing", "Threat Detector*", "Vorpal Weapon*", "Explosive Pact*", "Kill Tracker", "Tier 5: Blast Radius*", ], "Power": 10, "ROF": 20, "Range": 0, "Rarity": "Legendary", "Recoil": 61, "Reload": 44, "Season": 21, "Shield Duration": 0, "Source": "lastwish", "Stability": 58, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 69, "Year": 6, "Zoom": 20, }, { "AA": 29, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 29, "Hash": 3885259140, "Holofoil": false, "Id": ""6917530069402976657"", "Impact": 67, "Kill Tracker": 96, "Loadouts": "", "Locked": false, "Mag": 14, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Transfiguration", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Hammer-Forged Rifling*", "Smallbore", "Accurized Rounds*", "Extended Mag", "Rewind Rounds*", "Harmony*", "Explosive Pact*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 150, "Range": 96, "Rarity": "Legendary", "Recoil": 78, "Reload": 25, "Season": 21, "Shield Duration": 0, "Source": "lastwish", "Stability": 22, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 21, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 51, "Hash": 3612142623, "Holofoil": false, "Id": ""6917530069402980968"", "Impact": 62, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 17, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Live Fire", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Polygonal Rifling", "Appended Mag*", "Light Mag", "Perfect Float*", "To the Pain*", "Field-Tested*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 180, "Range": 50, "Rarity": "Legendary", "Recoil": 54, "Reload": 40, "Season": 25, "Shield Duration": 0, "Source": "engram", "Stability": 66, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 32, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 3615421669, "Holofoil": false, "Id": ""6917530069405086222"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Suspectum-4fr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Full Bore", "Enhanced Battery*", "Projection Fuse", "No Distractions*", "Box Breathing*", "Veist Stinger*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 97, "Reload": 26, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 29, "Archetype": "Adaptive Frame", "Blast Radius": 65, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 2599338625, "Holofoil": false, "Id": ""6917530069405086811"", "Impact": 0, "Kill Tracker": 58, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Bitter/Sweet", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Volatile Launch*", "Hard Launch", "Alloy Casing*", "Mini Frags", "Reverberation*", "Cascade Point*", "Dark Ether Reaper*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 58, "Reload": 72, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 32, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 25, "Year": 7, "Zoom": 13, }, { "AA": 42, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 33, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 35, "Hash": 4200122994, "Holofoil": false, "Id": ""6917530069405087429"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 3, "Masterwork Tier": 4, "Masterwork Type": "Reload Speed", "Name": "Veleda-F", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Corkscrew Rifling*", "Polygonal Rifling", "Tactical Mag*", "Steady Rounds", "Slickdraw*", "Destabilizing Rounds*", "Häkke Breach Armaments*", "Kill Tracker", "Tier 4: Reload Speed*", ], "Power": 10, "ROF": 72, "Range": 85, "Rarity": "Legendary", "Recoil": 78, "Reload": 41, "Season": 25, "Shield Duration": 0, "Source": "engram", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 47, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 39, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 800, "Crafted": "crafted", "Crafted Level": 1, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 32, "Hash": 768621510, "Holofoil": false, "Id": ""6917530069405087610"", "Impact": 80, "Kill Tracker": 4, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Deliverance", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Polygonal Rifling*", "Enhanced Battery*", "Demolitionist*", "Tap the Trigger*", "Souldrinker*", "Kill Tracker", "Ballistics*", ], "Power": 10, "ROF": 0, "Range": 68, "Rarity": "Legendary", "Recoil": 80, "Reload": 34, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 36, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "heavy", "Ammo Generation": 70, "Archetype": "Rapid-Fire Frame", "Blast Radius": 15, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "Festival of the Lost", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 425681240, "Holofoil": false, "Id": ""6917530069407306986"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Acosmic", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Hard Launch*", "Smart Drift Control", "Spike Grenades*", "High-Velocity Rounds", "Air Trigger*", "Chain Reaction*", "Search Party*", "Nadir Focus", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 150, "Range": 0, "Rarity": "Legendary", "Recoil": 64, "Reload": 27, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 23, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 73, "Year": 7, "Zoom": 13, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "heavy", "Ammo Generation": 37, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": "crafted", "Crafted Level": 7, "Draw Time": 0, "Element": "Solar", "Equipped": true, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 21, "Hash": 999767358, "Holofoil": false, "Id": ""6917530069407312085"", "Impact": 41, "Kill Tracker": 17, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Cataclysmic", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Precision Frame*", "Extended Barrel*", "Enhanced Battery*", "Surplus*", "Adaptive Munitions*", "Souldrinker*", "Kill Tracker", "Ballistics*", ], "Power": 10, "ROF": 0, "Range": 55, "Rarity": "Legendary", "Recoil": 80, "Reload": 25, "Season": 16, "Shield Duration": 0, "Source": "vowofthedisciple", "Stability": 54, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 5, "Zoom": 25, }, { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 42, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, "Hash": 3649985571, "Holofoil": false, "Id": ""6917530069409852738"", "Impact": 70, "Kill Tracker": 38, "Loadouts": "", "Locked": false, "Mag": 14, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Arcane Embrace", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Corkscrew Rifling*", "Tactical Mag*", "Dual Loader*", "Swashbuckler*", "Search Party*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 62, "Range": 67, "Rarity": "Legendary", "Recoil": 55, "Reload": 79, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 47, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 19, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 51, "Hash": 499245245, "Holofoil": false, "Id": ""6917530069616783446"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Ded Gramarye IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Rifled Barrel*", "Corkscrew Rifling", "Tactical Mag*", "Extended Mag", "Stats for All*", "Vorpal Weapon*", "Wild Card*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 80, "Range": 51, "Rarity": "Legendary", "Recoil": 60, "Reload": 66, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 41, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "special", "Ammo Generation": 65, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 41, "Hash": 2477980485, "Holofoil": false, "Id": ""6917530069616787318"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 4, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Mechabre", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Appended Mag*", "Steady Rounds", "Air Trigger*", "Closing Time*", "Search Party*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 72, "Range": 78, "Rarity": "Legendary", "Recoil": 77, "Reload": 30, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 33, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 45, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "special", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 17, "Hash": 2477980485, "Holofoil": false, "Id": ""6917530069618767517"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 4, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Mechabre", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Extended Barrel*", "Hammer-Forged Rifling", "Extended Mag*", "Flared Magwell", "Demolitionist*", "To the Pain*", "Search Party*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 72, "Range": 84, "Rarity": "Legendary", "Recoil": 87, "Reload": 10, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 28, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 45, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "special", "Ammo Generation": 45, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 16, "Hash": 2477980485, "Holofoil": false, "Id": ""6917530069625327992"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 3, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Mechabre", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Extended Barrel*", "Polygonal Rifling", "Accurized Rounds*", "Flared Magwell", "Lone Wolf*", "Discord*", "Search Party*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 72, "Range": 98, "Rarity": "Legendary", "Recoil": 87, "Reload": 30, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 28, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 45, }, { "AA": 41, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "special", "Ammo Generation": 65, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 41, "Hash": 2477980485, "Holofoil": false, "Id": ""6917530069625328996"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 4, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Mechabre", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Fluted Barrel*", "Full Bore", "Appended Mag*", "Tactical Mag", "Air Trigger*", "Opening Shot*", "Search Party*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 72, "Range": 74, "Rarity": "Legendary", "Recoil": 77, "Reload": 30, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 36, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 45, }, { "AA": 42, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 53, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 26, "Hash": 4200122994, "Holofoil": false, "Id": ""6917530069627527304"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 3, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Veleda-F", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Chambered Compensator*", "Hammer-Forged Rifling", "Accurized Rounds*", "Tactical Mag", "Air Trigger*", "Opening Shot*", "Häkke Breach Armaments*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 72, "Range": 90, "Rarity": "Legendary", "Recoil": 88, "Reload": 27, "Season": 25, "Shield Duration": 0, "Source": "engram", "Stability": 35, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 47, }, { "AA": 47, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 19, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 76, "Hash": 499245245, "Holofoil": false, "Id": ""6917530070063837287"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Ded Gramarye IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Barrel Shroud*", "Corkscrew Rifling", "Accurized Rounds*", "Light Mag", "Stats for All*", "Chain Reaction*", "Wild Card*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 80, "Range": 51, "Rarity": "Legendary", "Recoil": 60, "Reload": 56, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 33, "Archetype": "Double Fire", "Blast Radius": 50, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 2599338624, "Holofoil": false, "Id": ""6917530070068629573"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 1, "Masterwork Type": "Velocity", "Name": "Liturgy", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Double Fire*", "Confined Launch*", "High-Velocity Rounds*", "Rimestealer*", "Chill Clip*", "Dark Ether Reaper*", "Kill Tracker", "Tier 1: Velocity*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 66, "Reload": 42, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 90, "Year": 7, "Zoom": 13, }, { "AA": 57, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "primary", "Ammo Generation": 53, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 35, "Hash": 3830941962, "Holofoil": false, "Id": ""6917530070071007530"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 36, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Vantage Point", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Full Bore*", "Polygonal Rifling", "Accurized Rounds*", "Appended Mag", "Eddy Current*", "Swashbuckler*", "Dark Ether Reaper*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 390, "Range": 67, "Rarity": "Legendary", "Recoil": 54, "Reload": 42, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 7, "Zoom": 17, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 33, "Archetype": "Double Fire", "Blast Radius": 54, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 2599338624, "Holofoil": false, "Id": ""6917530070071010597"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 4, "Masterwork Type": "Blast Radius", "Name": "Liturgy", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Double Fire*", "Confined Launch*", "Linear Compensator", "Spike Grenades*", "High-Velocity Rounds", "Rimestealer*", "One for All*", "Dark Ether Reaper*", "Kill Tracker", "Tier 4: Blast Radius*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 66, "Reload": 32, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 79, "Year": 7, "Zoom": 13, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 423343404, "Holofoil": false, "Id": ""6917530070075603388"", "Impact": 49, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 16, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Controlling Vision", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Fluted Barrel", "Armor-Piercing Rounds*", "Flared Magwell", "Surplus*", "High Ground*", "Field-Tested*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 300, "Range": 36, "Rarity": "Legendary", "Recoil": 100, "Reload": 33, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 66, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 3325778512, "Holofoil": false, "Id": ""6917530070080417870"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 53, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "A Fine Memorial", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Smallbore", "Ricochet Rounds*", "Light Mag", "Field Prep*", "One for All*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 450, "Range": 51, "Rarity": "Legendary", "Recoil": 72, "Reload": 50, "Season": 8, "Shield Duration": 0, "Source": "moon", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 3, "Zoom": 16, }, { "AA": 36, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "heavy", "Ammo Generation": 50, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "Festival of the Lost", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 425681240, "Holofoil": false, "Id": ""6917530071059118065"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 2, "Masterwork Type": "Blast Radius", "Name": "Acosmic", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Hard Launch*", "Smart Drift Control", "Proximity Grenades*", "Augmented Drum", "Reverberation*", "Cascade Point*", "Search Party*", "Nadir Focus", "Kill Tracker", "Tier 2: Blast Radius*", ], "Power": 10, "ROF": 150, "Range": 0, "Rarity": "Legendary", "Recoil": 64, "Reload": 26, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 13, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 73, "Year": 7, "Zoom": 13, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 32, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 22, "Hash": 3615421669, "Holofoil": false, "Id": ""6917530071059119506"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Suspectum-4fr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Particle Repeater*", "Projection Fuse", "Enlightened Action*", "Chill Clip*", "Veist Stinger*", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 0, "Range": 39, "Rarity": "Legendary", "Recoil": 77, "Reload": 25, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 64, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 60, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 74, "Hash": 4233375372, "Holofoil": false, "Id": ""6917530071061232864"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 57, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Marcato-45", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Appended Mag*", "Tactical Mag", "Threat Detector*", "Golden Tricorn*", "Suros Synergy*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 450, "Range": 45, "Rarity": "Legendary", "Recoil": 80, "Reload": 49, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 42, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 68, "Hash": 3649985571, "Holofoil": false, "Id": ""6917530071061237849"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 14, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Arcane Embrace", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Corkscrew Rifling*", "Polygonal Rifling", "Tactical Mag*", "Extended Mag", "Lone Wolf*", "Desperado*", "Search Party*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 62, "Range": 67, "Rarity": "Legendary", "Recoil": 55, "Reload": 79, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 42, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 3649985571, "Holofoil": false, "Id": ""6917530071061238729"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 14, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Arcane Embrace", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Chambered Compensator*", "Fluted Barrel", "Tactical Mag*", "Alloy Magazine", "Dual Loader*", "Closing Time*", "Search Party*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 62, "Range": 64, "Rarity": "Legendary", "Recoil": 65, "Reload": 79, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 54, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 36, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "heavy", "Ammo Generation": 50, "Archetype": "Rapid-Fire Frame", "Blast Radius": 36, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "Festival of the Lost", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 20, "Hash": 425681240, "Holofoil": false, "Id": ""6917530071063746419"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Blast Radius", "Name": "Acosmic", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Volatile Launch*", "Hard Launch", "Alloy Casing*", "High-Explosive Ordnance", "Enlightened Action*", "Explosive Light*", "Search Party*", "Nadir Focus", "Kill Tracker", "Tier 1: Blast Radius*", ], "Power": 10, "ROF": 150, "Range": 0, "Rarity": "Legendary", "Recoil": 64, "Reload": 56, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 13, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 53, "Year": 7, "Zoom": 13, }, { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "special", "Ammo Generation": 42, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 3649985571, "Holofoil": false, "Id": ""6917530071063749562"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 12, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Arcane Embrace", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Full Bore*", "Hammer-Forged Rifling", "Steady Rounds*", "Alloy Magazine", "Grave Robber*", "Swashbuckler*", "Search Party*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 62, "Range": 76, "Rarity": "Legendary", "Recoil": 55, "Reload": 69, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 48, "Hash": 423343404, "Holofoil": false, "Id": ""6917530071063750592"", "Impact": 49, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 20, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Controlling Vision", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Polygonal Rifling*", "Smallbore", "Extended Mag*", "Armor-Piercing Rounds", "Slickdraw*", "Threat Detector*", "Field-Tested*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 300, "Range": 30, "Rarity": "Legendary", "Recoil": 80, "Reload": 14, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 70, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 68, "Accuracy": 65, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 667, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 80, "Hash": 2848549302, "Holofoil": false, "Id": ""6917530071066003390"", "Impact": 76, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 0, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Neoptolemus II", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Agile Bowstring*", "Natural String", "Compact Arrow Shaft*", "Natural Fletching", "Pugilist*", "Tunnel Vision*", "Wild Card*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 69, "Reload": 45, "Season": 25, "Shield Duration": 0, "Source": "engram", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 7, "Zoom": 20, }, { "AA": 68, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 3583275737, "Holofoil": false, "Id": ""6917530071068113377"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 39, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Ros Arago IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Smallbore", "Accurized Rounds*", "Appended Mag", "Repulsor Brace*", "Deconstruct*", "Wild Card*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 600, "Range": 64, "Rarity": "Legendary", "Recoil": 57, "Reload": 56, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "special", "Ammo Generation": 42, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 3649985571, "Holofoil": false, "Id": ""6917530071068115969"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 12, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Arcane Embrace", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Chambered Compensator*", "Extended Barrel", "Steady Rounds*", "Flared Magwell", "Fourth Time's the Charm*", "Swashbuckler*", "Search Party*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 62, "Range": 58, "Rarity": "Legendary", "Recoil": 65, "Reload": 69, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 64, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 47, "Accuracy": 0, "Airborne Effectiveness": 25, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "Festival of the Lost", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 34, "Hash": 3558681245, "Holofoil": false, "Id": ""6917530071068119942"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 39, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "BrayTech Werewolf", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Extended Mag*", "Appended Mag", "Dynamic Sway Reduction*", "Onslaught*", "Search Party*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 450, "Range": 71, "Rarity": "Legendary", "Recoil": 84, "Reload": 22, "Season": 25, "Shield Duration": 0, "Source": "events", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 7, "Zoom": 16, }, { "AA": 49, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 44, "Hash": 3959549446, "Holofoil": false, "Id": ""6917530071084773479"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 44, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Yarovit MG4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Appended Mag*", "Ricochet Rounds", "Encore*", "Desperate Measures*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 900, "Range": 26, "Rarity": "Legendary", "Recoil": 100, "Reload": 25, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "heavy", "Ammo Generation": 53, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 64, "Hash": 2828278545, "Holofoil": false, "Id": ""6917530071084774008"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 71, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Song of Ir Yût", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Corkscrew Rifling*", "Fluted Barrel", "Extended Mag*", "Flared Magwell", "Reconstruction*", "Cascade Point*", "Cursed Thrall*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 450, "Range": 56, "Rarity": "Legendary", "Recoil": 80, "Reload": 37, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 21, "Hash": 1432682459, "Holofoil": false, "Id": ""6917530071089016914"", "Impact": 45, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 17, "Masterwork Tier": 5, "Masterwork Type": "Stability", "Name": "Fang of Ir Yût", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Chambered Compensator*", "Smallbore", "High-Caliber Rounds*", "Flared Magwell", "Keep Away*", "Precision Instrument*", "Cursed Thrall*", "Kill Tracker", "Tier 5: Stability*", ], "Power": 10, "ROF": 260, "Range": 37, "Rarity": "Legendary", "Recoil": 59, "Reload": 35, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 20, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 27, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 21, "Hash": 1432682459, "Holofoil": false, "Id": ""6917530071091178288"", "Impact": 45, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 20, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Fang of Ir Yût", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Chambered Compensator*", "Smallbore", "Extended Mag*", "Flared Magwell", "Keep Away*", "Golden Tricorn*", "Cursed Thrall*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 260, "Range": 33, "Rarity": "Legendary", "Recoil": 59, "Reload": 15, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 54, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 6, "Zoom": 20, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "heavy", "Ammo Generation": 53, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 69, "Hash": 2828278545, "Holofoil": false, "Id": ""6917530071093518125"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 71, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Song of Ir Yût", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Polygonal Rifling", "Extended Mag*", "Ricochet Rounds", "Keep Away*", "Target Lock*", "Cursed Thrall*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 450, "Range": 49, "Rarity": "Legendary", "Recoil": 100, "Reload": 38, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "heavy", "Ammo Generation": 53, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 54, "Hash": 2828278545, "Holofoil": false, "Id": ""6917530071093519370"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 71, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Song of Ir Yût", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Extended Mag*", "Ricochet Rounds", "Rewind Rounds*", "Elemental Capacitor*", "Cursed Thrall*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 450, "Range": 53, "Rarity": "Legendary", "Recoil": 90, "Reload": 37, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 55, "Hash": 3319810953, "Holofoil": false, "Id": ""6917530071095381345"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 59, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Eidolon Ally", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Ricochet Rounds*", "Rangefinder*", "Perpetual Motion*", "Cursed Thrall*", "Kill Tracker", "Backup Mag*", "Masterworked: Reload Speed*", ], "Power": 10, "ROF": 720, "Range": 34, "Rarity": "Legendary", "Recoil": 86, "Reload": 59, "Season": 22, "Shield Duration": 0, "Source": "crotasend", "Stability": 70, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 15, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 48, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 2119346509, "Holofoil": false, "Id": ""6917530071096903636"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 31, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Ammit AR2", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Extended Barrel", "Alloy Magazine*", "High-Caliber Rounds", "Well-Rounded*", "Adaptive Munitions*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 450, "Range": 61, "Rarity": "Legendary", "Recoil": 100, "Reload": 38, "Season": 18, "Shield Duration": 0, "Source": "engram", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 38, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 2800870005, "Holofoil": false, "Id": ""6917530071098804641"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 4, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "The Domino", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Steady Rounds*", "Flared Magwell", "Pulse Monitor*", "Opening Shot*", "Nadir Focus*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 90, "Range": 43, "Rarity": "Legendary", "Recoil": 84, "Reload": 40, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 70, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 45, }, { "AA": 49, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 64, "Hash": 3959549446, "Holofoil": false, "Id": ""6917530071098808441"", "Impact": 15, "Kill Tracker": 31, "Loadouts": "", "Locked": true, "Mag": 41, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Yarovit MG4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Fluted Barrel*", "Full Bore", "Alloy Magazine*", "Appended Mag", "Encore*", "Collective Action*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 3: Range*", ], "Power": 10, "ROF": 900, "Range": 25, "Rarity": "Legendary", "Recoil": 98, "Reload": 25, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 15, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 1246793994, "Holofoil": false, "Id": ""6917530071098812158"", "Impact": 92, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 18, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Maahes HC4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Arrowhead Brake*", "Hammer-Forged Rifling", "Tactical Mag*", "Alloy Magazine", "Perpetual Motion*", "Golden Tricorn*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 257, "Range": 55, "Rarity": "Legendary", "Recoil": 100, "Reload": 35, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 26, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 44, "Accuracy": 0, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 48, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 36, "Hash": 2119346509, "Holofoil": false, "Id": ""6917530071100903080"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 37, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Ammit AR2", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Extended Barrel*", "Polygonal Rifling", "Appended Mag*", "Light Mag", "Turnabout*", "Pugilist*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 450, "Range": 71, "Rarity": "Legendary", "Recoil": 81, "Reload": 38, "Season": 18, "Shield Duration": 0, "Source": "engram", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 5, "Zoom": 15, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 5, "Ammo": "heavy", "Ammo Generation": 42, "Archetype": "Precision Frame", "Blast Radius": 35, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 3489657138, "Holofoil": false, "Id": ""6917530071100907653"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 5, "Masterwork Type": "Velocity", "Name": "Palmyra-B", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Volatile Launch*", "Quick Launch", "Implosion Rounds*", "Impact Casing", "Auto-Loading Holster*", "Explosive Light*", "Häkke Breach Armaments*", "Kill Tracker", "Radar Tuner*", "Tier 5: Velocity*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Legendary", "Recoil": 65, "Reload": 22, "Season": 16, "Shield Duration": 0, "Source": "engram", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 59, "Year": 5, "Zoom": 20, }, { "AA": 37, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 52, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 2720651699, "Holofoil": false, "Id": ""6917530071273942194"", "Impact": 50, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Zealot's Reward", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Chambered Compensator", "Accelerated Coils*", "Projection Fuse", "Destabilizing Rounds*", "Kickstart*", "Photoinhibition*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 0, "Range": 22, "Rarity": "Legendary", "Recoil": 79, "Reload": 50, "Season": 25, "Shield Duration": 0, "Source": "gardenofsalvation", "Stability": 28, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 15, }, { "AA": 74, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 57, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 963574173, "Holofoil": false, "Id": ""6917530071280833290"", "Impact": 84, "Kill Tracker": 46, "Loadouts": "", "Locked": false, "Mag": 12, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Ancient Gospel", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Hammer-Forged Rifling*", "Polygonal Rifling", "Appended Mag*", "Flared Magwell", "Rampage*", "To the Pain*", "Photoinhibition*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 140, "Range": 51, "Rarity": "Legendary", "Recoil": 100, "Reload": 43, "Season": 25, "Shield Duration": 0, "Source": "gardenofsalvation", "Stability": 61, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 47, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "special", "Ammo Generation": 19, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 76, "Hash": 499245245, "Holofoil": false, "Id": ""6917530071285065077"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Ded Gramarye IV", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Barrel Shroud*", "Corkscrew Rifling", "Extended Mag*", "Steady Rounds", "Slickdraw*", "Golden Tricorn*", "Wild Card*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 80, "Range": 42, "Rarity": "Legendary", "Recoil": 60, "Reload": 34, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 60, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 57, "Hash": 4233375372, "Holofoil": false, "Id": ""6917530071294406904"", "Impact": 41, "Kill Tracker": 7, "Loadouts": "", "Locked": false, "Mag": 53, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Marcato-45", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Hammer-Forged Rifling*", "Polygonal Rifling", "Tactical Mag*", "Extended Mag", "Attrition Orbs*", "Adagio*", "Suros Synergy*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 450, "Range": 56, "Rarity": "Legendary", "Recoil": 80, "Reload": 58, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 49, "Accuracy": 0, "Airborne Effectiveness": 28, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 59, "Hash": 3959549446, "Holofoil": false, "Id": ""6917530071305058558"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 45, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Yarovit MG4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Polygonal Rifling", "Extended Mag*", "Flared Magwell", "Dynamic Sway Reduction*", "Collective Action*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 900, "Range": 22, "Rarity": "Legendary", "Recoil": 100, "Reload": 5, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 63, "Archetype": "Area Denial Frame", "Blast Radius": 100, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 61, "Hash": 1197771438, "Holofoil": false, "Id": ""6917530071752434421"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Lost Signal", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Area Denial Frame*", "Volatile Launch*", "Confined Launch", "High-Velocity Rounds*", "Implosion Rounds", "Feeding Frenzy*", "Reverberation*", "Radiolaria Transposer*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 72, "Range": 0, "Rarity": "Legendary", "Recoil": 79, "Reload": 76, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 21, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 77, "Year": 7, "Zoom": 13, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 2249996761, "Holofoil": false, "Id": ""6917530071752440592"", "Impact": 60, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 17, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Patron of Lost Causes", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Hammer-Forged Rifling*", "Polygonal Rifling", "Appended Mag*", "Alloy Magazine", "Triple Tap*", "Strategist", "Focused Fury*", "Precision Instrument", "Cast No Shadows*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 200, "Range": 44, "Rarity": "Legendary", "Recoil": 59, "Reload": 53, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 32, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 20, }, { "AA": 57, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 36, "Archetype": "Precision Frame", "Blast Radius": 15, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 40, "Hash": 4195186942, "Holofoil": false, "Id": ""6917530071754530120"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "Faith-Keeper", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Volatile Launch*", "Smart Drift Control", "Alloy Casing*", "Black Powder", "Danger Zone*", "Bipod*", "Radiolaria Transposer*", "Kill Tracker", "Tier 4: Handling*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Legendary", "Recoil": 66, "Reload": 33, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 33, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 47, "Year": 7, "Zoom": 20, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 58, "Hash": 2249996761, "Holofoil": false, "Id": ""6917530071756774725"", "Impact": 60, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 16, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Patron of Lost Causes", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Chambered Compensator*", "Extended Barrel", "Tactical Mag*", "Alloy Magazine", "To the Pain*", "Focused Fury*", "Cast No Shadows*", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 200, "Range": 35, "Rarity": "Legendary", "Recoil": 69, "Reload": 63, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 20, }, { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 27, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 43, "Hash": 2126178511, "Holofoil": false, "Id": ""6917530071758854413"", "Impact": 35, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 24, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Corrasion", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Arrowhead Brake*", "Corkscrew Rifling", "Extended Mag*", "Steady Rounds", "Enlightened Action*", "Swashbuckler*", "Radiolaria Transposer*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 324, "Range": 76, "Rarity": "Legendary", "Recoil": 100, "Reload": 16, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 32, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 80, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "special", "Ammo Generation": 57, "Archetype": "Micro-Missile Frame", "Blast Radius": 42, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 2198166292, "Holofoil": false, "Id": ""6917530071758859373"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 9, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "Aberrant Action", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Confined Launch*", "Countermass", "Flared Magwell*", "High-Explosive Ordnance", "Field Prep*", "Reverberation*", "Radiolaria Transposer*", "Kill Tracker", "Tier 4: Handling*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 60, "Reload": 51, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 68, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 43, "Year": 7, "Zoom": 18, }, { "AA": 51, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 54, "Hash": 1058098236, "Holofoil": false, "Id": ""6917530071758859576"", "Impact": 100, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 12, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Timeworn Wayfarer", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Arrowhead Brake*", "Hammer-Forged Rifling", "Accurized Rounds*", "Alloy Magazine", "Heal Clip*", "Opening Shot*", "Radiolaria Transposer*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 120, "Range": 67, "Rarity": "Legendary", "Recoil": 88, "Reload": 44, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 26, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 60, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": "suros", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 4233375372, "Holofoil": false, "Id": ""6917530084557626221"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 57, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Marcato-45", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Full Bore*", "Polygonal Rifling", "Appended Mag*", "Alloy Magazine", "Attrition Orbs*", "Surrounded*", "Suros Synergy*", "Kill Tracker", "Tier 3: Range*", ], "Power": 10, "ROF": 450, "Range": 63, "Rarity": "Legendary", "Recoil": 80, "Reload": 49, "Season": 23, "Shield Duration": 0, "Source": "engram", "Stability": 34, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 423343404, "Holofoil": false, "Id": ""6917530084559530162"", "Impact": 49, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 20, "Masterwork Tier": 3, "Masterwork Type": "Reload Speed", "Name": "Controlling Vision", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Extended Mag*", "Armor-Piercing Rounds", "Slickdraw*", "Osmosis*", "Field-Tested*", "Kill Tracker", "Tier 3: Reload Speed*", ], "Power": 10, "ROF": 300, "Range": 30, "Rarity": "Legendary", "Recoil": 80, "Reload": 16, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 65, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 32, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 32, "Hash": 3615421669, "Holofoil": false, "Id": ""6917530099164705844"", "Impact": 38, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Suspectum-4fr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Hammer-Forged Rifling", "Accelerated Coils*", "Liquid Coils", "Backup Plan*", "Box Breathing*", "Veist Stinger*", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 0, "Range": 45, "Rarity": "Legendary", "Recoil": 67, "Reload": 25, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 45, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 2, "Ammo": "special", "Ammo Generation": 38, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 2800870005, "Holofoil": false, "Id": ""6917530099168322710"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "The Domino", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Extended Barrel*", "Polygonal Rifling", "Appended Mag*", "Alloy Magazine", "Shot Swap*", "Harmony*", "Nadir Focus*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 90, "Range": 58, "Rarity": "Legendary", "Recoil": 84, "Reload": 40, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 45, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Caster Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 52, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 90, "Guard Resistance": 0, "Handling": 0, "Hash": 3794274730, "Holofoil": false, "Id": ""6917530109018659583"", "Impact": 60, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 71, "Masterwork Tier": 1, "Masterwork Type": "Impact", "Name": "Ill Omen", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Caster Frame*", "Hungry Edge*", "Honed Edge", "Tempered Edge", "Enduring Guard*", "Swordmaster's Guard", "Duelist's Trance*", "Whirlwind Blade*", "Radiolaria Transposer*", "Kill Tracker", "Tier 1: Impact*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 0, "Type": "Sword", "Velocity": 0, "Year": 7, "Zoom": 0, }, { "AA": 57, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 36, "Archetype": "Precision Frame", "Blast Radius": 34, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 56, "Hash": 4195186942, "Holofoil": false, "Id": ""6917530109020369928"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 4, "Masterwork Type": "Blast Radius", "Name": "Faith-Keeper", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Quick Launch*", "Smart Drift Control", "High-Velocity Rounds*", "Impact Casing", "Repulsor Brace*", "Frenzy*", "Radiolaria Transposer*", "Kill Tracker", "Tier 4: Blast Radius*", ], "Power": 10, "ROF": 15, "Range": 0, "Rarity": "Legendary", "Recoil": 66, "Reload": 33, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 72, "Year": 7, "Zoom": 20, }, { "AA": 40, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 2126178511, "Holofoil": false, "Id": ""6917530109022272361"", "Impact": 35, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 20, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Corrasion", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Corkscrew Rifling*", "Tactical Mag*", "Pugilist*", "Swashbuckler*", "Radiolaria Transposer*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 324, "Range": 80, "Rarity": "Legendary", "Recoil": 72, "Reload": 47, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 80, "Hash": 2249996761, "Holofoil": false, "Id": ""6917530109022273677"", "Impact": 60, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 16, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Patron of Lost Causes", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Tactical Mag*", "Extended Mag", "Stats for All*", "Explosive Payload*", "Cast No Shadows*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 200, "Range": 33, "Rarity": "Legendary", "Recoil": 59, "Reload": 63, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 20, }, { "AA": 60, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "heavy", "Ammo Generation": 28, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 21, "Hash": 2450049485, "Holofoil": false, "Id": ""6917530109024146765"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Line in the Sand", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Chambered Compensator*", "Full Bore", "Enhanced Battery*", "Projection Fuse", "Rapid Hit*", "Reservoir Burst*", "Cast No Shadows*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 0, "Range": 29, "Rarity": "Legendary", "Recoil": 73, "Reload": 23, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 53, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 63, "Archetype": "Area Denial Frame", "Blast Radius": 100, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 62, "Hash": 1197771438, "Holofoil": false, "Id": ""6917530109024146995"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Lost Signal", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Area Denial Frame*", "Volatile Launch*", "Confined Launch", "High-Velocity Rounds*", "Implosion Rounds", "Feeding Frenzy*", "Vorpal Weapon*", "Radiolaria Transposer*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 72, "Range": 0, "Rarity": "Legendary", "Recoil": 79, "Reload": 75, "Season": 24, "Shield Duration": 0, "Source": "echoes", "Stability": 21, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 77, "Year": 7, "Zoom": 13, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 44, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 18, "Hash": 2510526114, "Holofoil": false, "Id": ""6917530111272949243"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 34, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Unending Tempest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Full Bore*", "Smallbore", "Tactical Mag*", "Alloy Magazine", "Moving Target*", "Harmony*", "One Quiet Moment*", "Nadir Focus", "Kill Tracker", "Backup Mag*", "Tier 2: Range*", ], "Power": 10, "ROF": 600, "Range": 70, "Rarity": "Legendary", "Recoil": 85, "Reload": 29, "Season": 22, "Shield Duration": 0, "Source": "crucible", "Stability": 36, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 37, "Accuracy": 0, "Airborne Effectiveness": 17, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 3776430252, "Holofoil": false, "Id": ""6917530111276964182"", "Impact": 67, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 14, "Masterwork Tier": 4, "Masterwork Type": "Range", "Name": "Admetus-D", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Full Bore*", "Hammer-Forged Rifling", "Alloy Magazine*", "Flared Magwell", "Keep Away*", "Precision Instrument*", "Häkke Breach Armaments*", "Kill Tracker", "Tier 4: Range*", ], "Power": 10, "ROF": 150, "Range": 84, "Rarity": "Legendary", "Recoil": 76, "Reload": 35, "Season": 26, "Shield Duration": 0, "Source": "engram", "Stability": 19, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 21, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 30, "Ammo": "special", "Ammo Generation": 42, "Archetype": "Micro-Missile Frame", "Blast Radius": 36, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 3922217119, "Holofoil": false, "Id": ""6917530111276968601"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 12, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Lotus-Eater", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Micro-Missile Frame*", "Countermass*", "Hard Launch", "Alloy Magazine*", "High-Velocity Rounds", "Shoot to Loot*", "Destabilizing Rounds*", "Stunning Recovery*", "Vanguard's Vindication", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 100, "Reload": 55, "Season": 26, "Shield Duration": 0, "Source": "nightfall", "Stability": 57, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 34, "Year": 7, "Zoom": 18, }, { "AA": 49, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 64, "Hash": 3959549446, "Holofoil": false, "Id": ""6917530111278917456"", "Impact": 15, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 44, "Masterwork Tier": 5, "Masterwork Type": "Reload Speed", "Name": "Yarovit MG4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Fluted Barrel*", "Full Bore", "Appended Mag*", "Light Mag", "Dynamic Sway Reduction*", "Deconstruct*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 5: Reload Speed*", ], "Power": 10, "ROF": 900, "Range": 22, "Rarity": "Legendary", "Recoil": 98, "Reload": 30, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 50, "Accuracy": 0, "Airborne Effectiveness": 9, "Ammo": "special", "Ammo Generation": 20, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 1612781792, "Holofoil": false, "Id": ""6917530111280867369"", "Impact": 65, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 7, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Retrofuturist", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Rifled Barrel*", "Full Choke", "Steady Rounds*", "Light Mag", "Quickdraw*", "Trench Barrel*", "One Quiet Moment*", "Nadir Focus", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 80, "Range": 46, "Rarity": "Legendary", "Recoil": 55, "Reload": 56, "Season": 23, "Shield Duration": 0, "Source": "crucible", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 6, "Zoom": 12, }, { "AA": 32, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 33, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 32287609, "Holofoil": false, "Id": ""6917530111280870475"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 30, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "Boondoggle Mk. 55", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Arrowhead Brake*", "Smallbore", "High-Caliber Rounds*", "Light Mag", "To the Pain*", "Swashbuckler*", "Tex Balanced Stock*", "Kill Tracker", "Tier 4: Handling*", ], "Power": 10, "ROF": 720, "Range": 52, "Rarity": "Legendary", "Recoil": 100, "Reload": 21, "Season": 26, "Shield Duration": 0, "Source": "engram", "Stability": 15, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 26, "Ammo": "primary", "Ammo Generation": 44, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 2510526114, "Holofoil": false, "Id": ""6917530111282835005"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 29, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "Unending Tempest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Arrowhead Brake*", "Fluted Barrel", "Steady Rounds*", "Flared Magwell", "Gutshot Straight*", "Rangefinder*", "One Quiet Moment*", "Nadir Focus", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 600, "Range": 50, "Rarity": "Legendary", "Recoil": 100, "Reload": 19, "Season": 22, "Shield Duration": 0, "Source": "crucible", "Stability": 56, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 79, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 60, "Hash": 2876244791, "Holofoil": false, "Id": ""6917530111282836382"", "Impact": 84, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 11, "Masterwork Tier": 2, "Masterwork Type": "Range", "Name": "The Palindrome", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Extended Barrel", "Tactical Mag*", "Extended Mag", "To the Pain*", "Opening Shot*", "Stunning Recovery*", "Vanguard's Vindication", "Wild Card", "Kill Tracker", "Tier 2: Range*", ], "Power": 10, "ROF": 140, "Range": 52, "Rarity": "Legendary", "Recoil": 100, "Reload": 52, "Season": 26, "Shield Duration": 0, "Source": "nightfall", "Stability": 63, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 90, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Starlight Beam", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 78, "Hash": 3725585710, "Holofoil": false, "Id": ""6917530111284606319"", "Impact": 6, "Kill Tracker": 27, "Loadouts": "", "Locked": false, "Mag": 74, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Lodestar", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Starlight Beam*", "Fluted Barrel*", "Phase Converter*", "Arc Alignment*", "Composite Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 1000, "Range": 76, "Rarity": "Exotic", "Recoil": 95, "Reload": 65, "Season": 26, "Shield Duration": 0, "Source": "seasonpass", "Stability": 95, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Trace Rifle", "Velocity": 0, "Year": 7, "Zoom": 17, }, { "AA": 43, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 41, "Hash": 157601190, "Holofoil": false, "Id": ""6917530111284606465"", "Impact": 34, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 24, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Joxer's Longsword", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Corkscrew Rifling*", "Extended Barrel", "Extended Mag*", "Alloy Magazine", "Dragonfly*", "Demoralize*", "One Quiet Moment*", "Nadir Focus", "Kill Tracker", "Tier 1: Range*", ], "Power": 10, "ROF": 324, "Range": 80, "Rarity": "Legendary", "Recoil": 78, "Reload": 13, "Season": 26, "Shield Duration": 0, "Source": "crucible", "Stability": 38, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 43, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 157601190, "Holofoil": false, "Id": ""6917530111286367074"", "Impact": 34, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 24, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Joxer's Longsword", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Corkscrew Rifling*", "Extended Barrel", "Extended Mag*", "High-Caliber Rounds", "Shoot to Loot*", "One for All*", "One Quiet Moment*", "Nadir Focus", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 324, "Range": 79, "Rarity": "Legendary", "Recoil": 78, "Reload": 13, "Season": 26, "Shield Duration": 0, "Source": "crucible", "Stability": 38, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 43, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 49, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 31, "Hash": 157601190, "Holofoil": false, "Id": ""6917530111738111342"", "Impact": 34, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 18, "Masterwork Tier": 5, "Masterwork Type": "Reload Speed", "Name": "Joxer's Longsword", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Heavy Burst*", "Chambered Compensator*", "Extended Barrel", "Alloy Magazine*", "Appended Mag", "Gutshot Straight*", "One for All*", "One Quiet Moment*", "Nadir Focus", "Kill Tracker", "Tier 5: Reload Speed*", ], "Power": 10, "ROF": 324, "Range": 74, "Rarity": "Legendary", "Recoil": 88, "Reload": 38, "Season": 26, "Shield Duration": 0, "Source": "crucible", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Pulse Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 79, "Accuracy": 0, "Airborne Effectiveness": 21, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 61, "Hash": 2876244791, "Holofoil": false, "Id": ""6917530111740119630"", "Impact": 84, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 10, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "The Palindrome", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "Polygonal Rifling", "Accurized Rounds*", "Appended Mag", "Recycled Energy*", "Master of Arms*", "Stunning Recovery*", "Vanguard's Vindication", "Wild Card", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 140, "Range": 60, "Rarity": "Legendary", "Recoil": 100, "Reload": 42, "Season": 26, "Shield Duration": 0, "Source": "nightfall", "Stability": 58, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Hand Cannon", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 32, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 33, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 32287609, "Holofoil": false, "Id": ""6917530111742618086"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 30, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Boondoggle Mk. 55", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Arrowhead Brake*", "Full Bore", "High-Caliber Rounds*", "Ricochet Rounds", "Subsistence*", "Swashbuckler*", "Tex Balanced Stock*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 10, "ROF": 720, "Range": 52, "Rarity": "Legendary", "Recoil": 100, "Reload": 21, "Season": 26, "Shield Duration": 0, "Source": "engram", "Stability": 15, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 58, "Accuracy": 0, "Airborne Effectiveness": 29, "Ammo": "primary", "Ammo Generation": 44, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "nadir", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 33, "Hash": 2510526114, "Holofoil": false, "Id": ""6917530111742619421"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 33, "Masterwork Tier": 5, "Masterwork Type": "Handling", "Name": "Unending Tempest", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Full Bore", "Extended Mag*", "Alloy Magazine", "Offhand Strike*", "Collective Action*", "One Quiet Moment*", "Nadir Focus", "Kill Tracker", "Tier 5: Handling*", ], "Power": 10, "ROF": 600, "Range": 58, "Rarity": "Legendary", "Recoil": 85, "Reload": 0, "Season": 22, "Shield Duration": 0, "Source": "crucible", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 14, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 30, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 72, "Hash": 1051949956, "Holofoil": false, "Id": ""6917530111742620943"", "Impact": 43, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 14, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Anonymous Autumn", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Arrowhead Brake*", "Hammer-Forged Rifling", "Armor-Piercing Rounds*", "High-Caliber Rounds", "Closing Time*", "Multikill Clip*", "One Quiet Moment*", "Field-Tested", "Kill Tracker", "Tier 2: Handling*", ], "Power": 10, "ROF": 360, "Range": 34, "Rarity": "Legendary", "Recoil": 100, "Reload": 45, "Season": 25, "Shield Duration": 0, "Source": "crucible", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 62, "Accuracy": 0, "Airborne Effectiveness": 6, "Ammo": "special", "Ammo Generation": 60, "Archetype": "Honed Edge", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 47, "Hash": 3211806999, "Holofoil": false, "Id": ""6917530112162280321"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 4, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Izanagi's Burden", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Honed Edge*", "Chambered Compensator*", "Accurized Rounds*", "No Distractions*", "Composite Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 10, "ROF": 90, "Range": 58, "Rarity": "Exotic", "Recoil": 83, "Reload": 40, "Season": 5, "Shield Duration": 0, "Source": "blackarmory", "Stability": 56, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 2, "Zoom": 45, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 54, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 66, "Hash": 3105930175, "Holofoil": false, "Id": ""6917530112164036957"", "Impact": 41, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 48, "Masterwork Tier": 10, "Masterwork Type": "Range", "Name": "Chain of Command", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Arrowhead Brake*", "High-Caliber Rounds*", "Adrenaline Junkie*", "Osmosis", "Demolitionist*", "Adaptive Munitions", "Kill Tracker", "Vanguard's Vindication*", "One Quiet Moment", "Gun and Run", "Masterworked: Range*", ], "Power": 10, "ROF": 450, "Range": 58, "Rarity": "Legendary", "Recoil": 100, "Reload": 46, "Season": 17, "Shield Duration": 0, "Source": "crucible", "Stability": 37, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Machine Gun", "Velocity": 0, "Year": 5, "Zoom": 16, }, { "AA": 67, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 10, "Archetype": "Double Fire", "Blast Radius": 76, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 38, "Hash": 1206729100, "Holofoil": false, "Id": ""6917530112164038292"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": 1, "Masterwork Type": "Blast Radius", "Name": "Wilderflight", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Double Fire*", "Volatile Launch*", "Hard Launch", "High-Velocity Rounds*", "Implosion Rounds", "Repulsor Brace*", "Lead from Gold*", "Tex Balanced Stock*", "Gravity Well", "Kill Tracker", "Tier 1: Blast Radius*", ], "Power": 10, "ROF": 100, "Range": 0, "Rarity": "Legendary", "Recoil": 25, "Reload": 42, "Season": 26, "Shield Duration": 0, "Source": "dungeon", "Stability": 20, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 98, "Year": 7, "Zoom": 13, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 4249949938, "Holofoil": false, "Id": ""6917530112165844881"", "Impact": 100, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 15, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Long Arm", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Arrowhead Brake*", "Hammer-Forged Rifling", "Tactical Mag*", "Flared Magwell", "Dual Loader*", "Redirection*", "Tex Balanced Stock*", "Gravity Well", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 120, "Range": 55, "Rarity": "Legendary", "Recoil": 100, "Reload": 56, "Season": 26, "Shield Duration": 0, "Source": "dungeon", "Stability": 30, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 34, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "primary", "Ammo Generation": 60, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 46, "Hash": 3483591058, "Holofoil": false, "Id": ""6917530112165845378"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 31, "Masterwork Tier": 10, "Masterwork Type": "Reload Speed", "Name": "Prosecutor", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Smallbore*", "Armor-Piercing Rounds*", "Rewind Rounds*", "Golden Tricorn*", "Crossing Over*", "Kill Tracker", "Masterworked: Reload Speed*", ], "Power": 10, "ROF": 450, "Range": 72, "Rarity": "Legendary", "Recoil": 69, "Reload": 55, "Season": 23, "Shield Duration": 0, "Source": "dungeon", "Stability": 49, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 6, "Zoom": 16, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 53, "Hash": 4249949938, "Holofoil": false, "Id": ""6917530112165845950"", "Impact": 100, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 16, "Masterwork Tier": 4, "Masterwork Type": "Stability", "Name": "Long Arm", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Hammer-Forged Rifling*", "Smallbore", "Appended Mag*", "Tactical Mag", "Lone Wolf*", "Explosive Payload*", "Tex Balanced Stock*", "Gravity Well", "Kill Tracker", "Tier 4: Stability*", ], "Power": 10, "ROF": 120, "Range": 65, "Rarity": "Legendary", "Recoil": 72, "Reload": 45, "Season": 26, "Shield Duration": 0, "Source": "dungeon", "Stability": 29, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 52, "Accuracy": 0, "Airborne Effectiveness": 14, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 68, "Hash": 4249949938, "Holofoil": false, "Id": ""6917530112165848498"", "Impact": 100, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 16, "Masterwork Tier": 5, "Masterwork Type": "Range", "Name": "Long Arm", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Fluted Barrel*", "Smallbore", "Appended Mag*", "Flared Magwell", "Rapid Hit*", "Dragonfly*", "Tex Balanced Stock*", "Gravity Well", "Kill Tracker", "Tier 5: Range*", ], "Power": 10, "ROF": 120, "Range": 60, "Rarity": "Legendary", "Recoil": 72, "Reload": 45, "Season": 26, "Shield Duration": 0, "Source": "dungeon", "Stability": 30, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 7, "Zoom": 18, }, { "AA": 54, "Accuracy": 0, "Airborne Effectiveness": 12, "Ammo": "primary", "Ammo Generation": 30, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 26, "Hash": 1013434963, "Holofoil": false, "Id": ""6917530112165850184"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 32, "Masterwork Tier": 10, "Masterwork Type": "Stability", "Name": "Adjudicator", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Tactical Mag*", "Dynamic Sway Reduction*", "Target Lock*", "Crossing Over*", "Kill Tracker", "Masterworked: Stability*", ], "Power": 10, "ROF": 600, "Range": 61, "Rarity": "Legendary", "Recoil": 93, "Reload": 26, "Season": 23, "Shield Duration": 0, "Source": "dungeon", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 6, "Zoom": 13, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 29, "Archetype": "Adaptive Frame", "Blast Radius": 65, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 34, "Hash": 2599338625, "Holofoil": false, "Id": ""6917530112171095075"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Bitter/Sweet", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Volatile Launch*", "Hard Launch", "Alloy Casing*", "High-Explosive Ordnance", "Stats for All*", "Harmony*", "Dark Ether Reaper*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 58, "Reload": 74, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 32, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 25, "Year": 7, "Zoom": 13, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 42, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 2913577176, "Holofoil": false, "Id": ""6917530112171098379"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Scavenger's Fate", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Appended Mag*", "Slideshot*", "Desperate Measures*", "Dark Ether Reaper*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 65, "Range": 65, "Rarity": "Legendary", "Recoil": 77, "Reload": 46, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 44, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Shotgun", "Velocity": 0, "Year": 7, "Zoom": 12, }, { "AA": 46, "Accuracy": 0, "Airborne Effectiveness": 8, "Ammo": "special", "Ammo Generation": 30, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 657, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 49, "Hash": 891996636, "Holofoil": false, "Id": ""6917530112172735963"", "Impact": 95, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 8, "Masterwork Tier": 3, "Masterwork Type": "Charge Time", "Name": "Cruoris FR4", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Fluted Barrel*", "Hammer-Forged Rifling", "Ionized Battery*", "Projection Fuse", "Stats for All*", "Barrel Constrictor*", "Omolon Fluid Dynamics*", "Kill Tracker", "Tier 3: Charge Time*", ], "Power": 10, "ROF": 0, "Range": 37, "Rarity": "Legendary", "Recoil": 55, "Reload": 11, "Season": 26, "Shield Duration": 0, "Source": "engram", "Stability": 41, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 15, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 22, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 23, "Hash": 825495813, "Holofoil": false, "Id": ""6917530112172736104"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 29, "Masterwork Tier": 3, "Masterwork Type": "Stability", "Name": "Noxious Vetiver", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Full Bore", "Alloy Magazine*", "Flared Magwell", "Attrition Orbs*", "Target Lock*", "Dark Ether Reaper*", "Kill Tracker", "Tier 3: Stability*", ], "Power": 10, "ROF": 600, "Range": 53, "Rarity": "Legendary", "Recoil": 94, "Reload": 19, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 29, "Archetype": "Adaptive Frame", "Blast Radius": 60, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 42, "Hash": 2599338625, "Holofoil": false, "Id": ""6917530112174429906"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Bitter/Sweet", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Confined Launch*", "Countermass", "Spike Grenades*", "High-Explosive Ordnance", "Loose Change*", "Jolting Feedback*", "Dark Ether Reaper*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 58, "Reload": 42, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 67, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 20, "Year": 7, "Zoom": 13, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 11, "Ammo": "heavy", "Ammo Generation": 29, "Archetype": "Adaptive Frame", "Blast Radius": 30, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 57, "Hash": 2599338625, "Holofoil": false, "Id": ""6917530112174430771"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 3, "Masterwork Type": "Handling", "Name": "Bitter/Sweet", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Quick Launch*", "Smart Drift Control", "Proximity Grenades*", "Spike Grenades", "Reverberation*", "Harmony*", "Dark Ether Reaper*", "Kill Tracker", "Tier 3: Handling*", ], "Power": 10, "ROF": 120, "Range": 0, "Rarity": "Legendary", "Recoil": 58, "Reload": 42, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 42, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Grenade Launcher", "Velocity": 40, "Year": 7, "Zoom": 13, }, { "AA": 59, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 40, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 61, "Hash": 3818198556, "Holofoil": false, "Id": ""6917530112178262867"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Sovereignty", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Adaptive Frame*", "Fluted Barrel*", "Appended Mag*", "No Distractions*", "Withering Gaze*", "Dark Ether Reaper*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 90, "Range": 44, "Rarity": "Legendary", "Recoil": 77, "Reload": 39, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 45, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 22, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 23, "Hash": 825495813, "Holofoil": false, "Id": ""6917530112186358843"", "Impact": 25, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 29, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Noxious Vetiver", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Alloy Magazine*", "Pugilist*", "Desperate Measures*", "Dark Ether Reaper*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 10, "ROF": 600, "Range": 53, "Rarity": "Legendary", "Recoil": 94, "Reload": 20, "Season": 25, "Shield Duration": 0, "Source": "revenant", "Stability": 43, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 68, "Accuracy": 65, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 74, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 667, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 67, "Hash": 2848549302, "Holofoil": false, "Id": ""6917530113046220341"", "Impact": 76, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Neoptolemus II", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Agile Bowstring*", "Elastic String", "Carbon Arrow Shaft*", "Natural Fletching", "Air Trigger*", "Wellspring*", "Wild Card*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 69, "Reload": 35, "Season": 25, "Shield Duration": 0, "Source": "engram", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 7, "Zoom": 20, }, { "AA": 64, "Accuracy": 0, "Airborne Effectiveness": 23, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Support Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 70, "Hash": 3229982889, "Holofoil": false, "Id": ""6917530113048274235"", "Impact": 21, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 44, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Adamantite", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Support Frame*", "Polygonal Rifling*", "Accurized Rounds*", "Slice*", "Tear*", "Willing Vessel*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 600, "Range": 49, "Rarity": "Legendary", "Recoil": 52, "Reload": 66, "Season": 26, "Shield Duration": 0, "Source": "heresy", "Stability": 60, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 7, "Zoom": 16, }, { "AA": 68, "Accuracy": 65, "Airborne Effectiveness": 20, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 667, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "cassoid", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 67, "Hash": 2848549302, "Holofoil": false, "Id": ""6917530113048278770"", "Impact": 76, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Neoptolemus II", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Agile Bowstring*", "Elastic String", "Carbon Arrow Shaft*", "Natural Fletching", "Slickdraw*", "Lone Wolf*", "Wild Card*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 69, "Reload": 35, "Season": 25, "Shield Duration": 0, "Source": "engram", "Stability": 55, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 7, "Zoom": 20, }, { "AA": 82, "Accuracy": 41, "Airborne Effectiveness": 7, "Ammo": "primary", "Ammo Generation": 64, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 567, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 82, "Hash": 2513965917, "Holofoil": false, "Id": ""6917530126464419598"", "Impact": 68, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 0, "Masterwork Tier": 1, "Masterwork Type": "Accuracy", "Name": "Lunulata-4b", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Lightweight Frame*", "Agile Bowstring*", "High Tension String", "Fiberglass Arrow Shaft*", "Natural Fletching", "No Distractions*", "Golden Tricorn*", "Veist Stinger*", "Kill Tracker", "Tier 1: Target Acquisition*", ], "Power": 10, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 49, "Reload": 60, "Season": 17, "Shield Duration": 0, "Source": "engram", "Stability": 59, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Combat Bow", "Velocity": 0, "Year": 5, "Zoom": 18, }, { "AA": 85, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 34, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 1916287826, "Holofoil": false, "Id": ""6917530126464419889"", "Impact": 51, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 12, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "Boudica-C", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Polygonal Rifling*", "Smallbore", "Flared Magwell*", "Light Mag", "Stats for All*", "Osmosis*", "Häkke Breach Armaments*", "Kill Tracker", "Tier 4: Handling*", ], "Power": 10, "ROF": 260, "Range": 63, "Rarity": "Legendary", "Recoil": 98, "Reload": 40, "Season": 18, "Shield Duration": 0, "Source": "engram", "Stability": 58, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sidearm", "Velocity": 0, "Year": 5, "Zoom": 12, }, { "AA": 61, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 32, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 32, "Hash": 3615421669, "Holofoil": false, "Id": ""6917530126464422237"", "Impact": 38, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Suspectum-4fr", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Corkscrew Rifling*", "Full Bore", "Accelerated Coils*", "Liquid Coils", "Headstone*", "High Ground*", "Veist Stinger*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 10, "ROF": 0, "Range": 44, "Rarity": "Legendary", "Recoil": 67, "Reload": 25, "Season": 24, "Shield Duration": 0, "Source": "engram", "Stability": 47, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 32, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 33, "Archetype": "Aggressive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "tex-mechanica", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 52, "Hash": 32287609, "Holofoil": false, "Id": ""6917530126465842488"", "Impact": 22, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 30, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Boondoggle Mk. 55", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Aggressive Frame*", "Hammer-Forged Rifling*", "Polygonal Rifling", "High-Caliber Rounds*", "Flared Magwell", "To the Pain*", "Tap the Trigger*", "Tex Balanced Stock*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 10, "ROF": 720, "Range": 62, "Rarity": "Legendary", "Recoil": 96, "Reload": 23, "Season": 26, "Shield Duration": 0, "Source": "engram", "Stability": 15, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Submachine Gun", "Velocity": 0, "Year": 7, "Zoom": 14, }, { "AA": 84, "Accuracy": 0, "Airborne Effectiveness": 16, "Ammo": "primary", "Ammo Generation": 57, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 970034755, "Holofoil": false, "Id": ""6917530126805090594"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 46, "Masterwork Tier": 4, "Masterwork Type": "Handling", "Name": "Giver's Blessing", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Full Bore*", "Polygonal Rifling", "Flared Magwell*", "Light Mag", "Feeding Frenzy*", "Kinetic Tremors*", "Exhaustive Research*", "Kill Tracker", "Tier 4: Handling*", ], "Power": 22, "ROF": 720, "Range": 48, "Rarity": "Legendary", "Recoil": 57, "Reload": 57, "Season": 27, "Shield Duration": 0, "Source": "kepler", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Auto Rifle", "Velocity": 0, "Year": 8, "Zoom": 16, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 24, "Ammo": "primary", "Ammo Generation": 51, "Archetype": "Tri-Planar Mass Driver", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 54, "Hash": 2973900274, "Holofoil": false, "Id": ""6917530126834883984"", "Impact": 60, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 16, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Third Iteration", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Tri-Planar Mass Driver*", "Smallbore*", "Modified Heatsink*", "Amalgamation Rounds*", "Composite Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 14, "ROF": 200, "Range": 42, "Rarity": "Exotic", "Recoil": 77, "Reload": 59, "Season": 27, "Shield Duration": 0, "Source": "seasonpass", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Scout Rifle", "Velocity": 0, "Year": 8, "Zoom": 19, }, { "AA": 69, "Accuracy": 0, "Airborne Effectiveness": 1, "Ammo": "special", "Ammo Generation": 54, "Archetype": "The Master", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "field-forged", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 74, "Hash": 2581676735, "Holofoil": false, "Id": ""6917530126836680435"", "Impact": 90, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 4, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "New Land Beyond", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "The Master*", "Horizon Ironsights*", "Accurized Rounds*", "Bullseye Bolster*", "Snapshot Sights*", "Short-Action Stock*", "Kill Tracker", "Empty Catalyst Socket*", ], "Power": 14, "ROF": 72, "Range": 87, "Rarity": "Exotic", "Recoil": 80, "Reload": 21, "Season": 26, "Shield Duration": 0, "Source": "deluxe", "Stability": 38, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 7, "Zoom": 25, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 13, "Ammo": "primary", "Ammo Generation": 52, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 25, "Hash": 1411560894, "Holofoil": false, "Id": ""6917530126870310720"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 50, "Masterwork Tier": 1, "Masterwork Type": "Stability", "Name": "Ahab Char", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Full Bore*", "Hammer-Forged Rifling", "Alloy Magazine*", "Appended Mag", "Subsistence*", "Eye of the Storm*", "Bray Legacy*", "Kill Tracker", "Tier 1: Stability*", ], "Power": 17, "ROF": 720, "Range": 45, "Rarity": "Legendary", "Recoil": 50, "Reload": 45, "Season": 27, "Shield Duration": 0, "Source": "campaign", "Stability": 46, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Auto Rifle", "Velocity": 0, "Year": 8, "Zoom": 16, }, { "AA": 38, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 24, "Hash": 1674692344, "Holofoil": false, "Id": ""6917530129161826867"", "Impact": 67, "Kill Tracker": 2, "Loadouts": "", "Locked": false, "Mag": 12, "Masterwork Tier": 2, "Masterwork Type": "Handling", "Name": "Sublimation", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "High-Impact Frame*", "Chambered Compensator*", "Fluted Barrel", "Accurized Rounds*", "Flared Magwell", "Eddy Current*", "Adagio*", "Exhaustive Research*", "Kill Tracker", "Tier 2: Handling*", ], "Power": 31, "ROF": 150, "Range": 82, "Rarity": "Legendary", "Recoil": 82, "Reload": 27, "Season": 27, "Shield Duration": 0, "Source": "kepler", "Stability": 36, "Swing Speed": 0, "Tag": undefined, "Tier": 2, "Type": "Scout Rifle", "Velocity": 0, "Year": 8, "Zoom": 21, }, { "AA": 66, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "special", "Ammo Generation": 48, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "omolon", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 62, "Hash": 4157959956, "Holofoil": false, "Id": ""6917530129161829515"", "Impact": 55, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Tongeren-LR3", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Rapid-Fire Frame*", "Longview SLR20*", "Longview SLR10", "Ricochet Rounds*", "Field Prep*", ], "Power": 18, "ROF": 140, "Range": 40, "Rarity": "Rare", "Recoil": 48, "Reload": 52, "Season": 4, "Shield Duration": 0, "Source": "campaign", "Stability": 41, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Sniper Rifle", "Velocity": 0, "Year": 2, "Zoom": 55, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 4, "Ammo": "special", "Ammo Generation": 45, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 41, "Hash": 367772693, "Holofoil": false, "Id": ""6917530129163830469"", "Impact": 70, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": 1, "Masterwork Type": "Reload Speed", "Name": "Precipial", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Precision Frame*", "Smoothbore*", "Corkscrew Rifling", "Tactical Mag*", "Light Mag", "Proximity Power*", "Vorpal Weapon*", "Exhaustive Research*", "Kill Tracker", "Tier 1: Reload Speed*", ], "Power": 24, "ROF": 65, "Range": 83, "Rarity": "Legendary", "Recoil": 75, "Reload": 49, "Season": 27, "Shield Duration": 0, "Source": "kepler", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Shotgun", "Velocity": 0, "Year": 8, "Zoom": 12, }, { "AA": 50, "Accuracy": 58, "Airborne Effectiveness": 16, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "High-Impact Longbow", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 767, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": "hakke", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 20, "Hash": 2838279629, "Holofoil": false, "Id": ""6917530129172938726"", "Impact": 92, "Kill Tracker": 0, "Loadouts": "", "Locked": true, "Mag": 0, "Masterwork Tier": 3, "Masterwork Type": "Accuracy", "Name": "Mercury-A", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "High-Impact Longbow*", "Tactile String*", "High Tension String", "Carbon Arrow Shaft*", "Straight Fletching", "Demolitionist*", "Elemental Honing*", "Vanguard Determination*", "Häkke Breach Armaments", "Tenacity", "Kill Tracker", "Tier 3: Target Acquisition*", ], "Power": 29, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 58, "Reload": 30, "Season": 27, "Shield Duration": 0, "Source": "edgeoffate", "Stability": 90, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Combat Bow", "Velocity": 0, "Year": 8, "Zoom": 18, }, { "AA": 36, "Accuracy": 0, "Airborne Effectiveness": 18, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 24, "Hash": 4090134063, "Holofoil": false, "Id": ""6917530129172939220"", "Impact": 67, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 14, "Masterwork Tier": 1, "Masterwork Type": "Handling", "Name": "Jurisprudent", "New Gear": true, "Notes": undefined, "Owner": "Vault", "Perks": [ "High-Impact Frame*", "Full Bore*", "Polygonal Rifling", "Tactical Mag*", "Steady Rounds", "Built to Blast*", "Focused Fury*", "Bray Legacy*", "Kill Tracker", "Tier 1: Handling*", ], "Power": 26, "ROF": 150, "Range": 83, "Rarity": "Legendary", "Recoil": 66, "Reload": 43, "Season": 27, "Shield Duration": 0, "Source": "campaign", "Stability": 18, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Scout Rifle", "Velocity": 0, "Year": 8, "Zoom": 21, }, { "AA": 55, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 26, "Archetype": "Adaptive Frame", "Blast Radius": 45, "Category": "Power", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 1877183765, "Holofoil": false, "Id": ""6917530129172940363"", "Impact": 0, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 1, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Cup-Bearer SA/2", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Adaptive Frame*", "Hard Launch*", "Linear Compensator", "High-Velocity Rounds*", "Cluster Bomb*", ], "Power": 23, "ROF": 20, "Range": 0, "Rarity": "Rare", "Recoil": 53, "Reload": 48, "Season": 4, "Shield Duration": 0, "Source": "campaign", "Stability": 26, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Rocket Launcher", "Velocity": 67, "Year": 2, "Zoom": 20, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "primary", "Ammo Generation": 47, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 2351747818, "Holofoil": false, "Id": ""6917530129174837332"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 46, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Sand Wasp-3au", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Polygonal Rifling", "High-Caliber Rounds*", "Dynamic Sway Reduction*", ], "Power": 26, "ROF": 720, "Range": 33, "Rarity": "Rare", "Recoil": 81, "Reload": 45, "Season": 4, "Shield Duration": 0, "Source": "campaign", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 2, "Zoom": 16, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 10, "Ammo": "heavy", "Ammo Generation": 31, "Archetype": "Adaptive Burst", "Blast Radius": 0, "Category": "Power", "Charge Rate": 0, "Charge Time": 533, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 35, "Hash": 3926153598, "Holofoil": false, "Id": ""6917530129176642416"", "Impact": 38, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 6, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Boomslang-4fr", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Adaptive Burst*", "Arrowhead Brake*", "Hammer-Forged Rifling", "Accelerated Coils*", "Projection Fuse", "Transcendent Moment*", "Reservoir Burst*", "Vanguard Determination*", "Veist Stinger", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 30, "ROF": 0, "Range": 47, "Rarity": "Legendary", "Recoil": 100, "Reload": 33, "Season": 27, "Shield Duration": 0, "Source": "edgeoffate", "Stability": 51, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Linear Fusion Rifle", "Velocity": 0, "Year": 8, "Zoom": 25, }, { "AA": 42, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Precision Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Kinetic", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 41, "Hash": 2351747816, "Holofoil": false, "Id": ""6917530129176647466"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 31, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Cuboid ARu", "New Gear": false, "Notes": undefined, "Owner": "Vault", "Perks": [ "Precision Frame*", "Red Dot-ORS1*", "Red Dot-ORS", "Armor-Piercing Rounds*", "Moving Target*", ], "Power": 29, "ROF": 450, "Range": 68, "Rarity": "Rare", "Recoil": 75, "Reload": 35, "Season": 4, "Shield Duration": 0, "Source": "campaign", "Stability": 38, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 2, "Zoom": 18, }, { "AA": 75, "Accuracy": 0, "Airborne Effectiveness": 3, "Ammo": "special", "Ammo Generation": 55, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Void", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 75, "Hash": 1193318082, "Holofoil": false, "Id": ""6917530129178802818"", "Impact": 55, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 5, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Shoreline Dissident", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Rapid-Fire Frame*", "Corkscrew Rifling*", "Full Bore", "Accurized Rounds*", "Extended Mag", "Recycled Energy*", "Destabilizing Rounds*", "Bray Legacy*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 35, "ROF": 140, "Range": 50, "Rarity": "Legendary", "Recoil": 55, "Reload": 62, "Season": 27, "Shield Duration": 0, "Source": "campaign", "Stability": 50, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Sniper Rifle", "Velocity": 0, "Year": 8, "Zoom": 35, }, { "AA": 0, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "heavy", "Ammo Generation": 0, "Archetype": "Wave Sword Frame", "Blast Radius": 0, "Category": "Power", "Charge Rate": 50, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 80, "Handling": 0, "Hash": 2111625436, "Holofoil": false, "Id": ""6917530141789959558"", "Impact": 60, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 74, "Masterwork Tier": 1, "Masterwork Type": "Impact", "Name": "Aurora Dawn", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Wave Sword Frame*", "Enduring Blade*", "Honed Edge", "Tempered Edge", "Burst Guard*", "Enduring Guard", "Sharp Harvest*", "One for All*", "Vanguard Determination*", "Kill Tracker", "Tier 1: Impact*", ], "Power": 29, "ROF": 0, "Range": 0, "Rarity": "Legendary", "Recoil": 0, "Reload": 0, "Season": 27, "Shield Duration": 0, "Source": "edgeoffate", "Stability": 0, "Swing Speed": 40, "Tag": undefined, "Tier": 1, "Type": "Sword", "Velocity": 0, "Year": 8, "Zoom": 0, }, { "AA": 72, "Accuracy": 0, "Airborne Effectiveness": 36, "Ammo": "primary", "Ammo Generation": 50, "Archetype": "Lightweight Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Stasis", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 63, "Hash": 1663482635, "Holofoil": false, "Id": ""6917530141791771674"", "Impact": 43, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 17, "Masterwork Tier": 3, "Masterwork Type": "Range", "Name": "Faustus Decline", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Lightweight Frame*", "Chambered Compensator*", "Hammer-Forged Rifling", "Extended Mag*", "Appended Mag", "Rimestealer*", "Headstone*", "Bray Legacy*", "Kill Tracker", "Tier 3: Range*", ], "Power": 29, "ROF": 360, "Range": 28, "Rarity": "Legendary", "Recoil": 100, "Reload": 30, "Season": 27, "Shield Duration": 0, "Source": "campaign", "Stability": 52, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Sidearm", "Velocity": 0, "Year": 8, "Zoom": 12, }, { "AA": 63, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 55, "Archetype": "Heavy Burst", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Solar", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 29, "Hash": 4124362340, "Holofoil": false, "Id": ""6917530141795465741"", "Impact": 92, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 18, "Masterwork Tier": 2, "Masterwork Type": "Stability", "Name": "Agape", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Heavy Burst*", "Polygonal Rifling*", "Smallbore", "Appended Mag*", "High-Caliber Rounds", "Heal Clip*", "Master of Arms*", "Exhaustive Research*", "Kill Tracker", "Tier 2: Stability*", ], "Power": 32, "ROF": 257, "Range": 64, "Rarity": "Legendary", "Recoil": 90, "Reload": 21, "Season": 27, "Shield Duration": 0, "Source": "kepler", "Stability": 40, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Hand Cannon", "Velocity": 0, "Year": 8, "Zoom": 14, }, { "AA": 70, "Accuracy": 0, "Airborne Effectiveness": 0, "Ammo": "primary", "Ammo Generation": 47, "Archetype": "Rapid-Fire Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": "veist", "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 50, "Hash": 2351747818, "Holofoil": false, "Id": ""6917530144945746209"", "Impact": 18, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 46, "Masterwork Tier": undefined, "Masterwork Type": undefined, "Name": "Sand Wasp-3au", "New Gear": false, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Rapid-Fire Frame*", "Arrowhead Brake*", "Polygonal Rifling", "High-Caliber Rounds*", "Dynamic Sway Reduction*", ], "Power": 32, "ROF": 720, "Range": 33, "Rarity": "Rare", "Recoil": 81, "Reload": 45, "Season": 4, "Shield Duration": 0, "Source": "campaign", "Stability": 48, "Swing Speed": 0, "Tag": undefined, "Tier": 0, "Type": "Auto Rifle", "Velocity": 0, "Year": 2, "Zoom": 16, }, { "AA": 65, "Accuracy": 0, "Airborne Effectiveness": 22, "Ammo": "primary", "Ammo Generation": 57, "Archetype": "Adaptive Frame", "Blast Radius": 0, "Category": "KineticSlot", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Strand", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 37, "Hash": 3813721211, "Holofoil": false, "Id": ""6917530144945747920"", "Impact": 29, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 39, "Masterwork Tier": 2, "Masterwork Type": "Reload Speed", "Name": "Last Thursday", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "Adaptive Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Appended Mag*", "High-Caliber Rounds", "Built to Blast*", "Elemental Honing*", "Exhaustive Research*", "Kill Tracker", "Tier 2: Reload Speed*", ], "Power": 41, "ROF": 390, "Range": 44, "Rarity": "Legendary", "Recoil": 70, "Reload": 40, "Season": 27, "Shield Duration": 0, "Source": "kepler", "Stability": 62, "Swing Speed": 0, "Tag": undefined, "Tier": 1, "Type": "Pulse Rifle", "Velocity": 0, "Year": 8, "Zoom": 17, }, { "AA": 38, "Accuracy": 0, "Airborne Effectiveness": 19, "Ammo": "primary", "Ammo Generation": 54, "Archetype": "High-Impact Frame", "Blast Radius": 0, "Category": "Energy", "Charge Rate": 0, "Charge Time": 0, "Crafted": false, "Crafted Level": 0, "Draw Time": 0, "Element": "Arc", "Equipped": false, "Event": "", "Foundry": undefined, "Guard Endurance": 0, "Guard Resistance": 0, "Handling": 22, "Hash": 1674692344, "Holofoil": false, "Id": ""6917530144947809337"", "Impact": 67, "Kill Tracker": 0, "Loadouts": "", "Locked": false, "Mag": 13, "Masterwork Tier": 1, "Masterwork Type": "Range", "Name": "Sublimation", "New Gear": true, "Notes": undefined, "Owner": "Hunter(11)", "Perks": [ "High-Impact Frame*", "Chambered Compensator*", "Corkscrew Rifling", "Tactical Mag*", "Flared Magwell", "Eddy Current*", "Redirection*", "Exhaustive Research*", "Kill Tracker", "Tier 1: Range*", ], "Power": 41, "ROF": 150, "Range": 73, "Rarity": "Legendary", "Recoil": 82, "Reload": 37, "Season": 27, "Shield Duration": 0, "Source": "kepler", "Stability": 41, "Swing Speed": 0, "Tag": undefined, "Tier": 2, "Type": "Scout Rifle", "Velocity": 0, "Year": 8, "Zoom": 21, }, ] `; ================================================ FILE: src/app/inventory/actions.ts ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import { currentAccountSelector } from 'app/accounts/selectors'; import { apiPermissionGrantedSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { showNotification } from 'app/notifications/notifications'; import { get } from 'app/storage/idb-keyval'; import { ThunkResult } from 'app/store/types'; import { infoLog, warnLog } from 'app/utils/log'; import { DestinyColor, DestinyItemChangeResponse, DestinyProfileResponse, } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { createAction } from 'typesafe-actions'; import { TagCommand, TagValue } from './dim-item-info'; import { DimItem } from './item-types'; import { appendedToNote, removedFromNote } from './note-hashtags'; import { notesSelector } from './selectors'; import { AccountCurrency, DimCharacterStat, DimStore } from './store-types'; import { ItemCreationContext } from './store/d2-item-factory'; /** * Update the computed/massaged state of inventory, plus account-wide info like currencies. */ export const update = createAction('inventory/UPDATE')<{ stores: DimStore[]; currencies: AccountCurrency[]; responseMintedTimestamp?: string; }>(); /** * Remove the loaded stores to force them to be recomputed on the next load (used when changing language). */ export const clearStores = createAction('inventory/CLEAR_STORES')(); export const profileLoaded = createAction('inventory/PROFILE_LOADED')<{ profile: DestinyProfileResponse; live: boolean; }>(); export const profileError = createAction('inventory/PROFILE_ERROR')(); export interface CharacterInfo { characterId: string; level: number; powerLevel: number; background: string; icon: string; stats: { [hash: number]: DimCharacterStat; }; percentToNextLevel?: number; color?: DestinyColor; } /** * Update just the stats of the characters, no inventory. */ export const charactersUpdated = createAction('inventory/CHARACTERS')(); /** * An error that occurred during building the stores */ export const error = createAction('inventory/ERROR')(); /** * An item has moved (or equipped/dequipped) */ export const itemMoved = createAction('inventory/MOVE_ITEM')<{ itemHash: number; itemId: string; itemLocation: BucketHashes; sourceId: string; targetId: string; equip: boolean; amount: number; }>(); /** * An item was mutated by Advanced Write Actions (perks changed, sockets inserted, etc.). * We need to update the inventory with the updated item and any removed/added items. */ export const awaItemChanged = createAction('inventory/AWA_CHANGE')<{ item: DimItem | undefined; changes: DestinyItemChangeResponse; itemCreationContext: ItemCreationContext; }>(); /* * An item has been locked or unlocked (or tracked/untracked) */ export const itemLockStateChanged = createAction('inventory/ITEM_LOCK')<{ item: DimItem; state: boolean; type: 'lock' | 'track'; }>(); /** Update the set of new items. */ export const setNewItems = createAction('new_items/SET')>(); /** Clear new-ness of an item by its instance ID */ export const clearNewItem = createAction('new_items/CLEAR_NEW')(); /** Clear new-ness of all items */ export const clearAllNewItems = createAction('new_items/CLEAR_ALL')(); /** Load which items are new from IndexedDB */ export function loadNewItems(account: DestinyAccount): ThunkResult { return async (dispatch, getState) => { if (getState().inventory.newItemsLoaded) { return; } const key = `newItems-m${account.membershipId}-d${account.destinyVersion}`; const newItems = await get | undefined>(key); if (newItems) { // If we switched account since starting this, give up if (account !== currentAccountSelector(getState())) { return; } dispatch(setNewItems(newItems)); } }; } export const setItemTag = createAction('tag_notes/SET_TAG')<{ /** Item instance ID */ itemId: string; tag?: TagValue; craftedDate?: number; }>(); export const setItemTagsBulk = createAction('tag_notes/SET_TAG_BULK')< { /** Item instance ID */ itemId: string; tag?: TagValue; craftedDate?: number; }[] >(); export const setItemNote = createAction('tag_notes/SET_NOTE')<{ /** Item instance ID */ itemId: string; note?: string; craftedDate?: number; }>(); /** * Tag an item by hash (for uninstanced items like shaders) */ export const setItemHashTag = createAction('tag_notes/SET_HASH_TAG')<{ itemHash: number; tag?: TagValue; }>(); export const setItemHashNote = createAction('tag_notes/SET_HASH_NOTE')<{ itemHash: number; note?: string; }>(); /** * Set the tag for an item regardless of whether it's instanced or not. Prefer this to setItemTag / setItemHashTag. */ export function setTag(item: DimItem, tag: TagCommand | undefined): ThunkResult { return async (dispatch) => { if (!item.taggable) { return; } if ($featureFlags.warnNoSync) { dispatch(warnNoSync()); } dispatch( item.instanced ? setItemTag({ itemId: item.id, tag: tag === 'clear' ? undefined : tag, craftedDate: item.craftedInfo?.craftedDate, }) : setItemHashTag({ itemHash: item.hash, tag: tag === 'clear' ? undefined : tag, }), ); }; } /** * Set the note for an item regardless of whether it's instanced or not. Prefer this to setItemNote / setItemHashNote. */ export function setNote(item: DimItem, note: string | undefined): ThunkResult { return async (dispatch) => { if (!item.taggable) { return; } if ($featureFlags.warnNoSync) { dispatch(warnNoSync()); } dispatch( item.instanced ? setItemNote({ itemId: item.id, note, craftedDate: item.craftedInfo?.craftedDate, }) : setItemHashNote({ itemHash: item.hash, note, }), ); }; } /** * Append a note to the end of the existing notes for an item. */ export function appendNote(item: DimItem, note: string | undefined): ThunkResult { return async (dispatch, getState) => { if (!item.taggable || !note) { return; } const existingNote = notesSelector(item)(getState()); dispatch(setNote(item, appendedToNote(existingNote, note))); }; } /** * Remove the provided text from an item's note. Most useful for deleting tags. */ export function removeFromNote(item: DimItem, note: string | undefined): ThunkResult { return async (dispatch, getState) => { if (!item.taggable || !note) { return; } const existingNote = notesSelector(item)(getState()); dispatch(setNote(item, removedFromNote(existingNote, note))); }; } /** * Warn the first time someone saves a tag or note and they haven't enabled DIM Sync. */ function warnNoSync(): ThunkResult { return async (_dispatch, getState) => { if ( !apiPermissionGrantedSelector(getState()) && localStorage.getItem('warned-no-sync') !== 'true' ) { if ('storage' in navigator && 'persist' in navigator.storage) { const isPersisted = await navigator.storage.persist(); if (isPersisted) { infoLog('storage', 'Persisted storage granted'); } else { warnLog('storage', 'Persisted storage not granted'); } } localStorage.setItem('warned-no-sync', 'true'); showNotification({ type: 'warning', title: t('Storage.DataIsLocal'), body: t('Storage.DimSyncNotEnabled'), duration: 60_000, }); } }; } /** Clear out tags and notes for items that no longer exist. Argument is the list of inventory item IDs to remove. */ export const tagCleanup = createAction('tag_notes/CLEANUP')(); /** input a mock profile API response */ export const setMockProfileResponse = createAction('inventory/MOCK_PROFILE')(); ================================================ FILE: src/app/inventory/advanced-write-actions.ts ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { unlockedItemsForCharacterOrProfilePlugSet } from 'app/records/plugset-helpers'; import { DEFAULT_ORNAMENTS, DEFAULT_SHADER } from 'app/search/d2-known-values'; import { get, set } from 'app/storage/idb-keyval'; import { ThunkResult } from 'app/store/types'; import { DimError } from 'app/utils/dim-error'; import { Destiny2CoreSettings } from 'bungie-api-ts/core'; import { AwaAuthorizationResult, AwaType, AwaUserSelection, DestinyInventoryItemDefinition, DestinyItemChangeResponse, DestinyProfileResponse, DestinySocketArrayType, insertSocketPlug, insertSocketPlugFree, } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import { DestinyAccount } from '../accounts/destiny-account'; import { authenticatedHttpClient } from '../bungie-api/bungie-service-helper'; import { requestAdvancedWriteActionToken } from '../bungie-api/destiny2-api'; import { showNotification } from '../notifications/notifications'; import { awaItemChanged } from './actions'; import { DimItem, DimSocket } from './item-types'; import { createItemContextSelector, currentStoreSelector, profileResponseSelector, storesSelector, } from './selectors'; import { makeItemSingle } from './store/d2-item-factory'; let awaCache: { [key: number]: AwaAuthorizationResult & { used: number }; }; export function canInsertPlug( socket: DimSocket, plugItemHash: number, destiny2CoreSettings: Destiny2CoreSettings | undefined, defs: D2ManifestDefinitions, ) { return $featureFlags.awa || canInsertForFree(socket, plugItemHash, destiny2CoreSettings, defs); } function hasInsertionCost(defs: D2ManifestDefinitions, plug: DestinyInventoryItemDefinition) { if (plug.plug?.insertionMaterialRequirementHash) { const requirements = defs.MaterialRequirementSet.get( plug.plug?.insertionMaterialRequirementHash, ); // There are some items that explicitly point to a definition that says it costs 0 glimmer: return requirements.materials.some((m) => m.count !== 0); } return false; } function canInsertForFree( socket: DimSocket, plugItemHash: number, destiny2CoreSettings: Destiny2CoreSettings | undefined, defs: D2ManifestDefinitions, ) { const pluggedDef = (socket.actuallyPlugged || socket.plugged)?.plugDef; if ( (pluggedDef && (destiny2CoreSettings?.insertPlugFreeProtectedPlugItemHashes || []).includes( pluggedDef.hash, )) || (destiny2CoreSettings?.insertPlugFreeBlockedSocketTypeHashes || []).includes( socket.socketDefinition.socketTypeHash, ) ) { return false; } const plug = defs.InventoryItem.get(plugItemHash); const free = // Must be reusable or randomized type Boolean( socket.socketDefinition.reusablePlugItems.length > 0 || socket.socketDefinition.reusablePlugSetHash || socket.socketDefinition.randomizedPlugSetHash, ) && // And have no cost to insert !hasInsertionCost(defs, plug) && // And the current plug didn't cost anything (can't replace a non-free mod with a free one) (!pluggedDef || !hasInsertionCost(defs, pluggedDef)); return free; } /** * Check whether the currently contained item is a shader that the user won't * be able to plug back in and should thus be left alone. * * Shaders can be overwritten even if they're not yet unlocked, so * save people's Cobalt Clash shaders here. * https://github.com/Bungie-net/api/issues/1580 */ function checkIrreversiblePlugging( socket: DimSocket, storeId: string, profileResponse?: DestinyProfileResponse, ) { const plugged = socket.actuallyPlugged || socket.plugged; if ( plugged?.plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.Shaders) && plugged.plugDef.hash !== DEFAULT_SHADER && // For some reason the default armor ornament is marked as a shader? !DEFAULT_ORNAMENTS.includes(plugged.plugDef.hash) ) { const plugSetHash = socket.socketDefinition.reusablePlugSetHash; const profileUnlocked = plugSetHash && profileResponse && unlockedItemsForCharacterOrProfilePlugSet(profileResponse, plugSetHash, storeId).has( plugged.plugDef.hash, ); const itemUnlocked = socket.reusablePlugItems?.some( (p) => p.enabled && p.plugItemHash === plugged.plugDef.hash, ); if (!profileUnlocked && !itemUnlocked) { return { protected: true, plug: plugged }; } } return { protected: false }; } /** * Modify an item to insert a new plug into one of its socket. */ export function insertPlug(item: DimItem, socket: DimSocket, plugItemHash: number): ThunkResult { return async (dispatch, getState) => { const account = currentAccountSelector(getState())!; const defs = d2ManifestSelector(getState())!; const coreSettings = getState().manifest.destiny2CoreSettings; // This is a special case for transmog ornaments - you can't apply a // transmog ornament to the same item it was created with. So instead we // swap at the last minute to applying the default ornament which should // match the appearance that the user wanted. if (plugItemHash === item.hash) { const defaultPlugHash = socket.emptyPlugItemHash; plugItemHash = defaultPlugHash ?? plugItemHash; } const free = canInsertForFree(socket, plugItemHash, coreSettings, defs); // TODO: if applying to the vault, choose a character that has the mod unlocked rather than current store // look at all plugsets on all characters to find one that's unlocked? // memoize that index probably // TODO: if applying a mod with a seasonal variant, use the seasonal variant instead (may need d2ai power) // The API requires either the ID of the character that owns the item, or // the current character ID if the item is in the vault. const storeId = item.owner === 'vault' ? currentStoreSelector(getState())!.id : item.owner; const irreversiblePlugCheck = checkIrreversiblePlugging( socket, storeId, profileResponseSelector(getState()), ); if (irreversiblePlugCheck.protected && irreversiblePlugCheck.plug) { throw new DimError( 'AWA.IrreversiblePlugging', t('AWA.IrreversiblePlugging', { plug: irreversiblePlugCheck.plug.plugDef.displayProperties.name, }), ); } const insertFn = free ? awaInsertSocketPlugFree : awaInsertSocketPlug; const response = await insertFn(account, storeId, item, socket, plugItemHash); // Update items that changed await dispatch(refreshItemAfterAWA(response.Response)); }; } /** basically just the DIM version of api-ts' `insertSocketPlugFree` */ async function awaInsertSocketPlugFree( account: DestinyAccount, storeId: string, item: DimItem, socket: DimSocket, plugItemHash: number, ) { return insertSocketPlugFree(authenticatedHttpClient, { itemId: item.id, plug: { socketIndex: socket.socketIndex, socketArrayType: DestinySocketArrayType.Default, plugItemHash, }, characterId: storeId, membershipType: account.originalPlatformType, }); } /** * DIM's wrapper around insertSocketPlug, the actual paid * insertion endpoint that gets user push approval */ async function awaInsertSocketPlug( account: DestinyAccount, storeId: string, item: DimItem, socket: DimSocket, plugItemHash: number, ) { if (!$featureFlags.awa) { throw new Error('AWA.NotSupported'); } const actionToken = await getAwaToken(account, AwaType.InsertPlugs, storeId, item); // TODO: if the plug costs resources to insert, add a confirmation. This // would be a great place for a dialog component? return insertSocketPlug(authenticatedHttpClient, { actionToken, itemInstanceId: item.id, plug: { socketIndex: socket.socketIndex, socketArrayType: DestinySocketArrayType.Default, plugItemHash, }, characterId: storeId, membershipType: account.originalPlatformType, }); } /** * Update our view of the item based on the new item state Bungie returned. */ function refreshItemAfterAWA(changes: DestinyItemChangeResponse): ThunkResult { return async (dispatch, getState) => { const itemCreationContext = createItemContextSelector(getState()); const stores = storesSelector(getState()); const newItem = makeItemSingle(itemCreationContext, changes.item, stores); dispatch( awaItemChanged({ item: newItem, changes, itemCreationContext: itemCreationContext, }), ); }; } /** * Given a request for an action token, and a particular action type, return either * a cached token or fetch and return a new one. * * Note: Error/success messaging must be handled by callers, but this will pop up a prompt to go to the app and grant permissions. * * @param item The item is optional unless the type is DismantleGroupA, but it's best to pass it when possible. */ async function getAwaToken( account: DestinyAccount, action: AwaType, storeId: string, item?: DimItem, ): Promise { if (!awaCache) { // load from cache first time // TODO: maybe put this in Redux! awaCache = (await get('awa-tokens')) || {}; } let info = awaCache[action]; if (!info || !tokenValid(info)) { try { // Note: Error messages should be handled by other components. This is just to tell them to check the app. showNotification({ type: 'info', title: t('AWA.ConfirmTitle'), body: t('AWA.ConfirmDescription'), }); // TODO: Do we need to cache a token per item? info = awaCache[action] = { ...(await requestAdvancedWriteActionToken(account, action, storeId, item)), used: 0, }; // Deletes of "group A" require an item and shouldn't be cached // TODO: This got removed from the API /* if (action === AwaType.DismantleGroupA) { delete awaCache[action]; // don't cache } */ } catch (e) { throw new DimError('AWA.FailedToken').withError(e); // TODO: handle userSelection, responseReason (TimedOut, Replaced) } if (!info || !tokenValid(info)) { throw new DimError('AWA.FailedToken', info ? info.developerNote : 'no response'); } } info.used++; // TODO: really should use a separate db for this await set('awa-tokens', awaCache); return info.actionToken; } function tokenValid(info: AwaAuthorizationResult & { used: number }) { return ( (!info.validUntil || new Date(info.validUntil) > new Date()) && (info.maximumNumberOfUses === 0 || info.used <= info.maximumNumberOfUses) && info.userSelection === AwaUserSelection.Approved ); } ================================================ FILE: src/app/inventory/bulk-actions.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import NotificationButton from 'app/notifications/NotificationButton'; import { showNotification } from 'app/notifications/notifications'; import { AppIcon, undoIcon } from 'app/shell/icons'; import { ThunkResult } from 'app/store/types'; import { errorMessage } from 'app/utils/errors'; import { partition } from 'es-toolkit'; import { canSyncLockState } from './SyncTagLock'; import { setItemHashTag, setItemTagsBulk } from './actions'; import { TagCommand, TagValue, tagConfig } from './dim-item-info'; import { setItemLockState } from './item-move-service'; import { DimItem } from './item-types'; import { getTagSelector, tagSelector } from './selectors'; /** * Bulk tag items, with an undo button in a notification. */ export function bulkTagItems( itemsToBeTagged: DimItem[], selectedTag: TagCommand, notification = true, ): ThunkResult { return async (dispatch, getState) => { const getTag = getTagSelector(getState()); // existing tags are later passed to buttonEffect so the notification button knows what to revert const previousState = new Map(); for (const item of itemsToBeTagged) { previousState.set(item, getTag(item)); } const [instanced, nonInstanced] = partition(itemsToBeTagged, (i) => i.instanced); if (instanced.length) { dispatch( setItemTagsBulk( instanced.map((item) => ({ itemId: item.id, tag: selectedTag === 'clear' ? undefined : selectedTag, craftedDate: item.craftedInfo?.craftedDate, })), ), ); } for (const item of nonInstanced) { dispatch( setItemHashTag({ itemHash: item.hash, tag: selectedTag === 'clear' ? undefined : selectedTag, }), ); } if (notification) { showNotification({ type: 'success', duration: 30000, title: t('Header.BulkTag'), body: ( <> {selectedTag === 'clear' ? t('Filter.BulkClear', { count: itemsToBeTagged.length, }) : t('Filter.BulkTag', { count: itemsToBeTagged.length, tag: t(tagConfig[selectedTag].label), })} { if (instanced.length) { dispatch( setItemTagsBulk( instanced.map((item) => ({ itemId: item.id, tag: previousState.get(item), craftedDate: item.craftedInfo?.craftedDate, })), ), ); } if (nonInstanced.length) { for (const item of nonInstanced) { dispatch( setItemHashTag({ itemHash: item.hash, tag: previousState.get(item), }), ); } } showNotification({ type: 'success', title: t('Header.BulkTag'), body: t('Filter.BulkRevert', { count: itemsToBeTagged.length }), }); }} > {t('Filter.Undo')} ), }); } }; } /** * Bulk lock/unlock items */ export function bulkLockItems(items: DimItem[], locked: boolean): ThunkResult { return async (dispatch, getState) => { // Filter out items that can't be locked items = items.filter((item) => item.lockable); // Don't change lock state for items that are having their lock state synced to their tag const autoLockTagged = settingSelector('autoLockTagged')(getState()); items = autoLockTagged ? items.filter((item) => !tagSelector(item)(getState()) || !canSyncLockState(item)) : items; try { for (const item of items) { await dispatch(setItemLockState(item, locked)); } showNotification({ type: 'success', title: locked ? t('Filter.LockAllSuccess', { num: items.length }) : t('Filter.UnlockAllSuccess', { num: items.length }), }); } catch (e) { showNotification({ type: 'error', title: locked ? t('Filter.LockAllFailed') : t('Filter.UnlockAllFailed'), body: errorMessage(e), }); } }; } ================================================ FILE: src/app/inventory/cross-tab.ts ================================================ import { infoLog } from 'app/utils/log'; import { BucketHashes } from 'data/d2/generated-enums'; import { useEffect } from 'react'; export const crossTabChannel = 'BroadcastChannel' in globalThis ? new BroadcastChannel('dim') : undefined; export interface StoreUpdatedMessage { type: 'stores-updated'; } export interface ItemMovedMessage { type: 'item-moved'; itemHash: number; itemId: string; itemLocation: BucketHashes; sourceId: string; targetId: string; equip: boolean; amount: number; } // TODO: other inventory changes, dim api changes, etc. export type CrossTabMessage = StoreUpdatedMessage | ItemMovedMessage; export function useCrossTabUpdates(callback: (m: CrossTabMessage) => void) { useEffect(() => { if (!crossTabChannel) { return; } const onMsg = (m: MessageEvent) => { const message = m.data; infoLog('cross-tab', 'message', message.type, message); if (message.type) { callback(message); } }; crossTabChannel.addEventListener('message', onMsg); return () => crossTabChannel.removeEventListener('message', onMsg); }, [callback]); } export function notifyOtherTabsStoreUpdated() { if (!crossTabChannel) { return; } crossTabChannel.postMessage({ type: 'stores-updated' } satisfies StoreUpdatedMessage); } export function notifyOtherTabsItemMoved(args: Omit) { if (!crossTabChannel) { return; } crossTabChannel.postMessage({ type: 'item-moved', ...args } satisfies ItemMovedMessage); } ================================================ FILE: src/app/inventory/d1-stores.ts ================================================ import { handleAuthErrors } from 'app/accounts/actions'; import { getPlatforms } from 'app/accounts/platforms'; import { currentAccountSelector } from 'app/accounts/selectors'; import { D1CharacterData, D1Inventory, D1ItemComponent, D1VaultInventory, } from 'app/destiny1/d1-manifest-types'; import { ThunkResult } from 'app/store/types'; import { convertToError, errorMessage } from 'app/utils/errors'; import { errorLog, infoLog } from 'app/utils/log'; import { DestinyDisplayPropertiesDefinition } from 'bungie-api-ts/destiny2'; import { getStores } from '../bungie-api/destiny1-api'; import { bungieErrorToaster } from '../bungie-api/error-toaster'; import { D1ManifestDefinitions, getDefinitions } from '../destiny1/d1-definitions'; import { showNotification } from '../notifications/notifications'; import { loadingTracker } from '../shell/loading-tracker'; import { reportException } from '../utils/sentry'; import { error, loadNewItems, update } from './actions'; import { cleanInfos } from './dim-item-info'; import { InventoryBuckets } from './inventory-buckets'; import { d1BucketsSelector, storesLoadedSelector } from './selectors'; import { D1Store } from './store-types'; import { processItems } from './store/d1-item-factory'; import { makeCharacter, makeVault } from './store/d1-store-factory'; import { resetItemIndexGenerator } from './store/item-index'; /** * Returns a promise for a fresh view of the stores and their items. */ // TODO: combine with d2 stores action! export function loadStores(): ThunkResult { return async (dispatch, getState) => { const promise = (async () => { let account = currentAccountSelector(getState()); if (!account) { await dispatch(getPlatforms); account = currentAccountSelector(getState()); if (account?.destinyVersion !== 1) { return; } } try { resetItemIndexGenerator(); const [defs, , { characters, profileInventory, vaultInventory }] = await Promise.all([ dispatch(getDefinitions()), dispatch(loadNewItems(account)), getStores(account), ]); const lastPlayedDate = findLastPlayedDate(characters); const buckets = d1BucketsSelector(getState())!; const stores = [ ...characters.map((characterData) => processCharacter(characterData, defs, buckets, lastPlayedDate), ), processVault(vaultInventory, defs, buckets), ]; const currencies = processCurrencies(profileInventory, defs); // If we switched account since starting this, give up if (account !== currentAccountSelector(getState())) { return; } dispatch(cleanInfos(stores)); dispatch(update({ stores, currencies })); return stores; } catch (e) { errorLog('d1-stores', 'Error loading stores', e); reportException('D1StoresService', e); // If we switched account since starting this, give up if (account !== currentAccountSelector(getState())) { return; } dispatch(handleAuthErrors(e)); if (storesLoadedSelector(getState())) { // don't replace their inventory with the error, just notify showNotification(bungieErrorToaster(errorMessage(e))); } else { dispatch(error(convertToError(e))); } // It's important that we swallow all errors here - otherwise // our observable will fail on the first error. We could work // around that with some rxjs operators, but it's easier to // just make this never fail. return undefined; } })(); loadingTracker.addPromise(promise); return promise; }; } function processCurrencies(profileInventory: D1Inventory, defs: D1ManifestDefinitions) { try { return profileInventory.currencies.map((c) => { const itemDef = defs.InventoryItem.get(c.itemHash); return { itemHash: c.itemHash, quantity: c.value, displayProperties: { name: itemDef.itemName, description: itemDef.itemDescription, icon: itemDef.icon, hasIcon: Boolean(itemDef.icon), } as DestinyDisplayPropertiesDefinition, }; }); } catch (e) { infoLog('d1-stores', 'error processing currencies', e); } return []; } /** * Process a single store from its raw form to a DIM store, with all the items. */ function processCharacter( characterData: D1CharacterData, defs: D1ManifestDefinitions, buckets: InventoryBuckets, lastPlayedDate: Date, ) { const store = makeCharacter(characterData, defs, lastPlayedDate); let items: D1ItemComponent[] = []; for (const buckets of Object.values(characterData.inventory.buckets)) { for (const bucket of buckets) { for (const item of bucket.items) { item.bucket = bucket.bucketHash; } items = items.concat(bucket.items); } } store.items = processItems(store, items, defs, buckets); store.hadErrors = items.length !== store.items.length; return store; } /** * Process a single store from its raw form to a DIM store, with all the items. */ function processVault( vaultInventory: D1VaultInventory, defs: D1ManifestDefinitions, buckets: InventoryBuckets, ) { const store = makeVault(); let items: D1ItemComponent[] = []; for (const bucket of Object.values(vaultInventory.buckets)) { for (const item of bucket.items) { item.bucket = bucket.bucketHash; } items = items.concat(bucket.items); } store.items = processItems(store, items, defs, buckets); store.hadErrors = items.length !== store.items.length; return store; } /** * Find the date of the most recently played character. */ function findLastPlayedDate(characterData: D1CharacterData[]): Date { return characterData.reduce((memo, characterData) => { const d1 = new Date(characterData.character.characterBase.dateLastPlayed); return memo ? (d1 >= memo ? d1 : memo) : d1; }, new Date(0)); } ================================================ FILE: src/app/inventory/d2-stores.test.ts ================================================ import { getWeaponArchetypeSocket } from 'app/utils/socket-utils'; import { BucketHashes } from 'data/d2/generated-enums'; import { getTestStores, setupi18n } from 'testing/test-utils'; import { generateCSVExportData } from './spreadsheets'; import { DimStore } from './store-types'; describe('process stores', () => { let stores: DimStore[]; beforeAll(async () => { stores = await getTestStores(); }); it('can process stores without errors', async () => { expect(stores).toBeDefined(); expect(stores?.length).toBe(4); }); it("all sockets' plugged is present in the list of plugOptions", async () => { for (const store of stores) { for (const item of store.items) { if (item.sockets) { for (const socket of item.sockets.allSockets) { if ( socket.plugged && // the plugged socket must appear in the list of plugOptions !socket.plugOptions.includes(socket.plugged) ) { throw new Error( `"${item.name}" - ${socket.plugged.plugDef.displayProperties.name} is not in the list of plugOptions`, ); } } } } } }); it('intrinsic sockets should have only one plugOption', async () => { for (const store of stores) { for (const item of store.items) { const archetypeSocket = getWeaponArchetypeSocket(item); if (archetypeSocket && archetypeSocket.plugOptions.length > 1) { throw new Error(`"${item.name}" has multiple archetype (intrinsic) plug options`); } } } }); // This was a bug once where I forgot to populate plug options for sparrow // perks because their reusable plugs list is empty even though they have a // plugged plug. // Alpine Dash is broken in-game (https://www.bungie.net/7/en/News/article/destiny_2_update_8_0_0_1 search:"Alpine Dash") it('sparrows should have perks', async () => { for (const store of stores) { for (const item of store.items) { if (item.hash !== 3981634627 && item.bucket.hash === BucketHashes.Vehicle && item.sockets) { for (const socket of item.sockets.allSockets) { if (socket.plugOptions.length === 0) { throw new Error(`Sparrow "${item.name}" is missing perks`); } } } } } }); // Another sanity check that stats are working (I messed this up) it('items with stats should have at least one nonzero stat', async () => { for (const store of stores) { for (const item of store.items) { if ( item.stats && // These naturally have all-zero stats item.bucket.hash !== BucketHashes.ClassArmor && item.bucket.hash !== BucketHashes.Subclass && !item.stats.some((s) => s.base > 0) ) { throw new Error(`"${item.name}" has all zero stats`); } } } }); // This relies on the sample profile having at least one item that has a plug // that can no longer roll. I keep a Commemoration around for that. it('item perks can be marked as cannotCurrentlyRoll', async () => { for (const store of stores) { for (const item of store.items) { if ( item.sockets?.allSockets.some((s) => s.plugOptions.some((p) => p.cannotCurrentlyRoll)) ) { return; // All good, we found one! } } } throw new Error('Expected at least one item with a perk that cannot roll'); }); test.each(['weapon', 'armor', 'ghost'] as const)('generates a correct %s CSV export', (type) => { setupi18n(); const getTag = () => undefined; const getNotes = () => undefined; const loadoutsByItem = {}; const csvExport = generateCSVExportData(type, stores, getTag, getNotes, loadoutsByItem, []); expect(csvExport).toMatchSnapshot(); }); }); ================================================ FILE: src/app/inventory/d2-stores.ts ================================================ import { startSpan } from '@sentry/browser'; import { handleAuthErrors } from 'app/accounts/actions'; import { compareAccounts, DestinyAccount } from 'app/accounts/destiny-account'; import { getPlatforms } from 'app/accounts/platforms'; import { currentAccountSelector } from 'app/accounts/selectors'; import { loadClarity } from 'app/clarity/descriptions/loadDescriptions'; import { customStatsSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { inGameLoadoutLoaded } from 'app/loadout/ingame/actions'; import { processInGameLoadouts } from 'app/loadout/loadout-type-converters'; import { loadCoreSettings } from 'app/manifest/actions'; import { checkForNewManifest } from 'app/manifest/manifest-service-json'; import { d2ManifestSelector, manifestSelector } from 'app/manifest/selectors'; import { loadingTracker } from 'app/shell/loading-tracker'; import { get, set } from 'app/storage/idb-keyval'; import { ThunkResult } from 'app/store/types'; import { DimError } from 'app/utils/dim-error'; import { convertToError, errorMessage } from 'app/utils/errors'; import { errorLog, infoLog, timer, warnLog } from 'app/utils/log'; import { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import { getCharacters as d1GetCharacters } from '../bungie-api/destiny1-api'; import { getCharacters, getStores } from '../bungie-api/destiny2-api'; import { bungieErrorToaster } from '../bungie-api/error-toaster'; import { D2ManifestDefinitions, getDefinitions } from '../destiny2/d2-definitions'; import { bungieNetPath } from '../dim-ui/BungieImage'; import { showNotification } from '../notifications/notifications'; import { reportException } from '../utils/sentry'; import { CharacterInfo, charactersUpdated, error, loadNewItems, profileError, profileLoaded, update, } from './actions'; import { notifyOtherTabsStoreUpdated } from './cross-tab'; import { cleanInfos } from './dim-item-info'; import { d2BucketsSelector, storesLoadedSelector } from './selectors'; import { DimStore } from './store-types'; import { getCharacterStatsData as getD1CharacterStatsData } from './store/character-utils'; import { buildStores, getCharacterStatsData } from './store/d2-store-factory'; import { resetItemIndexGenerator } from './store/item-index'; import { getCurrentStore } from './stores-helpers'; const TAG = 'd2-stores'; /** * Update the high level character information for all the stores * (level, power, stats, etc.). This does not update the * items in the stores. * * This works on both D1 and D2. * * TODO: Instead of this, update per-character after equip/dequip */ export function updateCharacters(): ThunkResult { return async (dispatch, getState) => { const account = currentAccountSelector(getState()); if (!account) { return; } const defs = manifestSelector(getState()); if (!defs) { return; } let characters: CharacterInfo[]; if (account.destinyVersion === 2) { const profileInfo = await getCharacters(account); characters = profileInfo.characters.data ? Object.values(profileInfo.characters.data).map((character) => ({ characterId: character.characterId, level: character.levelProgression.level, powerLevel: character.light, background: bungieNetPath(character.emblemBackgroundPath), icon: bungieNetPath(character.emblemPath), stats: getCharacterStatsData(d2ManifestSelector(getState())!, character.stats), color: character.emblemColor, })) : []; } else { const profileInfo = await d1GetCharacters(account); characters = profileInfo.characters.map((character) => { const characterBase = character.characterBase; return { characterId: characterBase.characterId, level: character.characterLevel, powerLevel: characterBase.powerLevel, percentToNextLevel: character.percentToNextLevel / 100, background: bungieNetPath(character.backgroundPath), icon: bungieNetPath(character.emblemPath), stats: getD1CharacterStatsData(getState().manifest.d1Manifest!, characterBase), }; }); } // If we switched account since starting this, give up if (account !== currentAccountSelector(getState())) { return; } dispatch(charactersUpdated(characters)); }; } let firstTime = true; let loading = false; /** * Returns a promise for a fresh view of the stores and their items. */ export function loadStores({ fromOtherTab = false, }: { fromOtherTab?: boolean; } = {}): ThunkResult { return async (dispatch, getState) => { try { let stores: DimStore[] | undefined; if (loading) { infoLog(TAG, 'Already loading stores, skipping this load'); return; } await navigator.locks.request( 'loadStores', { // If another tab is working on it, don't wait. The callback will get a null lock. ifAvailable: true, mode: 'exclusive', }, async (lock) => { if (!lock && !firstTime) { infoLog('cross-tab', 'Another tab is already loading stores'); // This means another tab was already requesting the stores. throw new Error('lock-held'); } loading = true; try { let account = currentAccountSelector(getState()); if (!account) { // TODO: throw here? await dispatch(getPlatforms); account = currentAccountSelector(getState()); if (account?.destinyVersion !== 2) { return; } } dispatch(loadCoreSettings()); // no need to wait $featureFlags.clarityDescriptions && dispatch(loadClarity()); // no need to await await dispatch(loadNewItems(account)); // The first time we load, allow the data to be loaded from IDB. We then do a second // load to make sure that we immediately try to get remote data. if (firstTime) { infoLog(TAG, 'First time loading stores, only loading from IDB (if available)'); await dispatch(loadStoresData(account, { firstTime, fromOtherTab })); firstTime = false; if (getState().inventory.live) { infoLog(TAG, 'Initial load got live data, skipping fast-follow load'); return; } else { infoLog(TAG, 'Fast-follow load live stores from Bungie.net'); } } // The account can be mutated by the first load (lastPlayedDate) account = currentAccountSelector(getState()); if (!account) { errorLog(TAG, 'No account after loading stores'); return; } stores = await dispatch(loadStoresData(account, { firstTime: false, fromOtherTab })); } finally { loading = false; } }, ); // Need to do this after the lock has been released if (!firstTime && stores !== undefined && !fromOtherTab) { notifyOtherTabsStoreUpdated(); } return stores; } catch (e) { if (!(e instanceof Error) || e.message !== 'lock-held') { throw e; } } }; } /** How old the profile can be and still trigger cleanup of tags. */ const FRESH_ENOUGH_TO_CLEAN_INFOS = 90_000; // 90 seconds function loadProfile( account: DestinyAccount, { firstTime, fromOtherTab, }: { firstTime: boolean; fromOtherTab: boolean; }, ): ThunkResult< | { profile: DestinyProfileResponse; /** Whether the data is from a "live", remote Bungie.net response. false if this is cached data. */ live: boolean; readOnly?: boolean; } | undefined > { return async (dispatch, getState) => { const mockProfileData = getState().inventory.mockProfileData; if (mockProfileData) { return { profile: mockProfileData, live: false, readOnly: true }; } const cachedProfileKey = `profile-${account.membershipId}`; // First try loading from IndexedDB let cachedProfileResponse = getState().inventory.profileResponse; // TODO: always check IDB, in case another tab loaded it? if (!cachedProfileResponse || fromOtherTab) { try { cachedProfileResponse = await get(cachedProfileKey); // Check to make sure the profile hadn't been loaded in the meantime if (!fromOtherTab && getState().inventory.profileResponse) { cachedProfileResponse = getState().inventory.profileResponse; } else if (cachedProfileResponse) { const profileAgeSecs = (Date.now() - new Date(cachedProfileResponse.responseMintedTimestamp ?? 0).getTime()) / 1000; if (fromOtherTab) { infoLog( TAG, `Loaded cached profile from IndexedDB because another tab updated it. It is ${profileAgeSecs}s old.`, ); } else { infoLog( TAG, `Loaded cached profile from IndexedDB, using it until new data is available. It is ${profileAgeSecs}s old.`, ); } dispatch(profileLoaded({ profile: cachedProfileResponse, live: fromOtherTab })); // The first time we load, just use the IDB version if we can, to speed up loading if (firstTime) { return { profile: cachedProfileResponse, live: false }; } } } catch (e) { errorLog(TAG, 'Failed to load profile response from IDB', e); } } const cachedProfileMintedDate = cachedProfileResponse ? new Date(cachedProfileResponse.responseMintedTimestamp ?? 0) : new Date(0); try { const remoteProfileResponse = await getStores(account); const now = Date.now(); const remoteProfileMintedDate = new Date(remoteProfileResponse.responseMintedTimestamp ?? 0); const remoteProfileAgeSec = (now - remoteProfileMintedDate.getTime()) / 1000; // compare new response against cached response, toss if it's not newer! if (cachedProfileResponse) { const cachedProfileAgeSec = (now - cachedProfileMintedDate.getTime()) / 1000; if (remoteProfileMintedDate.getTime() <= cachedProfileMintedDate.getTime()) { const eq = remoteProfileMintedDate.getTime() === cachedProfileMintedDate.getTime(); const storesLoaded = storesLoadedSelector(getState()); const action = storesLoaded ? 'Skipping update.' : 'Using the cached profile.'; if (eq) { infoLog( TAG, `Profile from Bungie.net is is ${remoteProfileAgeSec}s old, which is the same age as the cached profile.`, action, ); } else { warnLog( TAG, `Profile from Bungie.net is ${remoteProfileAgeSec}s old, while the cached profile is ${cachedProfileAgeSec}s old.`, action, ); } // Clear the error since we did load correctly dispatch(profileError(undefined)); // undefined means skip processing, in case we already have computed stores return storesLoaded ? undefined : { profile: cachedProfileResponse, live: false }; } else { infoLog( TAG, `Profile from Bungie.net is ${remoteProfileAgeSec}s old, while the cached profile is ${cachedProfileAgeSec}s old.`, `Using the new profile from Bungie.net.`, ); } } else { infoLog( TAG, `No cached profile, using profile from Bungie.net which is ${remoteProfileAgeSec}s old.`, ); } await set(cachedProfileKey, remoteProfileResponse); dispatch(profileLoaded({ profile: remoteProfileResponse, live: true })); return { profile: remoteProfileResponse, live: true }; } catch (e) { dispatch(handleAuthErrors(e)); dispatch(profileError(convertToError(e))); if (cachedProfileResponse) { errorLog(TAG, 'Error loading profile from Bungie.net, falling back to cached profile', e); // undefined means skip processing, in case we already have computed stores return storesLoadedSelector(getState()) ? undefined : { profile: cachedProfileResponse, live: false }; } // rethrow throw e; } }; } let lastCheckedManifest = 0; function loadStoresData( account: DestinyAccount, profileArgs: { firstTime: boolean; fromOtherTab: boolean; }, ): ThunkResult { return async (dispatch, getState) => { const promise = (async () => { // If we switched account since starting this, give up if (!compareAccounts(account, currentAccountSelector(getState()))) { infoLog( TAG, 'Switched accounts, giving up on loading stores', 1, account, currentAccountSelector(getState()), ); return; } return startSpan({ name: 'loadStoresD2' }, async () => { resetItemIndexGenerator(); try { const [originalDefs, profileInfo] = await Promise.all([ dispatch(getDefinitions()), dispatch(loadProfile(account, profileArgs)), ]); let defs = originalDefs; // If we switched account since starting this, give up if (!compareAccounts(account, currentAccountSelector(getState()))) { infoLog( TAG, 'Switched accounts, giving up on loading stores', 2, account, currentAccountSelector(getState()), ); return; } for (let i = 0; i < 2; i++) { if (!defs || !profileInfo) { infoLog(TAG, 'No defs or profile info, skipping store load', { defs: Boolean(defs), profileInfo: Boolean(profileInfo), }); return; } const { profile: profileResponse, live, readOnly } = profileInfo; const stopTimer = timer(TAG, 'Process inventory'); const buckets = d2BucketsSelector(getState())!; const customStats = customStatsSelector(getState()); const stores = buildStores({ defs, buckets, customStats, profileResponse, }); // One reason stores could have errors is if the manifest was not up // to date. Check to see if it has updated, and if so, download it and // immediately try again. if ( stores.some((s) => s.hadErrors) && lastCheckedManifest - Date.now() > 5 * 60 * 1000 ) { lastCheckedManifest = Date.now(); if (await checkForNewManifest()) { defs = await dispatch(getDefinitions(true)); continue; // go back to the top of the loop with the new defs } } if (readOnly) { for (const store of stores) { store.hadErrors = true; for (const item of store.items) { item.lockable = false; item.trackable = false; item.notransfer = true; item.taggable = false; } } } const currencies = processCurrencies(profileResponse, defs); const loadouts = processInGameLoadouts(profileResponse, defs); stopTimer(); return startSpan({ name: 'updateInventoryState' }, () => { const stopStateTimer = timer(TAG, 'Inventory state update'); // If we switched account since starting this, give up before saving if (!compareAccounts(account, currentAccountSelector(getState()))) { infoLog( TAG, 'Switched accounts, giving up on loading stores', 3, account, currentAccountSelector(getState()), ); return; } if (!getCurrentStore(stores)) { errorLog(TAG, 'No characters in profile'); dispatch( error( new DimError( 'Accounts.NoCharactersTitle', t('Accounts.NoCharacters'), ).withNoSocials(), ), ); return; } // Cached loads can come from IDB, which can be VERY outdated, so don't // remove item tags/notes based on that. We also refuse to clean tags if // the profile is too old in wall-clock time. Technically we could do // this *only* based on the minted timestamp, but there's no real point // in cleaning items for cached loads since they presumably were cleaned // already. const profileMintedDate = new Date(profileResponse.responseMintedTimestamp ?? 0); if (live && Date.now() - profileMintedDate.getTime() < FRESH_ENOUGH_TO_CLEAN_INFOS) { dispatch(cleanInfos(stores)); } dispatch( update({ stores, currencies, responseMintedTimestamp: profileResponse.responseMintedTimestamp, }), ); dispatch(inGameLoadoutLoaded(loadouts)); stopStateTimer(); return stores; }); } } catch (e) { errorLog(TAG, 'Error loading stores', e); reportException('d2stores', e); // If we switched account since starting this, give up if (!compareAccounts(account, currentAccountSelector(getState()))) { return; } dispatch(handleAuthErrors(e)); if (storesLoadedSelector(getState())) { // don't replace their inventory with the error, just notify showNotification(bungieErrorToaster(errorMessage(e))); } else { dispatch(error(convertToError(e))); } return undefined; } }); })(); loadingTracker.addPromise(promise); return promise; }; } function processCurrencies(profileInfo: DestinyProfileResponse, defs: D2ManifestDefinitions) { const profileCurrencies = profileInfo.profileCurrencies.data ? profileInfo.profileCurrencies.data.items : []; const currencies = profileCurrencies.map((c) => ({ itemHash: c.itemHash, quantity: c.quantity, displayProperties: defs.InventoryItem.get(c.itemHash)?.displayProperties ?? { name: 'Unknown', description: 'Unknown item', }, })); return currencies; } ================================================ FILE: src/app/inventory/dim-item-info.ts ================================================ import { ItemAnnotation, ItemHashTag } from '@destinyitemmanager/dim-api-types'; import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; import { I18nKey, tl } from 'app/i18next-t'; import { ThunkResult } from 'app/store/types'; import { filterMap, isEmpty } from 'app/utils/collections'; import { infoLog, warnLog } from 'app/utils/log'; import { keyBy } from 'es-toolkit'; import { archiveIcon, banIcon, boltIcon, heartIcon, tagIcon } from '../shell/icons'; import { setItemNote, setItemTag, tagCleanup } from './actions'; import { DimItem } from './item-types'; import { itemInfosSelector } from './selectors'; import { DimStore } from './store-types'; // sortOrder: orders items within a bucket, ascending export const tagConfig = { favorite: { type: 'favorite' as const, label: tl('Tags.Favorite'), sortOrder: 0, hotkey: 'shift+1', icon: heartIcon, }, keep: { type: 'keep' as const, label: tl('Tags.Keep'), sortOrder: 1, hotkey: 'shift+2', icon: tagIcon, }, junk: { type: 'junk' as const, label: tl('Tags.Junk'), sortOrder: 2, hotkey: 'shift+3', icon: banIcon, }, infuse: { type: 'infuse' as const, label: tl('Tags.Infuse'), sortOrder: 3, hotkey: 'shift+4', icon: boltIcon, }, archive: { type: 'archive' as const, label: tl('Tags.Archive'), sortOrder: 4, hotkey: 'shift+5', icon: archiveIcon, }, }; export type TagValue = keyof typeof tagConfig; export type TagCommand = TagValue | 'clear'; /** * Priority order for which items should get moved off a character (into the vault or another character) * when the character is full and you want to move something new in. Tag values earlier in this list * are more likely to be moved. */ export const characterDisplacePriority: (TagValue | 'none')[] = [ // Archived items should move to the vault 'archive', // Infusion fuel belongs in the vault 'infuse', 'none', 'junk', 'keep', // Favorites you probably want to keep on your character 'favorite', ]; /** * Priority order for which items should get moved out of the vault (onto a character) * when the vault is full and you want to move something new in. Tag values earlier in this list * are more likely to be moved. */ export const vaultDisplacePriority: (TagValue | 'none')[] = [ // Junk should probably bubble towards the character so you remember to delete them! 'junk', 'none', 'keep', // Favorites you probably want to keep in the vault if you put them there 'favorite', // Infusion fuel belongs in the vault 'infuse', // Archived items should absolutely stay in the vault 'archive', ]; /** * Priority order for which items should get chosen to replace an equipped item. * Tag values earlier in this list are more likely to be chosen. */ export const equipReplacePriority: (TagValue | 'none')[] = [ 'favorite', 'keep', 'none', 'infuse', 'junk', 'archive', ]; export interface ItemInfos { [itemId: string]: ItemAnnotation; } export interface TagInfo { type?: TagValue; label: I18nKey; sortOrder?: number; displacePriority?: number; hotkey?: string; icon?: string | IconDefinition; } // populate tag list from tag config info export const itemTagList: TagInfo[] = Object.values(tagConfig); export const vaultGroupTagOrder = filterMap(itemTagList, (tag) => tag.type); export const itemTagSelectorList: TagInfo[] = [ { label: tl('Tags.TagItem') }, ...Object.values(tagConfig), ]; /** * Delete items from the loaded items that don't appear in newly-loaded stores */ export function cleanInfos(stores: DimStore[]): ThunkResult { return async (dispatch, getState) => { if (!stores.length || stores.some((s) => s.items.length === 0 || s.hadErrors)) { // don't accidentally wipe out notes return; } const infos = itemInfosSelector(getState()); if (isEmpty(infos)) { return; } const infosWithCraftedDate = Object.values(infos).filter((i) => i.craftedDate); const infosByCraftedDate = keyBy(infosWithCraftedDate, (i) => i.craftedDate!); let maxItemId = 0n; // Tags/notes are stored keyed by instance ID. Start with all the keys of the // existing tags and notes and remove the ones that are still here, and the rest // should be cleaned up because they refer to deleted items. const cleanupIds = new Set(Object.keys(infos)); for (const store of stores) { for (const item of store.items) { const itemId = BigInt(item.id); if (itemId > maxItemId) { maxItemId = itemId; } const info = infos[item.id]; if (info && (info.tag !== undefined || info.notes?.length)) { cleanupIds.delete(item.id); } else if (item.craftedInfo?.craftedDate) { // Double-check crafted items - we may have them under a different ID. // If so, patch up the data by re-tagging them under the new ID. // We'll delete the old item's info, but the new infos will be saved. const craftedInfo = infosByCraftedDate[item.craftedInfo.craftedDate]; if (craftedInfo) { if (craftedInfo.tag) { dispatch( setItemTag({ itemId: item.id, tag: craftedInfo.tag, craftedDate: item.craftedInfo.craftedDate, }), ); } if (craftedInfo.notes) { dispatch( setItemNote({ itemId: item.id, note: craftedInfo.notes, craftedDate: item.craftedInfo.craftedDate, }), ); } } } } } if (cleanupIds.size > 0) { const eligibleCleanupIds = Array.from(cleanupIds).filter((id) => BigInt(id) < maxItemId); if (cleanupIds.size > eligibleCleanupIds.length) { warnLog( 'cleanInfos', `${cleanupIds.size - eligibleCleanupIds.length} infos have IDs newer than the newest ID in inventory`, ); } if (eligibleCleanupIds.length > 0) { infoLog('cleanInfos', `Purging tag/notes from ${eligibleCleanupIds.length} deleted items`); dispatch(tagCleanup(eligibleCleanupIds)); } } }; } export function getTag( item: DimItem, itemInfos: ItemInfos, itemHashTags?: { [itemHash: string]: ItemHashTag; }, ): TagValue | undefined { return item.taggable ? (item.instanced ? itemInfos[item.id]?.tag : itemHashTags?.[item.hash]?.tag) || undefined : undefined; } export function getNotes( item: DimItem, itemInfos: ItemInfos, itemHashTags?: { [itemHash: string]: ItemHashTag; }, ): string | undefined { return item.taggable ? (item.instanced ? itemInfos[item.id]?.notes : itemHashTags?.[item.hash]?.notes) || undefined : undefined; } ================================================ FILE: src/app/inventory/drag-events.ts ================================================ import { Observable } from 'app/utils/observable'; export const isDragging$ = new Observable(false); ================================================ FILE: src/app/inventory/inventory-buckets.ts ================================================ import { BucketCategory } from 'bungie-api-ts/destiny2'; /** The major toplevel sections of the inventory. "Progress" is only in D1. */ export type D2BucketCategory = 'Postmaster' | 'Weapons' | 'Armor' | 'General' | 'Inventory'; export type D1BucketCategory = 'Postmaster' | 'Weapons' | 'Armor' | 'General' | 'Progress'; export type BucketSortType = D2BucketCategory | D1BucketCategory | 'Unknown'; export type InventoryBucket = { readonly description: string; readonly name: string; readonly hash: number; readonly equippable: boolean; readonly hasTransferDestination: boolean; readonly capacity: number; readonly accountWide: boolean; readonly category: BucketCategory; readonly sort?: BucketSortType; /** * The corresponding vault bucket where these items would go if they were placed in the vault. */ vaultBucket?: InventoryBucket; } & { // inPostmaster, inArmor, etc [C in BucketSortType as `in${C}`]?: boolean; }; export interface InventoryBuckets { byHash: { [hash: number]: InventoryBucket }; byCategory: { [category: string]: InventoryBucket[] }; unknown: InventoryBucket; // TODO: get rid of this? setHasUnknown: () => void; } ================================================ FILE: src/app/inventory/item-move-service.ts ================================================ import { startSpan } from '@sentry/browser'; import { handleAuthErrors } from 'app/accounts/actions'; import { currentAccountSelector } from 'app/accounts/selectors'; import { t } from 'app/i18next-t'; import { isInInGameLoadoutForSelector } from 'app/loadout/selectors'; import { ItemRarityMap } from 'app/search/d2-known-values'; import { RootState, ThunkResult } from 'app/store/types'; import { CancelToken } from 'app/utils/cancel'; import { count, filterMap } from 'app/utils/collections'; import { DimError } from 'app/utils/dim-error'; import { errorMessage } from 'app/utils/errors'; import { itemCanBeEquippedBy } from 'app/utils/item-utils'; import { errorLog, infoLog, timer, warnLog } from 'app/utils/log'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { PlatformErrorCodes } from 'bungie-api-ts/user'; import { BucketHashes } from 'data/d2/generated-enums'; import { memoize } from 'es-toolkit'; import { Immutable } from 'immer'; import { AnyAction } from 'redux'; import { ThunkAction } from 'redux-thunk'; import { equipItems as d1EquipItems, setItemState as d1SetItemState, transfer as d1Transfer, equip as d1equip, } from '../bungie-api/destiny1-api'; import { equipItems as d2EquipItems, setLockState as d2SetLockState, setTrackedState as d2SetTrackedState, transfer as d2Transfer, equip as d2equip, } from '../bungie-api/destiny2-api'; import { chainComparator, compareBy, compareByIndex, reverseComparator, } from '../utils/comparators'; import { itemLockStateChanged, itemMoved } from './actions'; import { notifyOtherTabsItemMoved } from './cross-tab'; import { TagValue, characterDisplacePriority, equipReplacePriority, vaultDisplacePriority, } from './dim-item-info'; import { DimItem } from './item-types'; import { getLastManuallyMoved } from './manual-moves'; import { currentStoreSelector, getTagSelector, storesSelector } from './selectors'; import { DimStore } from './store-types'; import { amountOfItem, findItemsByBucket, getCurrentStore, getStore, getVault, spaceLeftForItem, } from './stores-helpers'; const TAG = 'move'; /** * An object we can use to track state across a "session" of move operations. * That might be just the moves involved in a single move request (including * move-asides), or it may encompass an entire loadout application. */ export interface MoveSession { /** Keep track of which buckets we tried to blindly move to but were actually full */ bucketsFullOnCurrentStore: Set; /** A token that can be checked to see if the whole operation is canceled. */ readonly cancelToken: CancelToken; /** * Items explicitly involved in the requested move. * Used to distinguish user-intentional moves vs make-space moves. * Contains instanceIds, or for uninstanced items, item hashes. */ involvedItems: Set; // TODO: a record of moves? something to prevent infinite moves loops? } export function createMoveSession( cancelToken: CancelToken, /** Items explicitly involved in the move. */ items: DimItem[], ): MoveSession { const involvedItems = new Set(); for (const item of items) { involvedItems.add(item.instanced ? item.id : item.hash); } return { bucketsFullOnCurrentStore: new Set(), involvedItems, cancelToken, }; } /** * You can reserve a number of spaces in each BucketHash in each store. */ export interface MoveReservations { [storeId: string]: { [type: number]: number; }; } /** * Minimum specification to identify an item that should be excluded from some consideration. */ export interface Exclusion { id: string; hash: number; } /** * Lock/unlock or track/untrack an item. */ export function setItemLockState( item: DimItem, state: boolean, type: 'lock' | 'track' = 'lock', ): ThunkResult { return async (dispatch, getState) => { const account = currentAccountSelector(getState())!; // The state APIs require either the ID of the character that owns the item, or // the current character ID if the item is in the vault. const storeId = item.owner === 'vault' ? currentStoreSelector(getState())!.id : item.owner; if (item.destinyVersion === 2) { if (type === 'lock') { await d2SetLockState(account, storeId, item, state); } else { await d2SetTrackedState(account, storeId, item, state); } } else if (item.destinyVersion === 1) { await d1SetItemState(account, item, storeId, state, type); } dispatch(itemLockStateChanged({ item, state, type })); }; } function equipApi(item: DimItem): typeof d2equip { return item.destinyVersion === 2 ? d2equip : d1equip; } function equipItemsApi(item: DimItem): typeof d2EquipItems { return item.destinyVersion === 2 ? d2EquipItems : d1EquipItems; } function transferApi(item: DimItem): typeof d2Transfer { return item.destinyVersion === 2 ? d2Transfer : d1Transfer; } /** * Update our item and store models after an item has been moved (or equipped/dequipped). * @return the new or updated item (it may create a new item!) */ function updateItemModel( item: DimItem, source: DimStore, target: DimStore, equip: boolean, amount: number = item.amount, ): ThunkAction { return (dispatch, getState) => startSpan({ name: 'updateItemModel' }, () => { const stopTimer = timer(TAG, 'itemMovedUpdate'); const args = { itemId: item.id, itemHash: item.hash, itemLocation: item.location.hash, sourceId: source.id, targetId: target.id, equip, amount, }; try { dispatch(itemMoved(args)); notifyOtherTabsItemMoved(args); const stores = storesSelector(getState()); return getItemAcrossStores(stores, item) || item; } finally { stopTimer(); } }); } /** * Find an item among all stores that matches the params provided. */ function getItemAcrossStores>( stores: Store[], params: DimItem, ) { for (const store of stores) { for (const item of store.items) { if ( params.id === item.id && params.hash === item.hash && params.notransfer === item.notransfer && params.amount === item.amount ) { return item; } } } return undefined; } /** * Finds an item similar to "item" which can be equipped on the item's owner in order to move "item". */ export function getSimilarItem( getState: () => RootState, stores: readonly DimStore[], item: DimItem, { exclusions, excludeExotic = false, }: { exclusions?: readonly Exclusion[]; /** Don't pick an exotic to equip in this item's place (because we're specifically trying to dequip an exotic) */ excludeExotic?: boolean; } = {}, ): DimItem | undefined { const target = getStore(stores, item.owner)!; // Try each store, preferring getting something from the same character, then vault, then any other character const sortedStores = stores.toSorted( compareBy((store) => { if (target.id === store.id) { return 0; } else if (store.isVault) { return 1; } else { return 2; } }), ); let result: DimItem | undefined; for (const store of sortedStores) { result = searchForSimilarItem(getState, item, store, exclusions, target, excludeExotic); if (result) { break; } } return result; } /** * Bulk equip items. Only use for multiple equips at once (just loadouts). * Returns a map of item ids to their success status (PlatformErrorCodes.Success if it succeeded), which can be less * that what was passed in or even more than what was passed in because * sometimes we have to de-equip an exotic to equip another exotic. */ export function equipItems( store: DimStore, items: DimItem[], /** A list of items to not consider equipping in order to de-equip an exotic */ exclusions: readonly Exclusion[], session: MoveSession, ): ThunkResult<{ [itemInstanceId: string]: PlatformErrorCodes }> { return async (dispatch, getState) => { const getStores = () => storesSelector(getState()); // Check for (and move aside) exotics const extraItemsToEquip: Promise[] = filterMap(items, (i) => { if (i.equippingLabel) { const otherExotic = getOtherExoticThatNeedsDequipping(i, store); // If we aren't already equipping into that slot... if (otherExotic && !items.find((i) => i.bucket.hash === otherExotic.bucket.hash)) { const similarItem = getSimilarItem(getState, getStores(), otherExotic, { excludeExotic: true, exclusions, }); if (!similarItem) { return Promise.reject( new DimError( 'ItemService.Deequip', t('ItemService.Deequip', { itemname: otherExotic.name }), ), ); } const target = getStore(getStores(), similarItem.owner)!; if (store.id === target.id) { return Promise.resolve(similarItem); } else { // If we need to get the similar item from elsewhere, do that first return dispatch(executeMoveItem(similarItem, store, { equip: true }, session)).then( () => similarItem, ); } } } return undefined; }); const extraItems = await Promise.all(extraItemsToEquip); items = items.concat(extraItems); if (items.length === 0) { return {}; } // It's faster to call equipItem for a single item if (items.length === 1) { try { await dispatch(equipItem(items[0], session.cancelToken)); return { [items[0].id]: PlatformErrorCodes.Success }; } catch (e) { return { [items[0].id]: (e instanceof DimError && e.bungieErrorCode()) || PlatformErrorCodes.UnhandledException, }; } } session.cancelToken.checkCanceled(); try { const results = await equipItemsApi(items[0])( currentAccountSelector(getState())!, store, items, ); // Update our view of each successful item for (const [itemInstanceId, resultCode] of Object.entries(results)) { if (resultCode === PlatformErrorCodes.Success) { const item = items.find((i) => i.id === itemInstanceId); if (item) { dispatch(updateItemModel(item, store, store, true)); } } } return results; } catch (e) { dispatch(handleAuthErrors(e)); throw e; } }; } function equipItem(item: DimItem, cancelToken: CancelToken): ThunkResult { return async (dispatch, getState) => { try { const store = getStore(storesSelector(getState()), item.owner)!; if ($featureFlags.debugMoves) { infoLog('equip', 'Equip', item.name, item.typeName, 'to', store.name); } cancelToken.checkCanceled(); await equipApi(item)(currentAccountSelector(getState())!, item); return dispatch(updateItemModel(item, store, store, true)); } catch (e) { dispatch(handleAuthErrors(e)); throw e; } }; } /** De-equip an item, which really means find another item to equip in its place. */ function dequipItem( item: DimItem, session: MoveSession, { excludeExotic = false, }: { /** Don't pick an exotic to equip in this item's place (because we're specifically trying to dequip an exotic) */ excludeExotic?: boolean; } = { excludeExotic: false }, ): ThunkResult { return async (dispatch, getState) => { const stores = storesSelector(getState()); const similarItem = getSimilarItem(getState, stores, item, { excludeExotic }); if (!similarItem) { throw new DimError('ItemService.Deequip', t('ItemService.Deequip', { itemname: item.name })); } const ownerStore = getStore(stores, item.owner)!; await dispatch(executeMoveItem(similarItem, ownerStore, { equip: true }, session)); return item; }; } function moveToVault(item: DimItem, amount: number, session: MoveSession): ThunkResult { return async (dispatch, getState) => dispatch(moveToStore(item, getVault(storesSelector(getState()))!, false, amount, session)); } function moveToStore( item: DimItem, store: DimStore, equip: boolean, amount: number, session: MoveSession, ): ThunkResult { return async (dispatch, getState) => { const getStores = () => storesSelector(getState()); const ownerStore = getStore(getStores(), item.owner)!; if ($featureFlags.debugMoves) { item.location.inPostmaster ? infoLog( TAG, 'Pull', amount, item.name, item.typeName, 'to', store.name, 'from Postmaster', ) : infoLog( 'move', 'Move', amount, item.name, item.typeName, 'to', store.name, 'from', ownerStore.name, ); } // Work around https://github.com/Bungie-net/api/issues/764#issuecomment-437614294 by recording lock state for items before moving. // Note that this can result in the wrong lock state if DIM is out of date (they've locked/unlocked in game but we haven't refreshed). // Only apply this hack if the source bucket contains duplicates of the same item hash. const overrideLockState = item.lockable && count(findItemsByBucket(ownerStore, item.location.hash), (i) => i.hash === item.hash) > 1 ? item.locked : undefined; session.cancelToken.checkCanceled(); try { await transferApi(item)(currentAccountSelector(getState())!, item, store, amount); } catch (e) { dispatch(handleAuthErrors(e)); // Not sure why this happens - maybe out of sync game state? if ( e instanceof DimError && e.bungieErrorCode() === PlatformErrorCodes.DestinyCannotPerformActionOnEquippedItem ) { await dispatch(dequipItem(item, session)); await transferApi(item)(currentAccountSelector(getState())!, item, store, amount); } else { throw e; } } const source = getStore(getStores(), item.owner)!; const newItem = dispatch(updateItemModel(item, source, store, false, amount)); item = newItem.owner !== 'vault' && equip ? await dispatch(equipItem(newItem, session.cancelToken)) : newItem; if (overrideLockState !== undefined) { // Run this async, without waiting for the result (async () => { infoLog( 'move', 'Resetting lock status of', item.name, 'to', overrideLockState, 'when moving to', store.name, 'to work around Bungie.net lock state bug', ); try { await dispatch(setItemLockState(item, overrideLockState)); } catch (e) { errorLog(TAG, 'Lock state override failed', e); } })(); } return item; }; } /** * This returns a promise for true if the exotic can be * equipped. In the process it will move aside any existing exotic * that would conflict. If it could not move aside, this * rejects. It never returns false. */ function canEquipExotic( item: DimItem, store: DimStore, session: MoveSession, ): ThunkResult { return async (dispatch) => { const otherExotic = getOtherExoticThatNeedsDequipping(item, store); if (otherExotic) { try { await dispatch(dequipItem(otherExotic, session, { excludeExotic: true })); return true; } catch (e) { throw new Error( t('ItemService.ExoticError', { itemname: item.name, slot: otherExotic.typeName, error: errorMessage(e), }), { cause: e }, ); } } else { return true; } }; } /** * Identify the other exotic, if any, that needs to be moved * aside. This is not a promise, it returns immediately. */ function getOtherExoticThatNeedsDequipping(item: DimItem, store: DimStore): DimItem | undefined { if (!item.equippingLabel) { return undefined; } // Find an item that's not in the slot we're equipping, but has a matching equipping label return store.items.find( (i) => i.equipped && i.equippingLabel === item.equippingLabel && i.bucket.hash !== item.bucket.hash, ); } interface MoveContext { /** Bucket hash */ originalItemType: number; excludes: readonly Exclusion[]; spaceLeft: (s: DimStore, i: DimItem) => number; } /** * Choose another item that we can move out of "target" in order to * make room for "item". We already know when this function is * called that store has no room for item. * * The concept is that DIM is able to make "smart moves" by moving other items * out of the way, but it should do so in the least disruptive way possible, and * should generally cause your inventory to move towards a state of organization. * Especially important is that we avoid moving items back onto the active character * unless there's no other option. * * @param target the store to choose a move aside item from. * @param item the item we're making space for. * @param moveContext a helper object that can answer questions about how much space is left. * @return An object with item and target properties representing both the item and its destination. This won't ever be undefined. * @throws {Error} An error if no move aside item could be chosen. */ function chooseMoveAsideItem( getState: () => RootState, target: DimStore, item: DimItem, moveContext: MoveContext, ): { item: DimItem; target: DimStore; } { // Check whether an item cannot or should not be moved function isMovable(otherItem: DimItem) { return ( !otherItem.notransfer && !moveContext.excludes.some((i) => i.id === otherItem.id && i.hash === otherItem.hash) ); } const stores = storesSelector(getState()); const otherStores = stores.filter((s) => s.id !== target.id); // Start with candidates of the same type (or vault bucket if it's vault) // TODO: This try/catch is to help debug https://sentry.io/destiny-item-manager/dim/issues/484361056/ let allItems: DimItem[]; try { allItems = target.isVault ? target.items.filter( (i) => i.bucket.vaultBucket && item.bucket.vaultBucket && i.bucket.vaultBucket.hash === item.bucket.vaultBucket.hash, ) : findItemsByBucket(target, item.bucket.hash); } catch (e) { if (target.isVault && !item.bucket.vaultBucket) { errorLog( 'move', 'Item', item.name, "has no vault bucket, but we're trying to move aside room in the vault for it", ); } else if (target.items.some((i) => !i.bucket.vaultBucket)) { errorLog( 'move', 'The vault has items with no vault bucket: ', target.items.filter((i) => !i.bucket.vaultBucket).map((i) => i.name), ); } throw e; } const moveAsideCandidates = allItems.filter(isMovable); // if there are no candidates at all, fail if (moveAsideCandidates.length === 0) { throw new DimError( 'no-space', t('ItemService.NotEnoughRoom', { store: target.name, itemname: item.name }), ).withError(new DimError('ItemService.NotEnoughRoomGeneral')); } // Find any stackable that could be combined with another stack // on a different store to form a single stack let otherStore: DimStore | undefined; const stackable = moveAsideCandidates.find((i) => { if (i.maxStackSize > 1) { // Find another store that has an appropriate stackable otherStore = otherStores.find((s) => s.items.some( (otherItem) => // Same basic item otherItem.hash === i.hash && !otherItem.location.inPostmaster && // Enough space to absorb this stack i.maxStackSize - otherItem.amount >= i.amount, ), ); } return Boolean(otherStore); }); if (stackable && otherStore) { return { item: stackable, target: otherStore, }; } const getTag = getTagSelector(getState()); const isInInGameLoadoutFor = isInInGameLoadoutForSelector(getState()); // A cached version of the space-left function const cachedSpaceLeft = memoize( ([store, item]: [store: DimStore, item: DimItem]) => moveContext.spaceLeft(store, item), { getCacheKey: ([store, item]) => { // cache key if (item.maxStackSize > 1) { return store.id + item.hash; } else { return store.id + item.bucket.hash; } }, }, ); const vault = getVault(stores)!; // Iterate through other stores from least recently played to most recently played. // The concept is that we prefer filling up the least-recently-played character before even // bothering with the others. let moveAsideCandidate = (() => { const otherCharacters = otherStores .filter((s) => !s.isVault) .sort(compareBy((s) => s.lastPlayed.getTime())); for (const targetStore of otherCharacters) { const sortedCandidates = sortMoveAsideCandidatesForStore( moveAsideCandidates, target, targetStore, getTag, isInInGameLoadoutFor, item, ); for (const candidate of sortedCandidates) { const spaceLeft = cachedSpaceLeft([targetStore, candidate]); if (target.isVault) { // If we're moving from the vault // If the target character has any space, put it there if (candidate.amount <= spaceLeft) { return { item: candidate, target: targetStore, }; } } else { // If we're moving from a character // If there's exactly one *slot* left on the vault, and // we're not moving the original item *from* the vault, put // the candidate on another character in order to avoid // gumming up the vault. const openVaultAmount = cachedSpaceLeft([vault, candidate]); const openVaultSlotsBeforeMove = Math.floor(openVaultAmount / candidate.maxStackSize); const openVaultSlotsAfterMove = Math.max( 0, Math.floor((openVaultAmount - candidate.amount) / candidate.maxStackSize), ); if (openVaultSlotsBeforeMove === 1 && openVaultSlotsAfterMove === 0 && spaceLeft) { return { item: candidate, target: targetStore, }; } } } } })(); // If we're moving off a character (into the vault) and we couldn't find a better match, // just try to shove it in the vault, and we'll recursively squeeze something else out of the vault. if (!moveAsideCandidate && !target.isVault) { moveAsideCandidate = { item: moveAsideCandidates[0], target: vault, }; } if (!moveAsideCandidate) { throw new DimError( 'no-space', t('ItemService.NotEnoughRoom', { store: target.name, itemname: item.name }), ).withError(new DimError('ItemService.NotEnoughRoomGeneral')); } return moveAsideCandidate; } /** * Ensures there is enough space to move the given item into store. * This will refresh data and/or move items aside in an attempt to make a move possible. * * This recursively calls itself to accommodate multi-step moves. * Returns `true` if you're good to go (or if the item's already there). * * @param item The item we're trying to move. * @param store The destination store. * @param options.triedFallback True if we've already tried reloading stores * @param options.excludes A list of items that should not be moved in * order to make space for this move. * @param options.reservations A map from store => type => number of spaces to leave open. * @param options.numRetries A count of how many alternate items we've tried. * @return a promise that's either resolved if the move can proceed or rejected with an error. */ function ensureCanMoveToStore( item: DimItem, store: DimStore, amount: number, options: { excludes: Exclusion[]; reservations: Immutable; numRetries?: number; }, session: MoveSession, ): ThunkResult { return async (dispatch, getState) => { const { excludes = [], reservations = {}, numRetries = 0 } = options; function spaceLeftWithReservations(s: DimStore, i: DimItem) { let left = spaceLeftForItem(s, i, storesSelector(getState())); // minus any reservations if (reservations[s.id]?.[i.bucket.hash]) { left -= reservations[s.id][i.bucket.hash]; } // but not counting the original item that's moving if ( s.id === item.owner && i.bucket.hash === item.bucket.hash && !item.location.inPostmaster ) { left--; } // if this is a consumable, and wasn't an explicitly requested move, // pretend the consumables bucket is 1 stack smaller, so we don't automatically max it out if (i.bucket.hash === BucketHashes.Consumables && !session.involvedItems.has(i.hash)) { left -= i.maxStackSize; } return Math.max(0, left); } if (item.owner === store.id && !item.location.inPostmaster) { return true; } // You can't move more than the max stack of a unique stack item. if (item.uniqueStack && amountOfItem(store, item) + amount > item.maxStackSize) { throw new DimError('no-space', t('ItemService.StackFull', { name: item.name })); } const stores = storesSelector(getState()); // How much space will be needed (in amount, not stacks) in the target store in order to make the transfer? const storeReservations: { [storeId: string]: number } = {}; storeReservations[store.id] = amount; // guardian-to-guardian transfer will also need space in the vault if (item.owner !== 'vault' && !store.isVault && item.owner !== store.id) { storeReservations.vault = amount; } // How many items need to be moved away from each store (in amount, not stacks) const movesNeeded: { [storeId: string]: number } = {}; for (const s of stores) { if (storeReservations[s.id]) { movesNeeded[s.id] = Math.max( 0, storeReservations[s.id] - spaceLeftWithReservations(s, item), ); } } if (Object.values(movesNeeded).every((m) => m === 0)) { // If there are no moves needed, we're clear to go return true; } else { // Move aside one of the items that's in the way const moveContext: MoveContext = { originalItemType: item.bucket.hash, excludes, spaceLeft(s, i) { let left = spaceLeftWithReservations(s, i); if (i.bucket.hash === this.originalItemType && storeReservations[s.id]) { left -= storeReservations[s.id]; } return Math.max(0, left); }, }; // Move starting from the vault (which is always last) const [sourceStoreId] = Object.entries(movesNeeded).findLast( ([_storeId, moveAmount]) => moveAmount > 0, )!; const moveAsideSource = getStore(stores, sourceStoreId)!; const { item: moveAsideItem, target: moveAsideTarget } = chooseMoveAsideItem( getState, moveAsideSource, item, moveContext, ); if ( !moveAsideTarget || (!moveAsideTarget.isVault && spaceLeftForItem(moveAsideTarget, moveAsideItem, stores) <= 0) ) { const itemtype = moveAsideTarget.isVault ? moveAsideItem.destinyVersion === 1 ? moveAsideItem.bucket.sort! : '' : moveAsideItem.typeName; throw new DimError( 'no-space', moveAsideTarget.isVault ? t('ItemService.BucketFull.Vault', { itemtype, store: moveAsideTarget.name, }) : t('ItemService.BucketFull.Guardian', { itemtype, store: moveAsideTarget.name, context: moveAsideTarget.genderName, }), ); } else { // Make one move and start over! try { const moveAsideOpts = { equip: false, amount: moveAsideItem.amount, excludes, reservations, }; await dispatch(executeMoveItem(moveAsideItem, moveAsideTarget, moveAsideOpts, session)); return await dispatch(ensureCanMoveToStore(item, store, amount, options, session)); } catch (e) { if (numRetries < 3) { // Exclude this item and try again so we pick another excludes.push(moveAsideItem); options.excludes = excludes; options.numRetries = numRetries + 1; errorLog( 'move', `Unable to move aside ${moveAsideItem.name} to ${moveAsideTarget.name}. Trying again.`, e, ); return dispatch(ensureCanMoveToStore(item, store, amount, options, session)); } else { throw e; } } } } }; } /** * Returns if possible, or throws an exception if the item can't be equipped. */ function canEquip(item: DimItem, store: DimStore): void { if (itemCanBeEquippedBy(item, store)) { return; } else if (item.classified) { throw new DimError('ItemService.Classified'); } else { const message = item.classType === DestinyClass.Unknown ? t('ItemService.OnlyEquippedLevel', { level: item.equipRequiredLevel }) : t('ItemService.OnlyEquippedClassLevel', { class: item.classTypeNameLocalized.toLowerCase(), level: item.equipRequiredLevel, }); throw new DimError('wrong-level', message); } } /** * Ensures there is enough space to move the given item into store. * This will refresh data/move items aside/de-equip exotics, * in an attempt to make a move possible. * * This is functionally just ensureCanMoveToStore, with an * additional accommodation for equips and the one-exotic rule. */ function ensureValidTransfer( equip: boolean, store: DimStore, item: DimItem, amount: number, excludes: Exclusion[], reservations: Immutable, session: MoveSession, ): ThunkResult { return async (dispatch) => { if (equip) { canEquip(item, store); // may throw if (item.equippingLabel) { await dispatch(canEquipExotic(item, store, session)); // may throw } } return dispatch(ensureCanMoveToStore(item, store, amount, { excludes, reservations }, session)); }; } /** * Move item to target store, optionally equipping it. This is the "low level" smart move, which will move items out of * the way if necessary, but it doesn't have error/progress notification or any of that. Use the functions in `move-item.ts` for * user-initiated moves, while this function is meant for implementing things that move items such as those user-initiated functions, * loadout apply, etc. * * @param item the item to move. * @param target the store to move it to. * @param equip true to equip the item, false to leave it unequipped. * @param amount how much of the item to move (for stacks). Can span more than one stack's worth. * @param excludes A list of {id, hash} objects representing items that should not be moved aside to make the move happen. * @param reservations A map of store id to the amount of space to reserve in it for items like "item". * @param session An object used to track properties such as cancellation or stores filling up for a whole sequence of moves. * @return A promise for the completion of the whole sequence of moves, or a rejection if the move cannot complete. */ export function executeMoveItem( item: DimItem, target: DimStore, { equip = false, amount = item.amount || 1, excludes = [], reservations = {}, }: { equip?: boolean; amount?: number; excludes?: Exclusion[]; reservations?: Immutable; }, session: MoveSession, ): ThunkResult { return async (dispatch, getState) => { const getStores = () => storesSelector(getState()); let source = getStore(getStores(), item.owner)!; // Reassign the target store to the active store if we're moving the item to an account-wide bucket if (!target.isVault && item.bucket.accountWide) { target = getCurrentStore(getStores())!; } // We're moving from the vault to the current character. Maybe they're // playing the game and deleting stuff? Try just jamming it in there, and // catch any errors. If we find out that the store is full through this // probe, don't try again for the rest of this move session. if ( // Either pulling from the vault, (source.isVault || // or pulling from the postmaster (item.location.inPostmaster && (source.id === target.id || item.bucket.accountWide))) && // To the current character target.current && // don't blind move if this destination bucket already had a blind move failure !session.bucketsFullOnCurrentStore.has(item.bucket.hash) && // don't blind move consumables to character, // because we don't want to unintentionally max out consumables item.bucket.hash !== BucketHashes.Consumables ) { try { infoLog(TAG, 'Try blind move of', item.name, 'to', target.name); return await dispatch(moveToStore(item, target, equip, amount, session)); } catch (e) { if ( e instanceof DimError && // TODO: does this fire for pull from postmaster? e.bungieErrorCode() === PlatformErrorCodes.DestinyNoRoomInDestination ) { warnLog( 'move', 'Tried blindly moving', item.name, 'to', target.name, 'but the bucket is really full', ); session.bucketsFullOnCurrentStore.add(item.bucket.hash); } else { throw e; } } } // for any case that's not char-to-char, we can free up space ahead of time if (source.isVault || target.isVault || source.id === target.id || item.bucket.accountWide) { await dispatch( ensureValidTransfer(equip, target, item, amount, excludes, reservations, session), ); // Replace the target store - ensureValidTransfer may have reloaded it target = getStore(getStores(), target.id)!; source = getStore(getStores(), item.owner)!; } // Get from postmaster first if (item.location.inPostmaster) { if (source.id === target.id || item.bucket.accountWide) { item = await dispatch(moveToStore(item, target, equip, amount, session)); } else { item = await dispatch( executeMoveItem(item, source, { equip, amount, excludes, reservations }, session), ); target = getStore(getStores(), target.id)!; source = getStore(getStores(), item.owner)!; } } if (!source.isVault && !target.isVault) { // Guardian to Guardian if (source.id !== target.id && !item.bucket.accountWide) { // Different Guardian if (item.equipped) { item = await dispatch(dequipItem(item, session)); } // for char to char, two moves are required: char to vault then vault to char // make sure the vault has space before trying to vault the item await dispatch( ensureValidTransfer( false, getVault(getStores())!, item, amount, excludes, reservations, session, ), ); target = getStore(getStores(), target.id)!; item = await dispatch(moveToVault(item, amount, session)); // now make sure the target char has space before trying to unvault the item await dispatch( ensureValidTransfer(equip, target, item, amount, excludes, reservations, session), ); target = getStore(getStores(), target.id)!; item = await dispatch(moveToStore(item, target, equip, amount, session)); } if (equip && !item.equipped) { item = await dispatch(equipItem(item, session.cancelToken)); } else if (!equip && item.equipped) { item = await dispatch(dequipItem(item, session)); } } else if (source.isVault && target.isVault) { // Vault to Vault // Do Nothing. } else if (source.isVault || target.isVault) { // Guardian to Vault or Vault to Guardian if (item.equipped) { item = await dispatch(dequipItem(item, session)); } item = await dispatch(moveToStore(item, target, equip, amount, session)); } return item; }; } /** * Sort a list of items to determine a prioritized order for which should be moved from fromStore * assuming they'll end up in targetStore. */ export function sortMoveAsideCandidatesForStore( moveAsideCandidates: DimItem[], fromStore: DimStore, targetStore: DimStore, getTag: (item: DimItem) => TagValue | undefined, isInInGameLoadoutFor: (item: DimItem, ownerId: string) => boolean, /** The item we're trying to make space for. May be missing. */ displacer?: DimItem, ) { // A sort for items to use for ranking *which item to move* // aside. The highest ranked items are the most likely to be moved. // Note that this is reversed, so higher values (including true over false) // come first in the list. const itemValueComparator: (a: DimItem, b: DimItem) => number = reverseComparator( chainComparator( // MECHANICAL CONSIDERATIONS RELATED TO MOVING ITEMS // Try our hardest never to unequip something compareBy((displaced) => !displaced.equipped), // prefer same bucket over everything, because that makes space in the correct "pocket" compareBy( (displaced) => !fromStore.isVault && displaced.bucket.hash === displacer?.bucket.hash, ), // TODO: Prefer moving from vault into Inventory (consumables) // TODO: Prefer moving from vault into the bucket with the most free space // D1 HAD ENGRAMS MIXED IN WITH INVENTORY // Engrams prefer to be in the vault, so not-engram is larger than engram compareBy((displaced) => (fromStore.isVault ? !displaced.isEngram : displaced.isEngram)), // DON'T ANNOY PEOPLE // Always prefer keeping something that was manually moved where it is compareBy((displaced) => -getLastManuallyMoved(displaced)), // CONVENIENCES RELATED TO WHETHER ITEMS ARE USEFUL, AND TO WHICH CHARACTER // Prefer displacing on-char items that AREN'T in their owner's in-game loadouts compareBy( (displaced) => !fromStore.isVault && !isInInGameLoadoutFor(displaced, fromStore.id), ), // prefer displacing a vaulted item to a char with the item in a loadout compareBy( (displaced) => !targetStore.isVault && isInInGameLoadoutFor(displaced, targetStore.id), ), // Prefer moving an item if the owner can't use it compareBy((displaced) => !fromStore.isVault && !itemCanBeEquippedBy(displaced, fromStore)), // Prefer moving things the target store can use compareBy((displaced) => !targetStore.isVault && itemCanBeEquippedBy(displaced, targetStore)), // TRYING TO ESTIMATE USER INTENTION AND ITEM VALUE // Tagged items sort by orders defined in dim-item-info reverseComparator( compareByIndex( fromStore.isVault ? vaultDisplacePriority : characterDisplacePriority, (displaced) => getTag(displaced) ?? 'none', ), ), // Prefer moving lower-tier into the vault and higher tier out compareBy((i) => (fromStore.isVault ? ItemRarityMap[i.rarity] : -ItemRarityMap[i.rarity])), // Prefer keeping higher-stat items on characters compareBy( (i) => (i.primaryStat && (fromStore.isVault ? i.primaryStat.value : -i.primaryStat.value)) || 0, ), ), ); // Sort all candidates moveAsideCandidates.sort(itemValueComparator); return moveAsideCandidates; } /** * Find an item in store like "item", excluding the exclusions, to be equipped * on target. Generally used to help with de-equipping item. * @param exclusions a list of {id, hash} objects that won't be considered for equipping. * @param excludeExotic exclude any item matching the equippingLabel of item, used when dequipping an exotic so we can equip an exotic in another slot. */ function searchForSimilarItem( getState: () => RootState, item: DimItem, store: DimStore, exclusions: readonly Exclusion[] = [], target: DimStore, excludeExotic: boolean, ): DimItem | undefined { const candidates = store.items.filter( (i) => i.location.hash === item.location.hash && !i.equipped && // Not the same item i.id !== item.id && itemCanBeEquippedBy(i, target) && // Not on the exclusion list !exclusions.some((item) => item.id === i.id && item.hash === i.hash) && (!excludeExotic || i.equippingLabel !== item.equippingLabel), ); if (!candidates.length) { return undefined; } const getTag = getTagSelector(getState()); // A sort for items to use for ranking which item to use to replace the // already equipped item. The highest ranked items are the most likely to be // used. Note that this is reversed, so higher values (including true over // false) come first in the list. const itemValueComparator: (a: DimItem, b: DimItem) => number = reverseComparator( chainComparator( // Try hard not to choose exotics - it's weird to replace an exotic with another random exotic. // But we might have to if there's no other option. compareBy((i) => !i.equippingLabel), // try to match type (e.g. scout rifle). TODO: look into using ItemSubType instead compareBy((i) => i.typeName === item.typeName), reverseComparator(compareByIndex(equipReplacePriority, (i) => getTag(i) ?? 'none')), // Prefer rarer items compareBy((i) => ItemRarityMap[i.rarity]), // Prefer higher-stat items compareBy((i) => i.primaryStat?.value ?? 0), ), ); const sortedCandidates = candidates.sort(itemValueComparator); return ( sortedCandidates.find((result) => { if (result.equippingLabel) { const otherExotic = getOtherExoticThatNeedsDequipping(result, store); // If there aren't other exotics equipped, or the equipped one is the one we're dequipping, we're good return !otherExotic || otherExotic.id === item.id; } else { return true; } }) || undefined ); } ================================================ FILE: src/app/inventory/item-types.ts ================================================ import type { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import type { ItemRarityName } from 'app/search/d2-known-values'; import { DestinyAmmunitionType, DestinyBreakerTypeDefinition, DestinyClass, DestinyDamageTypeDefinition, DestinyDisplayPropertiesDefinition, DestinyEquipableItemSetDefinition, DestinyIconDefinition, DestinyInventoryItemDefinition, DestinyItemInstanceEnergy, DestinyItemInvestmentStatDefinition, DestinyItemPerkEntryDefinition, DestinyItemPlugBase, DestinyItemQuantity, DestinyItemSocketEntryDefinition, DestinyItemTooltipNotification, DestinyObjectiveProgress, DestinyPlugItemCraftingRequirements, DestinyRecordComponent, DestinySocketCategoryDefinition, DestinyStat, } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes, TraitHashes } from 'data/d2/generated-enums'; import { InventoryBucket } from './inventory-buckets'; /** * A generic DIM item, representing almost anything. This completely represents any D2 item, and most D1 items, * though you can specialize down to the D1Item type for some special D1 properties and overrides. * * Prefer calculating values at the point of display instead of adding more stuff to this, and prefer making optional * properties instead of "| null". */ // TODO: This interface is clearly too large - break out interfaces for common subsets // TODO: separate out "mutable" vs "immutable" data export interface DimItem { // Static data - this won't ever change for the lifetime of the item, because it's derived from the definition or is intrinsic to the item's identity. These would only change if the manifest updates. /** A synthetic unique ID used to help React tell items apart. Use this as a "key" property. We can't just use "id" because some items don't have one. */ index: string; /** Item instance id. Non-instanced items have id "0" for D1 compatibility. */ id: string; /** The inventoryItemHash, see DestinyInventoryItemDefinition. */ hash: number; /** Is the item an instance of an item, in the user's inventory? Uninstanced items include stacked consumables, some bounties/quests, and fake items created for vendors, progress, etc. */ instanced: boolean; /** Is this classified? Some items are classified in the manifest. */ classified: boolean; /** The version of Destiny this comes from. */ destinyVersion: DestinyVersion; /** * Localized name of this item's type. Only used for display - use bucket.hash * or itemCategoryHashes to figure out what kind of item this is * programmatically. */ typeName: string; /** The bucket the item normally resides in (even though it may currently be elsewhere, such as in the postmaster). */ bucket: InventoryBucket; /** Hashes of DestinyItemCategoryDefinitions this item belongs to */ itemCategoryHashes: ItemCategoryHashes[]; /** * There's also traitHashes which fills a similar role of tagging some aspect * of an item, but is not as hierarchical as itemCategoryHashes. This seems to * be favored over itemCategoryHashes in newer content. */ traitHashes?: TraitHashes[]; /** A readable English name for the rarity of the item (e.g. "Exotic", "Rare"). Do not use this for display! */ rarity: ItemRarityName; /** Is this an Exotic item? */ isExotic: boolean; /** If this came from a vendor (instead of character inventory), this houses enough information to re-identify the item. */ vendor?: { vendorHash: number; vendorItemIndex: number; characterId: string }; /** Localized name of the item. */ name: string; /** Localized description of the item. */ description: string; /** * Icon path for the item. * @deprecated for display - use iconDef instead. */ icon: string; /** Hidden Icon overlay path for the item. Only used to figure out what season/event an item is from in some edge cases. */ hiddenOverlay?: string; /** * Icon overlay path for the item. Currently used to correct old season icons into new ones for reissued items * @deprecated for display - use iconDef instead. */ iconOverlay?: string; /** Some items have a secondary icon, namely Emblems. */ secondaryIcon?: string; /** Some items have a full icon definition attached which provides layered icon assets. */ iconDef?: DestinyIconDefinition; /** If the item has an ornament applied, this is the icon info for that ornament. */ ornamentIconDef?: DestinyIconDefinition; /** Whether we can pull this item from the postmaster */ canPullFromPostmaster: boolean; /** Is this "equipment" (items that can be equipped). */ equipment: boolean; /** * If defined, this is the label used to check if the character has other items of * matching types already equipped. * * For instance, when you aren't allowed to equip more than one Exotic Weapon, that's * because all exotic weapons have identical labels and the game checks the * to-be-equipped item's label vs. all other already equipped items (other * than the item in the slot that's about to be occupied). */ equippingLabel?: string; /** What type of ammo this weapon takes, or None if it isn't a weapon */ ammoType: DestinyAmmunitionType; /** The level a character must be to equip this item. */ equipRequiredLevel: number; /** The maximum number of items that stack together for this item type. */ maxStackSize: number; /** Is this stack unique (one per account, sometimes two if you can move to vault)? */ uniqueStack: boolean; /** * The class this item is restricted to. DestinyClass.Unknown means it can be used by any class. * DestinyClass.Classified is for classified armor, which, until proven otherwise, can't be equipped by any class. * */ classType: DestinyClass; /** The localized name of the class this item is restricted to. */ classTypeNameLocalized: string; /** Whether this item can be locked. */ lockable: boolean; /** Can this item be tracked? (For quests/bounties.) */ trackable: boolean; /** Can this be tagged? */ taggable: boolean; /** Can this be compared with other items? */ comparable: boolean; /** * Can this item receive wish list thumbs up icons? * True for inventory and vendor items, false for really fake items. */ wishListEnabled: boolean; /** Should we hide the percentage display? */ hidePercentage: boolean; /** Can this be infused? */ infusable: boolean; /** Can this be used as infusion fuel? */ infusionFuel: boolean; /** Perks, which are specifically called-out special abilities of items shown in the game's popup UI. */ perks?: DestinyItemPerkEntryDefinition[]; /** Is this an engram? */ isEngram: boolean; /** The reference hash for lore attached to this item (D2 only). */ loreHash?: number; /** Metrics that can be used with this item. */ availableMetricCategoryNodeHashes?: number[]; /** If any two items share at least one number on this list, they can be infused into each other. */ infusionCategoryHashes: number[] | null; /** The DestinyVendorDefinition hash of the vendor that can preview the contents of this item, if there is one. */ previewVendor?: number; /** Localized string for where this item comes from... or other stuff like it not being recoverable from collections */ displaySource?: string; collectibleHash?: number; // TODO: pull search-only fields out /** The DestinyCollectibleDefinition sourceHash for a specific item (D2). Derived entirely from collectibleHash */ source?: number; /** Information about this item as a plug. Mostly useful for mod collectibles. */ plug?: { energyCost: number; }; /** Extra pursuit info, if this item is a quest or bounty. */ pursuit: DimPursuit | null; // "Mutable" data - this may be changed by moving the item around, lock/unlock, etc. Any place DIM updates its view of the world without a profile refresh. This info is always reset to server truth on a refresh. /** * The ID of the store that currently contains this item. * * This will be a Bungie character ID (long string of numbers) or the string 'vault'. */ owner: string; /** Is this item currently equipped? */ equipped: boolean; /** The bucket the item is currently in. */ location: InventoryBucket; /** Is this item tracked? (For quests/bounties). */ tracked: boolean; /** Is this item locked? */ locked: boolean; // Dynamic data - this may change between profile updates /** The damage type this weapon deals, or damage type corresponding to the item's elemental resistance. */ element: DestinyDamageTypeDefinition | null; /** Whether this item CANNOT be transferred. */ notransfer: boolean; /** Is this item complete (leveled, unlocked, objectives complete)? */ complete: boolean; /** How many items does this represent? Only greater than one if maxStackSize is greater than one. */ amount: number; /** * The primary stat (Attack, Defense, Speed) of the item. Useful for display and for some weirder stat types. Prefer using "power" if what you want is power. */ primaryStat: DestinyStat | null; /** * Display info for the primary stat (Attack, Defense, Speed, etc). */ primaryStatDisplayProperties?: DestinyDisplayPropertiesDefinition; /** The power level of the item. This is a synonym for (primaryStat?.value ?? 0) for items with power, and 0 otherwise. */ power: number; /** Is this a masterwork? (D2 only) */ masterwork: boolean; /** If truthy, Bungie indicated this item is crafted. This could just mean the item has a level and a crafting date, like enhanced weapons, even partially-enhanced ones. */ crafted: 'crafted' | 'enhanced' | false; /** Does this have a highlighted (crafting) objective? (D2 Only) */ highlightedObjective: boolean; /** What percent complete is this item (considers XP and objectives). */ percentComplete: number; /** D2 items use sockets and plugs to represent everything from perks to mods to ornaments and shaders. */ sockets: DimSockets | null; /** Sometimes the API doesn't return socket info. This tells whether the item *should* have socket info but doesn't. */ missingSockets: false | 'missing' | 'not-loaded'; /** Detailed stats for the item. */ stats: DimStat[] | null; /** Any objectives associated with the item. */ objectives?: DestinyObjectiveProgress[]; /** Stat Tracker */ metricHash?: number; /** Stat Tracker Progress */ metricObjective?: DestinyObjectiveProgress; /** for D2 Y3 armor, this is the type and capacity information */ energy: DestinyItemInstanceEnergy | null; /** If this item is a masterwork, this will include information about its masterwork properties. */ masterworkInfo: DimMasterwork | null; /** If this item is crafted, this includes info about its crafting properties. */ craftedInfo?: DimCrafted; /** * The record (triumph) that corresponds to this item's crafting pattern, if * it has one. This should be populated whether or not the pattern is unlocked. * Optional in case we ever fail to match items to their record. */ patternUnlockRecord?: DestinyRecordComponent; /** If this item has Deepsight Resonance (a pattern can be extracted). */ deepsightInfo?: boolean; /** If this item has a catalyst, this includes info about its catalyst properties. */ catalystInfo?: DimCatalyst; /** an item's current breaker type, if it has one */ breakerType: DestinyBreakerTypeDefinition | null; /** The foundry this item was made by */ // TODO: only used by search/spreadsheet foundry?: string; /** Extra tooltips to show in the item popup */ tooltipNotifications?: DestinyItemTooltipNotification[]; /** Is this a "featured" weapon/armor that gains some bonus from being new? This was introduced in Edge of Fate. */ featured: boolean; /** * In D2 since Edge of Fate, items can drop at a particular tier, 1-5, which * provides increasing benefits. Pre-tiered items and all D1 items will have * tier 0. */ tier: number; /** In D2 since Edge of Fate, items can have a set bonus with other items */ setBonus?: DestinyEquipableItemSetDefinition; /** Is this an adept weapon? (D2 only) */ adept: boolean; /** Is this a holofoil (shiny) weapon? (D2 only) */ holofoil: boolean; } /** * A Destiny 1 item. Use this type when you need specific D1 properties. */ export interface D1Item extends DimItem { talentGrid: D1TalentGrid | null; stats: D1Stat[] | null; /** Armor quality evaluation. */ quality: { /** The maximum stat range this armor could achieve when fully infused. */ min: number; /** The minimum stat range this armor could achieve when fully infused. */ max: number; /** A displayable range of percentages. */ range: string; } | null; /** Hashes that allow us to figure out where this item can be found (what activities, locations, etc.) */ sourceHashes: number[]; } export interface DimMasterwork { /** The tier of the masterwork (not the same as the stat!). */ tier?: number; /** The stats that are enhanced by this masterwork. */ stats?: { hash: number; /** The name of the stat enhanced by this masterwork. */ name: string; /** How much the stat is enhanced by this masterwork. */ value: number; /** Is this a primary stat effect or secondary? Adept/crafted weapons can get a small +X to all stats; these are secondary */ isPrimary: boolean; }[]; } export interface DimCrafted { /** The level of this crafted weapon */ level: number; /** 0-1 progress to the next level */ progress: number; /** when this weapon was crafted, UTC epoch seconds timestamp */ craftedDate: number; /** the enhancement tier for this weapon, if enhanced. 0 otherwise. */ enhancementTier: number; } export interface DimCatalyst { /** Whether the weapon catalyst is completed */ complete: boolean; /** Whether the player has unlocked/discovered the catalyst */ unlocked: boolean; /** Progress the player has made on unlocking the catalyst */ objectives?: DestinyObjectiveProgress[]; } export interface DimStat { /** DestinyStatDefinition hash. */ statHash: number; /** Name, description, and icon for this stat. */ displayProperties: DestinyDisplayPropertiesDefinition; /** Sort order. */ sort: number; /** Absolute stat value. */ value: number; /** Base stat without bonus perks applied. Important in D2 for armor. */ base: number; /** * Base stat if it were masterworked. This allows useful comparison between eras of armor. * TODO: Get this right for weapons and un-optional it. */ baseMasterworked?: number; /** The maximum value this stat can have. */ maximumValue: number; /** Should this be displayed as a bar or just a number? */ bar: boolean; /** Most stats, bigger is better. Exceptions are things like Charge Time. */ smallerIsBetter: boolean; /** * Value of the investment stat, which may be different than the base stat. * This is really just a temporary value while building stats and shouldn't be used anywhere. */ investmentValue: number; /** * Does this stat add to a character-wide total, instead of just being a stat for that item? * This is true of armor stats. */ additive: boolean; } export interface D1Stat extends DimStat { bonus: number; scaled?: { max: number; min: number; }; split?: number; qualityPercentage?: { max: number; min: number; range: string; }; } export interface D1TalentGrid { /** Is the grid complete (leveled and unlocked)? */ complete: boolean; /** A flat list of nodes in the grid. */ nodes: D1GridNode[]; /** Have all nodes been unlocked via XP? */ xpComplete: boolean; /** How much XP in total to unlock the grid fully? */ totalXPRequired: number; /** Total XP earned in the grid. */ totalXP: number; /** Is there an "Ascend" action node? */ hasAscendNode: boolean; /** Has this item been ascended? */ ascended: boolean; /** Can this item be infused? */ infusable: boolean; } export interface D1GridNode { /** Localized name of the grid. */ name: string; /** Talent grid definition hash. */ hash: number; /** Localized description of the grid. */ description: string; /** Icon of the grid. */ icon: string; /** Column position in the grid. */ column: number; /** Row position in the grid. */ row: number; /** Is the node selected (lit up in the grid) */ activated: boolean; /** The item level at which this node can be unlocked */ activatedAtGridLevel: number; /** Only one node in this column can be selected (scopes, etc) */ exclusiveInColumn: boolean; /** Whether or not the material cost has been paid for the node */ unlocked: boolean; /** Some nodes don't show up in the grid, like purchased ascend nodes */ hidden: boolean; /** Is this an ornament node? */ ornament: boolean; /** How much XP has been put into this node? */ xp: number; /** How much XP is required to unlock the node? */ xpRequired: number; /** Has the XP requirement been met? */ xpRequirementMet: boolean; } /** an InventoryItem known to have a plug attribute, because this item is located in a socket */ export interface PluggableInventoryItemDefinition extends DestinyInventoryItemDefinition { plug: NonNullable; } /** Describes the conditions under which a plug stat is active */ export type PlugStatActivationRule = | /** always active */ undefined | { /** never active */ rule: 'never'; } | { /** only active for a specific class */ rule: 'classType'; classType: DestinyClass; } | { /** only active if the weapon is an adept weapon */ rule: 'adeptWeapon'; } | { /** only active if the weapon is masterworked */ rule: 'masterwork'; } | { /** Only active if the weapon is crafted and either adept or at level 20 */ rule: 'enhancedIntrinsic'; } | { /** New Armor 3.0 archetypes grant stats only to secondary stats when masterworked. */ rule: 'archetypeArmorMasterwork'; } | { /** Masterworked tier > 0 weapons get +tier to every stat */ rule: 'tieredWeaponMW'; }; /** * A single investment stat from a plug, together with an activity rule that * describes when the plug stat is active. * * The idea is that DestinyItemInvestmentStatDefinition[] is assignable to DimPlugInvestmentStat[]. * That way we can reuse the plug def's investmentStats array if it has no conditional stats or doesn't * require fixup in general, and map to our own format with a PlugStatActivationRule if need be */ export interface DimPlugInvestmentStat extends Omit< DestinyItemInvestmentStatDefinition, 'isConditionallyActive' > { activationRule?: PlugStatActivationRule; } /** * DIM's view of a "Plug" - an item that can go into a socket. * In D2, both perk grids and mods/shaders are sockets with plugs. */ export interface DimPlug { /** The InventoryItem definition associated with this plug. */ readonly plugDef: PluggableInventoryItemDefinition; /** Objectives associated with this plug, usually used to unlock it. */ readonly plugObjectives: DestinyObjectiveProgress[]; /** Is the plug enabled? For example, some perks only activate on certain planets. */ readonly enabled: boolean; /** If not enabled, this is the localized reasons why, as a single string. */ readonly enableFailReasons: string; /** * Stats this plug modifies. Only present for dimPlugs attached to an item. * If present, it's a map from the stat hash to the amount the stat is modified. */ readonly stats: { [statHash: number]: { value: number; investmentValue: number }; } | null; /** This plug is one of the random roll options but the current version of this item cannot roll this perk. */ readonly cannotCurrentlyRoll?: boolean; /** This plug is one of the collections perks and may not 100% roll */ readonly unreliablePerkOption?: boolean; } export interface DimPlugSet { /** The hash that links to a DestinyPlugSetDefinition. */ readonly hash: number; /** * A list of built DimPlugs that are found in the plugSet. * This plugs included encompass everything that can be plugged into the socket whether it * is available to the character or not. You should filter this list down based on the plugs * available to the profile/character. */ readonly plugs: DimPlug[]; /** * The cached empty plug item hash from this plugSet. You really * want to access DimSocket.emptyPlugItemHash instead! */ readonly precomputedEmptyPlugItemHash?: number; /** A precomputed list of plug hashes that can not roll on current versions of the item. */ readonly plugHashesThatCannotRoll: number[]; readonly plugHashesThatCanRoll: number[]; // "Why not just determine craftingData at the plug level?" you ask. // Well, we cache/de-dupe plugs on a per-hash basis, so the // DimPlug for Demolitionist should always be a reference the same object. // Additional metadata about Demolitionist, that's only applicable when // it's inside this plugSet, should live with the plugSet. // yes, the de-dupe thing is not strictly true, due to cannotCurrentlyRoll property... // but that's its own mess that needs cleanup. cannotCurrentlyRoll could definitely be // determined at runtime. there are *very* few places it's needed. // a good TO-DO for later. /** * If populated, this plugSet seems to belong to a crafted weapon. * * For rendering purposes, the child plugs in the owning socket's plugOptions * ought to share a little more about their material/level requirements. * * This property holds that metadata, keyed by plugHash. */ craftingData?: { [plugHash: number]: DestinyPlugItemCraftingRequirements | undefined }; } export interface DimSocket { /** The index of this socket in the overall socket list, used for the AWA InsertPlug API. */ socketIndex: number; /** The currently inserted plug item, if any. */ plugged: DimPlug | null; /** * If the "plugged" plug has been overridden by SocketOverrides, this captures * which plug option is actually still plugged on the item, even though we're * viewing an alternative. */ actuallyPlugged?: DimPlug; /** * The displayable/searchable list of potential plug choices for this socket. * For perks, this is all the potential perks in the perk column. * Otherwise, it'll just be the inserted plug for mods, shaders, etc. * Look at the plugSet and the socketDefinition's reusablePlugItems for * items that could fit into this socket. */ plugOptions: DimPlug[]; /** * A set of reusable plugs that can be placed in the socket. * The list of plugs included encompass everything that can be plugged into the socket * whether it is available to the character or not. You should filter this list down * based on the plugs available to the profile/character. */ plugSet?: DimPlugSet; /** * The plug item hash used to reset this plug to an empty default plug. * This is a heuristic improvement over singleInitialItemHash, but it's * entirely possible that this contains a value even when there isn't really * an empty plug. We do our best to leave this unset for sockets without * a meaningful empty plug (abilities, perks, intrinsics, ...), but this should * only be relied upon when you have a good reason to assume it exists. * If you rely on this, you can cheat and assume that this is always available -- * for blues, runtime info seems to be missing the empty shader entirely. */ emptyPlugItemHash?: number; /** Reusable plug items from runtime info, for the plug viewer. */ reusablePlugItems?: DestinyItemPlugBase[]; /** Does the socket contain randomized plug items? */ hasRandomizedPlugItems: boolean; /** * Is this socket a perk? This includes sockets marked Reusable, Unlockable, and LargePerk. * This might be widely synonymous with isReusable, but seems like it's being used for things other than display style logic. */ isPerk: boolean; /** * Is this socket a mod socket - these are displayed as squares with a border. */ isMod: boolean; /** Is this socket reusable? This is a notably different behavior and UI in Destiny, displayed in circles rather than squares. */ isReusable: boolean; /** Is this socket visible in-game? DIM mostly ignores this, but for some known sockets this controls item behavior / filter matching */ visibleInGame?: boolean; /** Deep information about this socket, including what types of things can be inserted into it. TODO: do we need all of this? */ socketDefinition: DestinyItemSocketEntryDefinition; } export interface DimSocketCategory { /** A grouping of sockets. */ category: DestinySocketCategoryDefinition; /** The indexes (from the original definitions) of sockets that belong to this group. */ socketIndexes: number[]; } export interface DimSockets { /** * A flat list of all sockets on the item. * Note that this list cannot be indexed by socketIndex - it must be *searched* by socketIndex, because some sockets have been removed. */ allSockets: DimSocket[]; /** Sockets grouped by category. */ categories: DimSocketCategory[]; /** Were these built from definitions, or from live data? */ fromDefinitions: boolean; } /** * If a pursuit can expire, this contains the relevant info. */ export interface DimPursuitExpiration { expirationDate: Date; suppressExpirationWhenObjectivesComplete: boolean; expiredInActivityMessage: string | undefined; } /** * If a pursuit belongs to a quest line, this tells us * at which point in the quest line this particular pursuit * is located. */ export interface DimQuestLine { questStepNum: number; questStepsTotal: number; description: string | undefined; } export interface DimPursuit { expiration: DimPursuitExpiration | undefined; rewards: DestinyItemQuantity[]; /** Modifiers active in this quest */ modifierHashes: number[]; questLine?: DimQuestLine; /** If this pursuit is really a Record (e.g. a seasonal challenge) */ recordHash?: number; trackedInGame?: boolean; } ================================================ FILE: src/app/inventory/locate-item.ts ================================================ import { EventBus } from 'app/utils/observable'; import { DimItem } from './item-types'; export const locateItem$ = new EventBus(); /** * Bring an item into view and briefly highlight it. Used to pick out a particular instance of an item in the inventory view. */ export function locateItem(item: DimItem) { locateItem$.next(item); } ================================================ FILE: src/app/inventory/manual-moves.ts ================================================ import { DimItem } from './item-types'; /** * A map from item index to the last time it was manually moved in this * session. We use this to avoid auto-moving things you just moved manually. * This could be in Redux, but it doesn't affect anything visually, so it's * a bit easier to just have it be a global. */ const moveTouchTimestamps = new Map(); export function updateManualMoveTimestamp(item: DimItem) { moveTouchTimestamps.set(item.index, Date.now()); } export function getLastManuallyMoved(item: DimItem) { return moveTouchTimestamps.get(item.index) ?? 0; } // TODO: do it for loadouts too! ================================================ FILE: src/app/inventory/move-item.ts ================================================ import { startSpan } from '@sentry/browser'; import { settingSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { ShowItemPickerFn } from 'app/item-picker/item-picker'; import { hideItemPopup } from 'app/item-popup/item-popup'; import { ThunkResult } from 'app/store/types'; import { CanceledError, neverCanceled, withCancel } from 'app/utils/cancel'; import { compareBy } from 'app/utils/comparators'; import { DimError } from 'app/utils/dim-error'; import { errorMessage } from 'app/utils/errors'; import { noop } from 'app/utils/functions'; import { itemCanBeEquippedBy } from 'app/utils/item-utils'; import { errorLog, infoLog } from 'app/utils/log'; import { PlatformErrorCodes } from 'bungie-api-ts/destiny2'; import { showNotification } from '../notifications/notifications'; import { loadingTracker } from '../shell/loading-tracker'; import { queueAction } from '../utils/action-queue'; import { reportException } from '../utils/sentry'; import { moveItemNotification } from './MoveNotifications'; import { updateCharacters } from './d2-stores'; import { InventoryBucket } from './inventory-buckets'; import { createMoveSession, executeMoveItem } from './item-move-service'; import { DimItem } from './item-types'; import { updateManualMoveTimestamp } from './manual-moves'; import { currentStoreSelector, storesSelector } from './selectors'; import { DimStore } from './store-types'; import { amountOfItem, getCurrentStore, getStore, getVault } from './stores-helpers'; const TAG = 'move'; /** * Move the item to the currently active store. Used for double-click action. */ export function moveItemToCurrentStore(item: DimItem, e?: React.MouseEvent): ThunkResult { return async (dispatch, getState) => { e?.stopPropagation(); const active = getCurrentStore(storesSelector(getState()))!; // Equip if it's not equipped or it's on another character const equip = !item.equipped || item.owner !== active.id; return dispatch(moveItemTo(item, active, itemCanBeEquippedBy(item, active) ? equip : false)); }; } /** * Show an item picker dialog, and then pull the selected item to the current store. */ export function pullItem( storeId: string, bucket: InventoryBucket, showItemPicker: ShowItemPickerFn, ): ThunkResult { return async (dispatch, getState) => { const store = getStore(storesSelector(getState()), storeId)!; const item = await showItemPicker({ filterItems: (item) => item.bucket.hash === bucket.hash && itemCanBeEquippedBy(item, store), prompt: t('MovePopup.PullItem', { bucket: bucket.name, store: store.name, }), }); if (item) { await dispatch(moveItemTo(item, store)); } }; } /** * Drop a dragged item. */ export function dropItem(item: DimItem, storeId: string, equip = false): ThunkResult { return async (dispatch, getState) => { const store = getStore(storesSelector(getState()), storeId)!; return dispatch(moveItemTo(item, store, equip, item.amount)); }; } /** * Move the item to the specified store. Equip it if equip is true. * This function needs to handle displaying any errors itself - it should not reject. */ export function moveItemTo( item: DimItem, store: DimStore, equip = false, amount: number = item.amount, ): ThunkResult { return async (dispatch, getState) => { const currentStore = currentStoreSelector(getState())!; const singleCharacterSetting = settingSelector('singleCharacter')(getState()); return startSpan({ name: 'moveItemTo' }, async () => { hideItemPopup(); if ( item.location.inPostmaster ? !item.canPullFromPostmaster : item.notransfer && item.owner !== store.id ) { // Show an error immediately showNotification( moveItemNotification(item, store, Promise.reject(new DimError('Help.CannotMove')), noop), ); return; } if (item.owner === store.id && !item.location.inPostmaster && item.equipped === equip) { // Nothing to do! return; } // In single character mode dropping something from the "vault" back on the "vault" shouldn't move anything if (singleCharacterSetting && store.isVault && item.owner !== currentStore.id) { return; } const moveAmount = amount || 1; const reload = item.equipped || equip; try { const stores = storesSelector(getState()); if ($featureFlags.debugMoves) { infoLog( TAG, 'User initiated move:', moveAmount, item.name, item.typeName, 'to', store.name, 'from', getStore(stores, item.owner)!.name, ); } // We mark this *first*, because otherwise things observing state (like farming) may not see this // in time. updateManualMoveTimestamp(item); const [cancelToken, cancel] = withCancel(); const moveSession = createMoveSession(cancelToken, [item]); const movePromise = queueAction(() => loadingTracker.addPromise( dispatch(executeMoveItem(item, store, { equip, amount: moveAmount }, moveSession)), ), ); showNotification(moveItemNotification(item, store, movePromise, cancel)); await movePromise; if (reload) { // TODO: only reload the character that changed? // Refresh light levels and such dispatch(updateCharacters()); } } catch (e) { if (e instanceof CanceledError) { return; } errorLog(TAG, 'error moving item', item.name, 'to', store.name, e); // Some errors aren't worth reporting if ( e instanceof DimError && (e.code === 'wrong-level' || e.code === 'no-space' || e.bungieErrorCode() === PlatformErrorCodes.DestinyCannotPerformActionAtThisLocation || e.bungieErrorCode() === PlatformErrorCodes.DestinyNoRoomInDestination || e.bungieErrorCode() === PlatformErrorCodes.DestinyItemNotFound) ) { // don't report } else { reportException('moveItem', e); } } }); }; } /** * Consolidate all copies of a stackable item into a single stack in store. */ export function consolidate(actionableItem: DimItem, store: DimStore): ThunkResult { return (dispatch, getState) => queueAction(() => loadingTracker.addPromise( (async () => { const stores = storesSelector(getState()); const characters = stores.filter((s) => !s.isVault); const vault = getVault(stores)!; const moveSession = createMoveSession(neverCanceled, [actionableItem]); try { for (const s of characters) { // First move everything into the vault const item = s.items.find( (i) => store.id !== i.owner && i.hash === actionableItem.hash && !i.location.inPostmaster, ); if (item) { const amount = amountOfItem(s, actionableItem); await dispatch(executeMoveItem(item, vault, { equip: false, amount }, moveSession)); } } // Then move from the vault to the character if (!store.isVault) { const vault = getVault(storesSelector(getState()))!; const item = vault.items.find( (i) => i.hash === actionableItem.hash && !i.location.inPostmaster, ); if (item) { const amount = amountOfItem(vault, actionableItem); await dispatch(executeMoveItem(item, store, { equip: false, amount }, moveSession)); } } const data = { name: actionableItem.name, store: store.name }; const message = store.isVault ? t('ItemMove.ToVault', data) : t('ItemMove.ToStore', data); showNotification({ type: 'success', title: t('ItemMove.Consolidate', data), body: message, }); } catch (e) { showNotification({ type: 'error', title: actionableItem.name, body: errorMessage(e) }); errorLog(TAG, 'error consolidating', actionableItem, e); } })(), ), ); } interface Move { source: DimStore; target: DimStore; amount: number; } /** * Distribute a stackable item evenly across characters. */ export function distribute(actionableItem: DimItem): ThunkResult { return (dispatch, getState) => queueAction(() => loadingTracker.addPromise( (async () => { // Sort vault to the end const stores = storesSelector(getState()).toSorted( compareBy((s) => (s.id === 'vault' ? 2 : 1)), ); const moveSession = createMoveSession(neverCanceled, [actionableItem]); let total = 0; const amounts = stores.map((store) => { const amount = amountOfItem(store, actionableItem); total += amount; return amount; }); const numTargets = stores.length - 1; // exclude the vault let remainder = total % numTargets; const targets = stores.map((_store, index) => { if (index >= numTargets) { return 0; // don't want any in the vault } const result = remainder > 0 ? Math.ceil(total / numTargets) : Math.floor(total / numTargets); remainder--; return result; }); const deltas = amounts.map((amount, i) => targets[i] - amount); const vaultMoves: Move[] = []; const targetMoves: Move[] = []; const vaultIndex = stores.length - 1; const vault = stores[vaultIndex]; for (const [index, delta] of deltas.entries()) { if (delta < 0 && index !== vaultIndex) { vaultMoves.push({ source: stores[index], target: vault, amount: -delta, }); } else if (delta > 0) { targetMoves.push({ source: vault, target: stores[index], amount: delta, }); } } // All moves to vault in parallel, then all moves to targets in parallel async function applyMoves(moves: Move[]) { for (const move of moves) { const item = move.source.items.find((i) => i.hash === actionableItem.hash)!; await dispatch( executeMoveItem( item, move.target, { equip: false, amount: move.amount }, moveSession, ), ); } } try { await applyMoves(vaultMoves); await applyMoves(targetMoves); showNotification({ type: 'success', title: t('ItemMove.Distributed', { name: actionableItem.name }), }); } catch (e) { showNotification({ type: 'error', title: actionableItem.name, body: errorMessage(e) }); errorLog(TAG, 'error distributing', actionableItem, e); } })(), ), ); } ================================================ FILE: src/app/inventory/note-hashtags.ts ================================================ import { compact, filterMap } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { maxBy } from 'es-toolkit'; import { ItemInfos } from './dim-item-info'; /** * An object that can collect usage of hashtags and return their most popular form. */ export class HashTagTracker { // { // '#pve': { // variants: { // '#PVE': 4, // '#pve': 2 // }, // count: 6 // } // } hashtagCollection: NodeJS.Dict<{ variants: NodeJS.Dict; count: number }> = {}; addHashtag(hashtag: string) { const lower = hashtag.toLowerCase(); this.hashtagCollection[lower] ??= { count: 0, variants: {} }; this.hashtagCollection[lower].count++; this.hashtagCollection[lower].variants[hashtag] ??= 0; this.hashtagCollection[lower].variants[hashtag]++; } /** * Gets the best "canonical" form of a hashtag to use for display. */ canonicalForm(hashtag: string): string { const lower = hashtag.toLowerCase(); const normalizedMeta = this.hashtagCollection[lower]; if (!normalizedMeta) { return hashtag; // No variants, return as is } return extractMostPopular(normalizedMeta)[0]; } allHashtags(): string[] { return Object.values(this.hashtagCollection) .map((m) => extractMostPopular(m!)) .sort(compareBy(([, count]) => -count)) .map(([variant]) => variant); } } function extractMostPopular(normalizedMeta: { variants: NodeJS.Dict; count: number; }): [canonicalForm: string, totalObserved: number] { const countsByVariant = Object.entries(normalizedMeta.variants); const mostPopularVariant = maxBy(countsByVariant, (v) => v[1]!)![0]; return [mostPopularVariant, normalizedMeta.count]; } /** * Collects all hashtags from all item notes. * * Orders by use count, de-dupes case-insensitive, and picks the most popular capitalization. */ export function collectHashtagsFromInfos(itemInfos: ItemInfos) { const hashtagTracker = new HashTagTracker(); for (const info of Object.values(itemInfos)) { const hashtags = getHashtagsFromString(info.notes); for (const h of hashtags) { hashtagTracker.addHashtag(h); } } return hashtagTracker.allHashtags(); } const hashtagRegex = /(^|[\s,])(#[\p{L}\p{N}\p{Private_Use}\p{Other_Symbol}_:-]+)/gu; export function getHashtagsFromString(...notes: (string | null | undefined)[]) { return notes.flatMap((note) => Array.from(note?.matchAll(hashtagRegex) ?? [], (m) => m[2])); } // TODO: am I really gonna need to write a parser again /** * Add notes to an existing note. This is hashtag-aware, so it will not add a duplicate hashtag. */ export function appendedToNote(originalNote: string | undefined, append: string) { const originalSegmented = segmentHashtags(originalNote); const newSegmented = segmentHashtags(append); const existingHashtags = new Set( filterMap(originalSegmented, (s) => (typeof s !== 'string' ? s.hashtag : undefined)), ); // Don't add hashtags that already exist again - remove them from the input const filteredAppendSegments = newSegmented.filter( (s) => typeof s === 'string' || !existingHashtags.has(s.hashtag), ); return compact([...originalSegmented, ' ', ...filteredAppendSegments]) .map((s) => (typeof s === 'string' ? s : s.hashtag)) .join('') .replaceAll(/(\s)+/g, '$1') .trim(); } const allHashtagsRegex = /^\s*(?:(?:^|[\s,])#[\p{L}\p{N}\p{Private_Use}\p{Other_Symbol}_:-]+\s*)+$/u; /** * Add notes to an existing note. This is hashtag-aware, so it will not remove * partial hashtags. */ export function removedFromNote(originalNote: string | undefined, removed: string) { if (!originalNote) { return undefined; } const originalSegmented = segmentHashtags(originalNote); // Treat it like a remove-hashtags operation and just remove all the named hashtags individually if (removed.match(allHashtagsRegex)) { const removeHashTags = new Set(getHashtagsFromString(removed)); return originalSegmented .filter((s) => typeof s === 'string' || !removeHashTags.has(s.hashtag)) .map((s) => (typeof s === 'string' ? s : s.hashtag)) .join('') .replaceAll(/(\s)+/g, '$1') .trim(); } // Otherwise subtract out the literal string const hashtagSpans = filterMap(originalSegmented, (s) => typeof s === 'string' ? undefined : [s.index, s.index + s.hashtag.length], ); return originalNote ?.replaceAll(removed.trim(), (original, index) => // Refuse to cut a tag in half hashtagSpans.some(([start, end]) => index > start && index < end) ? original : '', ) .replaceAll(/\s+/g, ' ') .trim(); } /** Break up a string into normal-string bits and hashtags */ function segmentHashtags( note: string | undefined, ): (string | { hashtag: string; index: number })[] { if (!note) { return []; } const result: (string | { hashtag: string; index: number })[] = []; let lastIndex = 0; let match: RegExpExecArray | null; while ((match = hashtagRegex.exec(note))) { const matchIndex = match.index + match[1].length; if (matchIndex > lastIndex) { const segment = note.substring(lastIndex, matchIndex); result.push(segment); } result.push({ hashtag: match[2], index: matchIndex }); lastIndex = matchIndex + match[2].length; } if (lastIndex < note.length) { result.push(note.substring(lastIndex, note.length)); } return result; } ================================================ FILE: src/app/inventory/notes-hashtags.test.ts ================================================ import { ItemInfos } from './dim-item-info'; import { appendedToNote, collectHashtagsFromInfos, getHashtagsFromString, removedFromNote, } from './note-hashtags'; test.each([ ['#foo #bar', ['#foo', '#bar']], ['#foo, #bar', ['#foo', '#bar']], ['This note has #foo tag and also#bar', ['#foo']], ['#foo#bar', ['#foo']], ['#foo,#bar', ['#foo', '#bar']], ['#foo-#bar', ['#foo-']], // Not great, could be better ['Emoji #🤯 tags', ['#🤯']], ])('getHashtagsFromString: %s', (notes, expectedTags) => { const tags = new Set(getHashtagsFromString(notes)); expect(tags).toEqual(new Set(expectedTags)); }); test('collectHashtagsFromInfos should get a unique set of hashtags from multiple notes', () => { const itemInfos: ItemInfos = { 1: { id: '1', notes: 'This has #three #Hash #tags' }, // A lowercase #three occurs first, 2: { id: '1', notes: '#Three #🤯' }, // but #Three should be preferred (two occurences) 3: { id: '1', notes: '#Three' }, 4: { id: '1', notes: '#Hash' }, 5: { id: '1', notes: '#hash' }, // A lowercase #hash occured most recently, but #Hash should be preferred (two occurences) }; expect(new Set(collectHashtagsFromInfos(itemInfos))).toEqual( new Set(['#Three', '#Hash', '#tags', '#🤯']), ); }); test.each([ [undefined, 'A note', 'A note'], ['A note', '#fancy', 'A note #fancy'], ['A #fancy note', '#fancy', 'A #fancy note'], ['#pve #s20', '#pve #stasis', '#pve #s20 #stasis'], ['#pve #s20', '#void #pve #stasis', '#pve #s20 #void #stasis'], ['My favorite! #pve #s20', '#pve #stasis', 'My favorite! #pve #s20 #stasis'], ['My favorite!\n#pve #s20', '#pve #stasis', 'My favorite!\n#pve #s20 #stasis'], ['My favorite!\n#pve #s20', '#pve\n#stasis', 'My favorite!\n#pve #s20\n#stasis'], ['#pve, #s20', '#pve #stasis', '#pve, #s20 #stasis'], ['#void', '#void #voidwalker #other', '#void #voidwalker #other'], ])('appendedToNote: %s + %s => %s', (original, appended, expected) => { expect(appendedToNote(original, appended)).toBe(expected); }); test.each([ ['A note', 'A note', ''], ['A note #fancy', '#fancy', 'A note'], ['A #fancy note', '#fancy', 'A note'], ['#voidwalker #void', '#void', '#voidwalker'], ['#voidwalker #void', 'void', '#voidwalker #void'], ['#voidwalker My #void gun', 'My #void gun', '#voidwalker'], ['#pve, #s20, #stasis', '#s20', '#pve, , #stasis'], ['Void void void arc', 'void', 'Void arc'], ])('removedFromNote: %s - %s => %s', (original, removed, expected) => { expect(removedFromNote(original, removed)).toBe(expected); }); ================================================ FILE: src/app/inventory/observers.ts ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { set } from 'app/storage/idb-keyval'; import { StoreObserver } from 'app/store/observerMiddleware'; import { errorLog } from 'app/utils/log'; import { debounce } from 'es-toolkit'; import { shallowEqual } from 'fast-equals'; interface SaveInfosObservedState { key: string | undefined; newItems: Set; } /** * Set up an observer on the store that'll save item infos to IndexedDB. */ export function createSaveItemInfosObserver(): StoreObserver { return { id: 'save-item-infos-observer', equals: shallowEqual, getObserved: (rootState) => { const account = currentAccountSelector(rootState); return { key: account && `newItems-m${account.membershipId}-d${account.destinyVersion}`, newItems: rootState.inventory.newItems, }; }, sideEffect: debounce(async ({ current }: { current: SaveInfosObservedState }) => { if (current.key) { try { return await set(current.key, current.newItems); } catch (e) { errorLog('new-items', "Couldn't save new items", e); } } }, 1000), }; } ================================================ FILE: src/app/inventory/reducer.ts ================================================ import { compareBy } from 'app/utils/comparators'; import { warnLog } from 'app/utils/log'; import { DestinyItemChangeResponse, DestinyItemComponent, DestinyProfileResponse, ItemLocation, } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { Draft, produce } from 'immer'; import { Reducer } from 'redux'; import { ActionType, getType } from 'typesafe-actions'; import { setCurrentAccount } from '../accounts/actions'; import type { AccountsAction } from '../accounts/reducer'; import * as actions from './actions'; import { DimItem } from './item-types'; import { AccountCurrency, DimStore } from './store-types'; import { ItemCreationContext, makeItem } from './store/d2-item-factory'; import { createItemIndex } from './store/item-index'; import { findItemsByBucket, getCurrentStore, getStore, getVault } from './stores-helpers'; const TAG = 'move'; // TODO: Should this be by account? Accounts need IDs export interface InventoryState { // The same stores as before - these are regenerated anew // when stores reload or change, so they're safe for now. // Updates to items need to deeply modify their store though. // TODO: ReadonlyArray> readonly stores: DimStore[]; /** * Account-wide currencies (glimmer, shards, etc.). Silver is only available * while the player is in game. */ readonly currencies: AccountCurrency[]; readonly live: boolean; readonly profileResponse?: DestinyProfileResponse; readonly profileError?: Error; /** * The inventoryItemIds of all items that are "new". */ readonly newItems: Set; readonly newItemsLoaded: boolean; /** * An API profile response. If this is present, * we use it instead of talking to the Bungie API. */ readonly mockProfileData?: DestinyProfileResponse; } export type InventoryAction = ActionType; const initialState: InventoryState = { stores: [], currencies: [], newItems: new Set(), newItemsLoaded: false, live: false, }; export const inventory: Reducer = ( state: InventoryState = initialState, action: InventoryAction | AccountsAction, ): InventoryState => { switch (action.type) { case getType(actions.profileLoaded): if ( action.payload.profile.responseMintedTimestamp <= (state.profileResponse?.responseMintedTimestamp ?? 0) ) { warnLog( 'd2-stores', 'Not updating profile, it is older than what we already have', action.payload.profile.responseMintedTimestamp, state.profileResponse?.responseMintedTimestamp, ); return state; } return { ...state, profileResponse: action.payload.profile, profileError: action.payload.live ? undefined : state.profileError, live: action.payload.live, }; case getType(actions.profileError): return { ...state, profileError: action.payload }; case getType(actions.update): if ( action.payload.responseMintedTimestamp && state.profileResponse && action.payload.responseMintedTimestamp !== state.profileResponse.responseMintedTimestamp ) { warnLog( 'd2-stores', 'Not updating inventory - the profile has changed from under us', action.payload.responseMintedTimestamp, state.profileResponse.responseMintedTimestamp, ); return state; } return updateInventory(state, action.payload); case getType(actions.charactersUpdated): return updateCharacters(state, action.payload); case getType(actions.itemMoved): { const { itemId, itemHash, itemLocation, sourceId, targetId, equip, amount } = action.payload; return produce(state, (draft) => itemMoved(draft, itemHash, itemId, itemLocation, sourceId, targetId, equip, amount), ); } case getType(actions.itemLockStateChanged): { const { item, state: lockState, type } = action.payload; return produce(state, (draft) => itemLockStateChanged(draft, item, lockState, type)); } case getType(actions.awaItemChanged): { const { changes, item, itemCreationContext } = action.payload; return produce(state, (draft) => awaItemChanged(draft, changes, item, itemCreationContext)); } case getType(actions.error): return { ...state, profileError: action.payload, }; // *** New items *** case getType(actions.setNewItems): return { ...state, newItems: action.payload, newItemsLoaded: true, }; case getType(actions.clearNewItem): if (state.newItems.has(action.payload)) { const newItems = new Set(state.newItems); newItems.delete(action.payload); return { ...state, newItems, }; } else { return state; } case getType(actions.clearAllNewItems): return { ...state, newItems: new Set(), }; case getType(setCurrentAccount): return initialState; case getType(actions.setMockProfileResponse): return { ...state, mockProfileData: action.payload, profileResponse: undefined, }; case getType(actions.clearStores): return { ...state, stores: [], }; default: return state; } }; function updateInventory( state: InventoryState, { stores, currencies, }: { stores: DimStore[]; currencies: AccountCurrency[]; }, ) { // TODO: we really want to decompose these, drive out all deep mutation // TODO: mark DimItem, DimStore properties as Readonly return { ...state, stores, currencies, newItems: computeNewItems(state.stores, state.newItems, stores), }; } /** * Merge in new top-level character info (stats, etc) */ function updateCharacters(state: InventoryState, characters: actions.CharacterInfo[]) { return { ...state, stores: state.stores.map((store) => { const character = characters.find((c) => c.characterId === store.id); if (!character) { return store; } const { characterId, ...characterInfo } = character; return { ...store, ...characterInfo, stats: { ...store.stats, ...characterInfo.stats, }, }; }), }; } /** Can an item be marked as new? */ const canBeNew = (item: DimItem) => item.equipment && item.instanced && item.bucket.hash !== BucketHashes.Subclass; /** * Given an old inventory, a new inventory, and all the items that were previously marked as new, * calculate the new set of new items. */ function computeNewItems(oldStores: DimStore[], oldNewItems: Set, newStores: DimStore[]) { if (oldStores === newStores) { return oldNewItems; } // Get the IDs of all old items const allOldItems = new Set(); for (const store of oldStores) { for (const item of store.items) { if (canBeNew(item)) { allOldItems.add(item.id); } } } // If we didn't have any items before, don't suddenly mark everything new if (!allOldItems.size) { return oldNewItems; } // Get the IDs of all new items const allNewItems = new Set(); for (const store of newStores) { for (const item of store.items) { if (canBeNew(item)) { allNewItems.add(item.id); } } } const newItems = new Set(); // Add all previous new items that are still in the new inventory for (const itemId of oldNewItems) { if (allNewItems.has(itemId)) { newItems.add(itemId); } } // Add all new items that aren't in old items for (const itemId of allNewItems) { if (!allOldItems.has(itemId)) { newItems.add(itemId); } } return setsEqual(newItems, oldNewItems) ? oldNewItems : newItems; } /** * Compute if two sets are equal by seeing that every item of each set is present in the other. */ function setsEqual(first: Set, second: Set) { if (first.size !== second.size) { return false; } let equal = true; for (const itemId of first) { if (!second.has(itemId)) { equal = false; break; } } if (equal) { for (const itemId of second) { if (!first.has(itemId)) { equal = false; break; } } } return equal; } /** * Update our item and store models after an item has been moved (or equipped/dequipped). */ function itemMoved( draft: Draft, itemHash: number, itemId: string, itemLocation: BucketHashes, sourceStoreId: string, targetStoreId: string, equip: boolean, amount: number, ): void { // Refresh all the items - they may have been reloaded! const stores = draft.stores; const source = getStore(stores, sourceStoreId); const target = getStore(stores, targetStoreId); if (!source || !target) { warnLog(TAG, 'Either source or target store not found', source, target); return; } let item = source.items.find( (i) => i.hash === itemHash && i.id === itemId && i.location.hash === itemLocation, )!; if (!item) { warnLog(TAG, 'Moved item not found', item); return; } // If we've moved to a new place if (source.id !== target.id || item.location.inPostmaster) { // We handle moving stackable and nonstackable items almost exactly the same! const stackable = item.maxStackSize > 1; // Items to be decremented const sourceItems = stackable ? // For stackables, pull from all the items as a pool findItemsByBucket(source, item.location.hash) .filter((i) => i.hash === item.hash && i.id === item.id) .sort(compareBy((i) => i.amount)) : // Otherwise we're moving the exact item we passed in [item]; // Items to be incremented. There's really only ever at most one of these, but // it's easier to deal with as a list. An empty list means we'll vivify a new item there. const targetItems = stackable ? findItemsByBucket(target, item.bucket.hash) .filter( (i) => i.hash === item.hash && i.id === item.id && // Don't consider full stacks as targets i.amount !== i.maxStackSize, ) .sort(compareBy((i) => i.amount)) : []; // moveAmount could be more than maxStackSize if there is more than one stack on a character! const moveAmount = amount || item.amount || 1; let addAmount = moveAmount; let removeAmount = moveAmount; let removedSourceItem = false; // Remove inventory from the source while (removeAmount > 0) { const sourceItem = sourceItems.shift(); if (!sourceItem) { warnLog(TAG, 'Source item missing', item); return; } const amountToRemove = Math.min(removeAmount, sourceItem.amount); sourceItem.amount -= amountToRemove; // Completely remove the source item if (sourceItem.amount <= 0 && removeItem(source, sourceItem)) { removedSourceItem = sourceItem.index === item.index; } removeAmount -= amountToRemove; } // Add inventory to the target (destination) let targetItem = item; while (addAmount > 0) { targetItem = targetItems.shift()!; if (!targetItem) { targetItem = item; if (!removedSourceItem) { // This assumes (as we shouldn't) that we have no nested mutable state in the item targetItem = { ...item }; targetItem.index = createItemIndex(targetItem); } removedSourceItem = false; // only move without cloning once targetItem.amount = 0; // We'll increment amount below if (targetItem.location.inPostmaster) { targetItem.location = targetItem.bucket; } addItem(target, targetItem); } const amountToAdd = Math.min(addAmount, targetItem.maxStackSize - targetItem.amount); targetItem.amount += amountToAdd; addAmount -= amountToAdd; } item = targetItem; // The item we're operating on switches to the last target } if (equip) { for (const i of target.items) { // Set equipped for all items in the bucket if (i.location.hash === item.bucket.hash) { i.equipped = i.index === item.index; } } } } function itemLockStateChanged( draft: Draft, item: DimItem, state: boolean, type: 'lock' | 'track', ) { const source = getStore(draft.stores, item.owner); if (!source) { warnLog(TAG, 'Store', item.owner, 'not found'); return; } // Only instanced items can be locked/tracked item = source.items.find((i) => i.id === item.id)!; if (!item) { warnLog(TAG, 'Item not found in stores', item); return; } if (type === 'lock') { item.locked = state; } else if (type === 'track') { item.tracked = state; } } /** * Handle the changes that come from messing with perks/sockets via AWA. The item * itself is recreated, while various currencies and tokens get consumed or created. */ function awaItemChanged( draft: Draft, changes: DestinyItemChangeResponse, item: DimItem | undefined, itemCreationContext: ItemCreationContext, ) { const { defs, buckets } = itemCreationContext; // Replace item if (!item) { warnLog('awaChange', 'No item produced from change'); return; } const owner = getStore(draft.stores, item.owner); if (!owner) { return; } const sourceIndex = owner.items.findIndex((i) => i.index === item.index); if (sourceIndex >= 0) { owner.items[sourceIndex] = item; } else { addItem(owner, item); } const getSource = (component: DestinyItemComponent) => { let realOwner = owner; if (component.location === ItemLocation.Vault) { // I don't think this can happen realOwner = getVault(draft.stores)! as Draft; } else { const itemDef = defs.InventoryItem.get(component.itemHash); if (itemDef.inventory && buckets.byHash[itemDef.inventory.bucketTypeHash].accountWide) { realOwner = getCurrentStore(draft.stores)!; } } return realOwner; }; // Remove items // TODO: Question - does the API just completely remove a stack and add a new stack, or does it just // say it deleted a stack representing the difference? for (const removedItemComponent of changes.removedInventoryItems) { // Currencies (glimmer, shards) are easy! const currency = draft.currencies.find((c) => c.itemHash === removedItemComponent.itemHash); if (currency) { currency.quantity = Math.max(0, currency.quantity - removedItemComponent.quantity); } else if (removedItemComponent.itemInstanceId) { for (const store of draft.stores) { const removedItemIndex = store.items.findIndex( (i) => i.id === removedItemComponent.itemInstanceId, ); if (removedItemIndex >= 0) { store.items.splice(removedItemIndex, 1); break; } } } else { // uninstanced (stacked, likely) item. const source = getSource(removedItemComponent); const sourceItems = source.items .filter((i) => i.hash === removedItemComponent.itemHash) .sort(compareBy((i) => i.amount)); // TODO: refactor! let removeAmount = removedItemComponent.quantity; // Remove inventory from the source while (removeAmount > 0) { const sourceItem = sourceItems.shift(); if (!sourceItem) { warnLog(TAG, 'Source item missing', item, removedItemComponent); return; } const amountToRemove = Math.min(removeAmount, sourceItem.amount); sourceItem.amount -= amountToRemove; if (sourceItem.amount <= 0) { // Completely remove the source item removeItem(source, sourceItem); } removeAmount -= amountToRemove; } } } // Add items for (const addedItemComponent of changes.addedInventoryItems) { // Currencies (glimmer, shards) are easy! const currency = draft.currencies.find((c) => c.itemHash === addedItemComponent.itemHash); if (currency) { const max = defs.InventoryItem.get(addedItemComponent.itemHash).inventory?.maxStackSize || Number.MAX_SAFE_INTEGER; currency.quantity = Math.min(max, currency.quantity + addedItemComponent.quantity); } else if (addedItemComponent.itemInstanceId) { const addedOwner = getSource(addedItemComponent); const addedItem = makeItem(itemCreationContext, addedItemComponent, addedOwner); if (addedItem) { addItem(addedOwner, addedItem); } } else { // Uninstanced (probably stacked) item const target = getSource(addedItemComponent); const targetItems = target.items .filter((i) => i.hash === addedItemComponent.itemHash) .sort(compareBy((i) => i.amount)); let addAmount = addedItemComponent.quantity; const addedItem = makeItem(itemCreationContext, addedItemComponent, target); if (!addedItem) { continue; } // TODO: refactor out "increment/decrement item amounts"? while (addAmount > 0) { let targetItem = targetItems.shift(); if (!targetItem) { targetItem = addedItem; targetItem.amount = 0; // We'll increment amount below addItem(target, targetItem); } const amountToAdd = Math.min(addAmount, targetItem.maxStackSize - targetItem.amount); targetItem.amount += amountToAdd; addAmount -= amountToAdd; } } } } // Remove an item from this store. Returns whether it actually removed anything. function removeItem(store: Draft, item: Draft) { // Completely remove the source item const sourceIndex = store.items.findIndex((i: DimItem) => item.index === i.index); if (sourceIndex >= 0) { store.items.splice(sourceIndex, 1); return true; } return false; } function addItem(store: Draft, item: Draft) { item.owner = store.id; // Originally this was just "store.items.push(item)" but it caused Immer to think we had circular references store.items = [...store.items, item]; } ================================================ FILE: src/app/inventory/rewards.ts ================================================ import type { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { getSeasonPassStatus } from 'app/progress/SeasonalRank'; import { sumBy } from 'app/utils/collections'; import { errorLog } from 'app/utils/log'; import { useCurrentSeasonInfo } from 'app/utils/seasons'; import type { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import type { DimItem } from './item-types'; import type { DimStore } from './store-types'; // Temporary solution. Might need to be created with d2ai? // These bonuses are hard to programatically identify, they're just dummies. const activityScoreBoosts = new Set([3858293505, 3858293504, 3858293507, 3858293506, 3858293509]); const maxPowerLevel = 550; // Not expected to change, but should be moved to the constants file or pulled from d2ai. // Per https://redd.it/1m5obsu, thanks u/Testifye, Reward Multiplier is based on your Gear's Power, // multiplied by a combination of its Newness/Featuredness and Season Pass bonuses. // 10,000 * C * ( ( G + A + 1 ) * ( ( P - 90 ) * ( 9 / 460 ) + 1 ) ) export function useRewardMultiplier( defs: D2ManifestDefinitions, profileInfo: DestinyProfileResponse | undefined, gear: DimStore | DimItem[], ) { // Activity score boosts come from the season pass const { season, seasonPass } = useCurrentSeasonInfo(defs, profileInfo); if (!profileInfo) { return null; } if (!season || !seasonPass) { errorLog('RewardMultiplier', `getRewardMultiplier called with no season/pass available?`); return null; } const equippedGear = 'id' in gear ? gear?.items.filter((i) => i.equipped && (i.bucket.inWeapons || i.bucket.inArmor)) : gear; if (equippedGear.length !== 8) { errorLog('RewardMultiplier', `getRewardMultiplier called with ${equippedGear.length} items`); return null; } // Treat gearMultiplier as a whole number percentage first for math precision. 10 = 10% // 1% for each featured item let gearMultiplier = equippedGear.filter((i) => i.featured).length; // 2% additional bonus for using all featured gear. if (gearMultiplier === 8) { gearMultiplier = 10; } const { seasonPassLevel, seasonProgressionDef } = getSeasonPassStatus( defs, profileInfo, seasonPass, season, ); const activityUnlocks = seasonProgressionDef.rewardItems.filter( (i) => i.rewardedAtProgressionLevel <= seasonPassLevel && activityScoreBoosts.has(i.itemHash), ).length; // There are 5 collectible bonuses, each contributing 3% gearMultiplier += activityUnlocks * 3; // Convert this into a multipliable ratio number. 25% becomes 1.25 gearMultiplier = 1 + 0.01 * gearMultiplier; // The power multiplier, starting at 0, can reach 9x, leading to the 9 numerator here. // Power level gains, from PL90 to PL550, affect the bonus linearly, so there are 460 increments (460 (550-90) denominator). const equippedPowerLevel = Math.floor(sumBy(equippedGear, (i) => i.power) / 8); const powerBeyond90 = Math.max(equippedPowerLevel - 90, 0); const powerMultiplier = 1 + powerBeyond90 * (9 / (maxPowerLevel - 90)); return powerMultiplier * gearMultiplier; } ================================================ FILE: src/app/inventory/selectors.ts ================================================ import { ItemHashTag, LoadoutParameters } from '@destinyitemmanager/dim-api-types'; import { destinyVersionSelector } from 'app/accounts/selectors'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { currentProfileSelector, customStatsSelector, settingsSelector, } from 'app/dim-api/selectors'; import { tuningSocketReusablePlugSetHash } from 'app/loadout-builder/types'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { createCollectibleFinder } from 'app/records/collectible-matching'; import { filterUnlockedPlugs } from 'app/records/plugset-helpers'; import { RootState } from 'app/store/types'; import { emptyArray, emptyObject, emptySet } from 'app/utils/empty'; import { currySelector } from 'app/utils/selectors'; import { DestinyItemPlug, DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import { D2CalculatedSeason } from 'data/d2/d2-season-info'; import { BucketHashes, ItemCategoryHashes, TraitHashes } from 'data/d2/generated-enums'; import { createSelector } from 'reselect'; import { getBuckets as getBucketsD1 } from '../destiny1/d1-buckets'; import { getBuckets as getBucketsD2 } from '../destiny2/d2-buckets'; import { characterSortImportanceSelector, characterSortSelector } from '../settings/character-sort'; import { ItemInfos, getNotes, getTag } from './dim-item-info'; import { DimItem } from './item-types'; import { collectHashtagsFromInfos } from './note-hashtags'; import { AccountCurrency } from './store-types'; import { ItemCreationContext } from './store/d2-item-factory'; import { getCurrentStore, getVault } from './stores-helpers'; /** All stores, unsorted. */ export const storesSelector = (state: RootState) => state.inventory.stores; export const d2BucketsSelector = createSelector( (state: RootState) => state.manifest.d2Manifest, (d2Manifest) => d2Manifest && getBucketsD2(d2Manifest), ); export const d1BucketsSelector = createSelector( (state: RootState) => state.manifest.d1Manifest, (d1Manifest) => d1Manifest && getBucketsD1(d1Manifest), ); export const bucketsSelector = createSelector( destinyVersionSelector, d1BucketsSelector, d2BucketsSelector, (destinyVersion, d1Buckets, d2Buckets) => (destinyVersion === 2 ? d2Buckets : d1Buckets), ); /** Bucket hashes for buckets that we actually show on the inventory page. */ export const displayableBucketHashesSelector = createSelector(bucketsSelector, (buckets) => buckets ? new Set(Object.values(buckets.byCategory).flatMap((buckets) => buckets.map((b) => b.hash))) : emptySet(), ); /** All stores, sorted according to user preference. */ export const sortedStoresSelector = createSelector( storesSelector, characterSortSelector, (stores, sortStores) => sortStores(stores), ); /** Sorted by "importance" which handles reversed sorting a bit better - for menus only */ export const storesSortedByImportanceSelector = createSelector( characterSortImportanceSelector, storesSelector, (sort, stores) => sort(stores), ); /** * Get a flat list of all items. */ export const allItemsSelector = createSelector(storesSelector, (stores) => stores.flatMap((s) => s.items), ); /** Have stores been loaded? */ export const storesLoadedSelector = (state: RootState) => storesSelector(state).length > 0; /** The current (last played) character */ export const currentStoreSelector = (state: RootState) => getCurrentStore(storesSelector(state)); export const singleStoreSelector = currySelector( createSelector( storesSelector, (_state: RootState, storeId: string) => storeId, (stores, storeId) => stores.find((s) => s.id === storeId), ), ); /** All items equipped to a specific DimStore */ export const equippedItemsSelector = currySelector( createSelector( (state: RootState, storeId: string) => singleStoreSelector(storeId)(state), (store) => store?.items.filter((i) => i.equipped) ?? emptyArray(), ), ); /** The vault */ export const vaultSelector = (state: RootState) => getVault(storesSelector(state)); /** The inventoryItemIds of all items that are "new". */ export const newItemsSelector = (state: RootState) => state.inventory.newItems; export const isNewSelector = (item: DimItem) => (state: RootState) => settingsSelector(state).showNewItems ? newItemsSelector(state).has(item.id) : false; const visibleCurrencies = [ 3159615086, // Glimmer 2817410917, // Bright Dust 3147280338, // Silver 2534352370, // Legendary Marks (D1) 2749350776, // Silver (D1) ]; /** Account wide currencies */ export const currenciesSelector = createSelector( (state: RootState) => state.inventory.currencies, (currencies) => currencies.filter((c) => visibleCurrencies.includes(c.itemHash)), ); const transmogCurrencies = [ 1583786617, // InventoryItem "Synthweave Template" 4238733045, // InventoryItem "Synthweave Plate" 1498161294, // InventoryItem "Synthweave Bolt" 4019412287, // InventoryItem "Synthweave Strap" ]; /** Synthweave {Template, Bolt, Plate, Strap} currencies */ export const transmogCurrenciesSelector = createSelector( (state: RootState) => state.inventory.currencies, (currencies) => currencies.filter((c) => transmogCurrencies.includes(c.itemHash)), ); const upgradeCurrencies = [ 2718300701, // Unstable Cores ]; /** Mostly just unstable cores right now */ export const upgradeCurrenciesSelector = createSelector( (state: RootState) => state.inventory.currencies, (currencies) => currencies.filter((c) => upgradeCurrencies.includes(c.itemHash)), ); /** Vendor engrams you can decrypt at a vendor or use for item focusing */ export const vendorCurrencyEngramsSelector = createSelector( d2ManifestSelector, (state: RootState) => state.inventory.currencies, (defs, currencies) => { if (!defs) { return emptyArray(); } // silver has no stackUniqueLabel return currencies.filter((curr) => defs.InventoryItem.get(curr.itemHash).inventory!.stackUniqueLabel?.match( /virtual_engram|\.virtual$/, ), ); }, ); const materialsWithMissingICH = [ 3702027555, // InventoryItem "Spoils of Conquest" ]; /** materials/currencies that aren't top level stuff */ export const materialsSelector = createSelector(allItemsSelector, (allItems) => allItems.filter( (i) => i.traitHashes?.includes(TraitHashes.ItemCurrency) || i.itemCategoryHashes?.includes(ItemCategoryHashes.Materials) || materialsWithMissingICH.includes(i.hash), ), ); /** The actual raw profile response from the Bungie.net profile API */ export const profileResponseSelector = (state: RootState) => state.inventory.mockProfileData ?? state.inventory.profileResponse; /** Whether or not the user is currently playing Destiny 2 */ const userIsPlayingSelector = (state: RootState) => Boolean( // the user's playing if their transitory component acts like they're in-game state.inventory.profileResponse?.profileTransitoryData?.data || // or, as a grace period for character swaps, if they've been playing in the last 10 minutes Date.now() - Date.parse(state.inventory.profileResponse?.profile.data?.dateLastPlayed || '0') < 10 * 60 * 1000, ); /** The time when the currently displayed profile was last refreshed from live game data */ export const profileMintedSelector = createSelector( profileResponseSelector, (profileResponse) => new Date(profileResponse?.responseMintedTimestamp ?? 0), ); export const profileErrorSelector = (state: RootState) => state.inventory.profileError; /** A variant of profileErrorSelector which returns undefined if we still have a valid profile to use despite the error. */ export const blockingProfileErrorSelector = (state: RootState) => currentStoreSelector(state) ? undefined : state.inventory.profileError; /** Whether DIM will automatically refresh on a schedule */ export const autoRefreshEnabledSelector = (state: RootState) => userIsPlayingSelector(state) && state.dimApi.globalSettings.autoRefresh; /** * All the dependencies for item creation. Don't use this before profile is loaded... */ export const createItemContextSelector = createSelector( d2ManifestSelector, profileResponseSelector, bucketsSelector, customStatsSelector, (defs, profileResponse, buckets, customStats): ItemCreationContext => ({ defs: defs!, buckets: buckets!, profileResponse: profileResponse!, customStats, }), ); const STORE_SPECIFIC_OWNERSHIP_BUCKETS = [ // Emblems cannot be transferred between characters and if one character owns an emblem, // other characters don't really own it. Also affects vendor claimability. BucketHashes.Emblems, // Quests and bounties are character-specific. BucketHashes.Quests, ]; /** * Sets of items considered "owned" for checkmark purposes, some * account-scoped, some character-scoped. * * Most items are considered owned from the view of a character if * they're in any bucket because they can be transferred or are in * the consumables bucket, but for quests and bounties, it's necessary * to see whether the current character has them. */ export interface OwnedItemsInfo { accountWideOwned: Set; storeSpecificOwned: { [key: string]: Set; }; } /** * Sets containing all the hashes of owned items, globally and from the * view of individual characters. Excludes plugs, see * ownedUncollectiblePlugsSelector for those. */ export const ownedItemsSelector = createSelector(allItemsSelector, (allItems) => { const accountWideOwned = new Set(); const storeSpecificOwned: { [owner: string]: Set } = {}; for (const item of allItems) { if (STORE_SPECIFIC_OWNERSHIP_BUCKETS.includes(item.bucket.hash)) { if (!storeSpecificOwned[item.owner]) { storeSpecificOwned[item.owner] = new Set(); } storeSpecificOwned[item.owner].add(item.hash); } else { accountWideOwned.add(item.hash); } } return { accountWideOwned, storeSpecificOwned }; }); /** * Sets containing all the hashes of owned uncollectible plug items, * e.g. emotes and ghost projections. These plug items do not appear * in collections, so we use plug availability from the profile response * to mark them as "owned". Plugs where the associated item has a * collectibleHash will never be included. */ export const ownedUncollectiblePlugsSelector = createSelector( d2ManifestSelector, profileResponseSelector, (defs, profileResponse) => { const accountWideOwned = new Set(); const storeSpecificOwned: { [storeId: string]: Set } = {}; if (defs && profileResponse) { const collectibleFinder = createCollectibleFinder(defs); const processPlugSet = ( plugs: { [key: number]: DestinyItemPlug[] }, insertInto: Set, ) => { for (const [plugSetHash_, plugSet] of Object.entries(plugs)) { const plugSetHash = parseInt(plugSetHash_, 10); filterUnlockedPlugs(plugSetHash, plugSet, insertInto, (plug) => { const def = defs.InventoryItem.get(plug.plugItemHash); return !def || !collectibleFinder(def); }); } }; if (profileResponse.profilePlugSets?.data) { processPlugSet(profileResponse.profilePlugSets.data.plugs, accountWideOwned); } if (profileResponse.characterPlugSets?.data) { for (const [storeId, plugSetData] of Object.entries( profileResponse.characterPlugSets.data, )) { if (!storeSpecificOwned[storeId]) { storeSpecificOwned[storeId] = new Set(); } processPlugSet(plugSetData.plugs, storeSpecificOwned[storeId]); } } } return { accountWideOwned, storeSpecificOwned }; }, ); /** A set containing all the hashes of unlocked PlugSet items (mods, shaders, ornaments, etc) for the given character. */ // TODO: reconcile with other owned/unlocked selectors export const unlockedPlugSetItemsSelector = currySelector( createSelector( (_state: RootState, characterId?: string) => characterId, profileResponseSelector, d2ManifestSelector, gatherUnlockedPlugSetItems, ), ); function gatherUnlockedPlugSetItems( characterId: string | undefined, profileResponse: DestinyProfileResponse | undefined, defs: D2ManifestDefinitions | undefined, ) { const unlockedPlugs = new Set(); if (profileResponse?.profilePlugSets.data?.plugs) { for (const plugSetHashStr in profileResponse.profilePlugSets.data.plugs) { const plugSetHash = parseInt(plugSetHashStr, 10); const plugs = profileResponse.profilePlugSets.data.plugs[plugSetHash]; filterUnlockedPlugs(plugSetHash, plugs, unlockedPlugs); } } if (characterId && profileResponse?.characterPlugSets.data?.[characterId]?.plugs) { for (const plugSetHashStr in profileResponse.characterPlugSets.data[characterId].plugs) { const plugSetHash = parseInt(plugSetHashStr, 10); const plugs = profileResponse.characterPlugSets.data[characterId].plugs[plugSetHash]; filterUnlockedPlugs(plugSetHash, plugs, unlockedPlugs); } } // Manually add all the tuning mods since they don't get unlocked on the // profile, they just show up on items with the tuning socket. // // TODO: We could filter these down by looking at all the user's items to see // which ones are available, though that would make this selector depend on // allItemsSelector. const tuningPlugSet = defs?.PlugSet.get(tuningSocketReusablePlugSetHash); if (tuningPlugSet) { for (const plugEntry of tuningPlugSet.reusablePlugItems) { unlockedPlugs.add(plugEntry.plugItemHash); } } return unlockedPlugs; } /** gets all the dynamic strings from a profile response */ export const dynamicStringsSelector = createSelector(profileResponseSelector, (profileResp) => { if (profileResp) { const { profileStringVariables, characterStringVariables } = profileResp; const allProfile: { // are these keys really strings? no. are they numbers? yes. but are all keys strings in js? yes // and are they being extracted from strings and not worth converting to numbers just to convert back to strings? yes [valueHash: string]: number; } = profileStringVariables?.data?.integerValuesByHash ?? {}; const byCharacter: { [charId: string]: { [valueHash: string]: number; }; } = {}; for (const charId in characterStringVariables?.data) { byCharacter[charId] = characterStringVariables.data?.[charId].integerValuesByHash ?? {}; } return { allProfile, byCharacter, }; } }); /** A flat list of all currently active artifact unlocks. */ export const artifactUnlocksSelector = currySelector( createSelector( profileResponseSelector, (_state: RootState, characterId: string) => characterId, (profileResponse: DestinyProfileResponse | undefined, characterId: string) => profileResponse && getArtifactUnlocks(profileResponse, characterId), ), ); /** A flat list of all currently active artifact unlocks. */ function getArtifactUnlocks( profileResponse: DestinyProfileResponse, characterId: string, ): LoadoutParameters['artifactUnlocks'] { // Lots of optional chaining because apparently this can be missing sometimes? const artifactData = profileResponse?.characterProgressions.data?.[characterId]?.seasonalArtifact; if (!artifactData?.tiers) { return undefined; } const unlockedItemHashes = artifactData.tiers ?.flatMap((tier) => tier.items) .filter((item) => item.isVisible && item.isActive) .map((item) => item.itemHash) || []; return { unlockedItemHashes, seasonNumber: D2CalculatedSeason, }; } /** Item infos (tags/notes) */ export const itemInfosSelector = (state: RootState): ItemInfos => currentProfileSelector(state)?.tags || emptyObject(); /** * DIM tags which should be applied to matching item hashes (instead of per-instance) */ const itemHashTagsSelector = (state: RootState): { [itemHash: string]: ItemHashTag } => state.dimApi.itemHashTags; /* Returns a function that can be used to get the tag for a particular item. */ export const getTagSelector = createSelector( itemInfosSelector, itemHashTagsSelector, (itemInfos, itemHashTags) => (item: DimItem) => getTag(item, itemInfos, itemHashTags), ); /* Returns a function that can be used to get the notes for a particular item. */ export const getNotesSelector = createSelector( itemInfosSelector, itemHashTagsSelector, (itemInfos, itemHashTags) => (item: DimItem) => getNotes(item, itemInfos, itemHashTags), ); /** Get a specific item's tag */ export const tagSelector = (item: DimItem) => (state: RootState) => getTagSelector(state)(item); /** Get a specific item's notes */ export const notesSelector = (item: DimItem) => (state: RootState) => getNotesSelector(state)(item); /** * all hashtags used in existing item notes, with (case-insensitive) dupes removed */ export const allNotesHashtagsSelector = createSelector(itemInfosSelector, collectHashtagsFromInfos); ================================================ FILE: src/app/inventory/spreadsheets.ts ================================================ import { CustomStatDef, DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { currentAccountSelector } from 'app/accounts/selectors'; import { customStatsSelector, languageSelector } from 'app/dim-api/selectors'; import { maxLength } from 'app/item-popup/NotesArea'; import { LoadoutsByItem, loadoutsByItemSelector } from 'app/loadout/selectors'; import { buildStatInfo, getColumns } from 'app/organizer/Columns'; import { SpreadsheetContext } from 'app/organizer/table-types'; import { D1_StatHashes } from 'app/search/d1-known-values'; import { TOTAL_STAT_HASH } from 'app/search/d2-known-values'; import { ThunkResult } from 'app/store/types'; import { filterMap } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { CsvRow, downloadCsv } from 'app/utils/csv'; import { DimError } from 'app/utils/dim-error'; import { localizedSorter } from 'app/utils/intl'; import { isKillTrackerSocket } from 'app/utils/item-utils'; import { getDisplayedItemSockets, getSocketsByIndexes } from 'app/utils/socket-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { BucketHashes, StatHashes } from 'data/d2/generated-enums'; import Papa from 'papaparse'; import { setItemNote, setItemTagsBulk } from './actions'; import { TagValue, tagConfig } from './dim-item-info'; import { D1GridNode, DimItem } from './item-types'; import { getNotesSelector, getTagSelector, storesSelector } from './selectors'; import { DimStore } from './store-types'; function getClass(type: DestinyClass) { switch (type) { case DestinyClass.Titan: return 'Titan'; case DestinyClass.Hunter: return 'Hunter'; case DestinyClass.Warlock: return 'Warlock'; case DestinyClass.Unknown: return 'Unknown'; case DestinyClass.Classified: return 'Classified'; } } const D1_FILTERED_NODE_HASHES = [ 1920788875, // Ascend 1270552711, // Infuse 2133116599, // Deactivate Chroma 643689081, // Kinetic Damage 472357138, // Void Damage 1975859941, // Solar Damage 2688431654, // Arc Damage 1034209669, // Increase Intellect 1263323987, // Increase Discipline 913963685, // Reforge Shell 193091484, // Increase Strength 217480046, // Twist Fate 191086989, // Reforge Artifact 2086308543, // Upgrade Defense 4044819214, // The Life Exotic ]; /** Stat names are not localized in CSV. We use a map to preserve order. */ export const csvStatNamesForDestinyVersion = (destinyVersion: DestinyVersion) => new Map([ [StatHashes.RecoilDirection, 'Recoil'], [StatHashes.AimAssistance, 'AA'], [StatHashes.Impact, 'Impact'], [StatHashes.Range, 'Range'], [StatHashes.Zoom, 'Zoom'], [StatHashes.BlastRadius, 'Blast Radius'], [StatHashes.Velocity, 'Velocity'], [StatHashes.Persistence, 'Persistence'], [StatHashes.Stability, 'Stability'], [StatHashes.RoundsPerMinute, 'ROF'], [StatHashes.ReloadSpeed, 'Reload'], [StatHashes.AmmoCapacity, 'Mag'], [StatHashes.Magazine, 'Mag'], [StatHashes.Handling, destinyVersion === 2 ? 'Handling' : 'Equip'], [StatHashes.ChargeTime, 'Charge Time'], [StatHashes.DrawTime, 'Draw Time'], [StatHashes.Accuracy, 'Accuracy'], [StatHashes.ChargeRate, 'Charge Rate'], [StatHashes.GuardResistance, 'Guard Resistance'], [StatHashes.GuardEndurance, 'Guard Endurance'], [StatHashes.SwingSpeed, 'Swing Speed'], [StatHashes.ShieldDuration, 'Shield Duration'], [StatHashes.AirborneEffectiveness, 'Airborne Effectiveness'], [StatHashes.AmmoGeneration, 'Ammo Generation'], [StatHashes.HeatGenerated, 'Heat Generated'], [StatHashes.CoolingEfficiency, 'Cooling Efficiency'], [StatHashes.Weapons, destinyVersion === 2 ? 'Weapons' : 'Mobility'], [StatHashes.Health, destinyVersion === 2 ? 'Health' : 'Resilience'], [StatHashes.Class, destinyVersion === 2 ? 'Class' : 'Recovery'], [StatHashes.Grenade, destinyVersion === 2 ? 'Grenade' : 'Disc'], [StatHashes.Super, destinyVersion === 2 ? 'Super' : 'Int'], [StatHashes.Melee, destinyVersion === 2 ? 'Melee' : 'Str'], [TOTAL_STAT_HASH, 'Total'], ]); export function generateCSVExportData( type: 'weapon' | 'armor' | 'ghost', stores: DimStore[], getTag: (item: DimItem) => TagValue | undefined, getNotes: (item: DimItem) => string | undefined, loadoutsByItem: LoadoutsByItem, customStats: CustomStatDef[], ) { const storeNamesById: { [storeId: string]: string } = {}; let allItems: DimItem[] = []; for (const store of stores) { allItems = allItems.concat(store.items); storeNamesById[store.id] = store.id === 'vault' ? 'Vault' : `${getClass(store.classType)}(${store.powerLevel})`; } allItems.sort(compareBy((item) => item.index)); let items: DimItem[] = []; if (type === 'weapon') { items = allItems.filter( (item) => // Checking the primary stat filters out some quest items item.primaryStat && (item.primaryStat?.statHash === D1_StatHashes.Attack || item.primaryStat?.statHash === StatHashes.Attack), ); } else if (type === 'armor') { items = allItems.filter( // Checking the primary stat filters out festival masks (item) => item.primaryStat && item.primaryStat?.statHash === StatHashes.Defense, ); } else if (type === 'ghost') { items = allItems.filter((item) => item.bucket.hash === BucketHashes.Ghost); } const statHashes = buildStatInfo(items); const destinyVersion = items[0]?.destinyVersion ?? 2; // Use the column definitions from Organizer to drive the CSV output const columns = getColumns( 'spreadsheet', type, statHashes, getTag, getNotes, () => undefined /* wishList */, false /* hasWishList */, customStats, loadoutsByItem, new Set() /* newItems */, destinyVersion /* destinyVersion */, ); // The order of the spreadsheet columns differs from the Organizer order. // PapaParse determines column order from the insertion order of data into the // first object that's returned, so we need to iterate the columns in this // order. const statNames = csvStatNamesForDestinyVersion(destinyVersion); const order = [ 'name', 'hash', 'id', 'tag', 'tier', // rarity 'itemTier', 'Type', 'source', 'Equippable', 'Category', 'dmg', 'ammo', 'power', 'energy', 'archetype', 'tertiary', 'tuning', 'masterworkStat', 'masterworkTier', 'location', 'locked', 'Equipped', 'featured', 'holofoil', 'year', 'season', 'event', ...[...statNames.keys()].map((statHash) => `stat${statHash}`), ...[...statNames.keys()].map((statHash) => `base${statHash}`), 'crafted', 'level', 'killTracker', 'foundry', 'modslot', 'loadouts', 'notes', // then perks ]; columns.sort( compareBy((c) => { if (c.id === 'perks') { // perks are always last return 2000; } const index = order.indexOf(c.id); if (index < 0) { // A new column was added and we need to add it to the order above throw new Error(`missing-column-order ${c.id}`); } return index; }), ); const context: SpreadsheetContext = { storeNamesById }; const data = items.map((item) => { const row: CsvRow = {}; for (const column of columns) { const value = column.value(item); if (typeof column.csv === 'string') { row[column.csv] ||= value; } else if (column.csv) { const values = column.csv(value, item, context); if (!values) { continue; } const [key, csvValue] = values; row[key] ||= csvValue; } else { // Column has no CSV representation - either remove the column in spreadsheet mode or add a 'csv' or 'csvVal' property throw new Error(`missing-csv: ${column.id}`); } } return row; }); return data; } export function downloadCsvFiles(type: 'weapon' | 'armor' | 'ghost'): ThunkResult { return async (_dispatch, getState) => { const stores = storesSelector(getState()); const getTag = getTagSelector(getState()); const getNotes = getNotesSelector(getState()); const loadoutsForItem = loadoutsByItemSelector(getState()); const customStats = customStatsSelector(getState()); const language = languageSelector(getState()); // perhaps we're loading if (stores.length === 0) { return; } const data = generateCSVExportData( type, stores, getTag, getNotes, loadoutsForItem, customStats, ); data.sort(localizedSorter(language, (r) => (r as { Name: string }).Name)); downloadCsv(`destiny-${type}`, data, { unpackArrays: ['Perks'], }); }; } interface CSVRow { Loadouts: string; Notes: string; Tag: string; Hash: string; Id: string; } export function importTagsNotesFromCsv(files: File[]): ThunkResult { return async (dispatch, getState) => { const account = currentAccountSelector(getState()); if (!account) { return; } let total = 0; for (const file of files) { const results = await new Promise>((resolve, reject) => Papa.parse(file, { header: true, complete: resolve, error: reject, }), ); if ( results.errors?.length && !results.errors.every((e) => e.code === 'TooManyFields' || e.code === 'TooFewFields') ) { throw new Error(results.errors[0].message); } const contents = results.data; if (!contents?.length) { throw new DimError('Csv.EmptyFile'); } const row = contents[0]; if (!('Id' in row) || !('Hash' in row) || !('Tag' in row) || !('Notes' in row)) { throw new DimError('Csv.WrongFields'); } dispatch( setItemTagsBulk( filterMap(contents, (row) => { if ('Id' in row && 'Hash' in row) { row.Tag = row.Tag.toLowerCase(); row.Id = row.Id.replace(/"/g, ''); // strip quotes from row.Id return { tag: row.Tag in tagConfig ? tagConfig[row.Tag as TagValue].type : undefined, itemId: row.Id, }; } }), ), ); for (const row of contents) { if ('Id' in row && 'Hash' in row) { row.Tag = row.Tag.toLowerCase(); row.Id = row.Id.replace(/"/g, ''); // strip quotes from row.Id dispatch( setItemNote({ note: row.Notes.substring(0, maxLength), itemId: row.Id, }), ); } } total += contents.length; } return total; }; } export function buildSocketNames(item: DimItem): string[] { if (!item.sockets) { return []; } const sockets = []; const { intrinsicSocket, modSocketsByCategory, perks } = getDisplayedItemSockets( item, /* excludeEmptySockets */ true, )!; if (intrinsicSocket) { sockets.push(intrinsicSocket); } if (perks) { sockets.push(...getSocketsByIndexes(item.sockets, perks.socketIndexes)); } // Improve this when we use iterator-helpers sockets.push(...[...modSocketsByCategory.values()].flat()); const socketItems = sockets.map( (s) => (isKillTrackerSocket(s) && s.plugged?.plugDef.displayProperties.name) || s.plugOptions.map((p) => s.plugged?.plugDef.hash === p.plugDef.hash ? `${p.plugDef.displayProperties.name}*` : p.plugDef.displayProperties.name, ), ); return socketItems.flat(); } export function buildNodeNames(nodes: D1GridNode[]): string[] { return filterMap(nodes, (node) => { if (D1_FILTERED_NODE_HASHES.includes(node.hash)) { return; } return node.activated ? `${node.name}*` : node.name; }); } ================================================ FILE: src/app/inventory/store/armor-quality.ts ================================================ import { D1BucketHashes } from 'app/search/d1-known-values'; import { BucketHashes } from 'data/d2/generated-enums'; import { D1Stat } from '../item-types'; /** * Calculate stat ranges for armor. This also modifies the input stats to add per-stat quality ratings. * * @param stats a list of the item's stats * @param light the item's defense * @param type a string indicating the item's type */ // thanks to bungie armory for the max-base stats // thanks to /u/iihavetoes for rates + equation // https://www.reddit.com/r/DestinyTheGame/comments/4geixn/a_shift_in_how_we_view_stat_infusion_12tier/ // TODO set a property on a bucket saying whether it can have quality rating, etc export function getQualityRating( stats: D1Stat[] | null, light: { value: number }, bucketHash: BucketHashes | D1BucketHashes, ): { min: number; max: number; range: string; } | null { if (!stats?.length || !light || light.value < 280) { return null; } let split: number; switch (bucketHash) { case BucketHashes.Helmet: split = 46; // bungie reports 48, but i've only seen 46 break; case BucketHashes.Gauntlets: split = 41; // bungie reports 43, but i've only seen 41 break; case BucketHashes.ChestArmor: split = 61; break; case BucketHashes.LegArmor: split = 56; break; case BucketHashes.ClassArmor: case BucketHashes.Ghost: split = 25; break; case D1BucketHashes.Artifact: split = 38; break; default: return null; } const ret = { total: { min: 0, max: 0, }, max: split * 2, }; let pure = 0; for (const stat of stats) { let scaled = { min: 0, max: 0, }; if (stat.base) { scaled = getScaledStat(stat.base, light.value); pure = scaled.min; } stat.scaled = scaled; stat.split = split; stat.qualityPercentage = { range: '', min: Math.round((100 * stat.scaled.min) / stat.split), max: Math.round((100 * stat.scaled.max) / stat.split), }; ret.total.min += scaled.min || 0; ret.total.max += scaled.max || 0; } if (pure === ret.total.min) { for (const stat of stats) { if (stat.scaled) { stat.scaled = { min: Math.floor(stat.scaled.min / 2), max: Math.floor(stat.scaled.max / 2), }; if (stat.split) { stat.qualityPercentage = { range: '', min: Math.round((100 * stat.scaled.min) / stat.split), max: Math.round((100 * stat.scaled.max) / stat.split), }; } } } } let quality = { min: Math.round((ret.total.min / ret.max) * 100), max: Math.round((ret.total.max / ret.max) * 100), range: '', }; if (bucketHash !== D1BucketHashes.Artifact) { for (const stat of stats) { if (stat.qualityPercentage) { stat.qualityPercentage = { range: '', min: Math.min(100, stat.qualityPercentage.min), max: Math.min(100, stat.qualityPercentage.max), }; } } quality = { min: Math.min(100, quality.min), max: Math.min(100, quality.max), range: '', }; } for (const stat of stats) { if (stat.qualityPercentage) { stat.qualityPercentage.range = getQualityRange(light.value, stat.qualityPercentage); } } quality.range = getQualityRange(light.value, quality); return quality; } // For a quality property, return a range string (min-max percentage) function getQualityRange(light: number, quality: { min: number; max: number }): string { if (!quality) { return ''; } if (light > 335) { light = 335; } return `${ quality.min === quality.max || light === 335 ? quality.min : `${quality.min}%-${quality.max}` }%`; } function fitValue(light: number) { if (light > 300) { return 0.2546 * light - 23.825; } else if (light > 200) { return 0.1801 * light - 1.4612; } else { return -1; } } function getScaledStat(base: number, light: number) { const max = 335; if (light > 335) { light = 335; } return { min: Math.floor(base * (fitValue(max) / fitValue(light))), max: Math.floor((base + 1) * (fitValue(max) / fitValue(light))), }; } ================================================ FILE: src/app/inventory/store/catalyst.ts ================================================ import { DestinyCharacterRecordsComponent, DestinyProfileRecordsComponent, DestinyRecordState, } from 'bungie-api-ts/destiny2'; import exoticToCatalystRecordHash from 'data/d2/exotic-to-catalyst-record.json'; import exoticsWithCatalysts from 'data/d2/exotics-with-catalysts'; import { DimCatalyst } from '../item-types'; export function buildCatalystInfo( itemHash: number, profileRecords: DestinyProfileRecordsComponent | undefined, characterRecords: { [key: string]: DestinyCharacterRecordsComponent } | undefined, ): DimCatalyst | undefined { if (!exoticsWithCatalysts.has(itemHash)) { return undefined; } const recordHash = exoticToCatalystRecordHash[itemHash]; const record = recordHash && (profileRecords?.records[recordHash] ?? (characterRecords && Object.values(characterRecords).find((records) => records.records[recordHash])?.records[ recordHash ])); if (!record) { return undefined; } // TODO: Can't tell the difference between unlocked and inserted for new-style catalysts? const complete = Boolean( !(record.state & DestinyRecordState.ObjectiveNotCompleted) || record.state & DestinyRecordState.RecordRedeemed, ); // TODO: seasonal exotics (e.g. Ticuu's) are unlocked by default but still show as obscured - they're run by a quest instead of a record? // Need to map from weapon -> catalyst plug item -> quest that rewards it -> quest in inventory or objectives in profile (across all chars)? // if the quest item exists in inventory (how do we figure *that* out?) then it's unlocked and not complete // 1. could pass along a set of quest item hashes in all inventories, or all uninstanced objectives that match the quest item // 2. could do a second pass on items populating it? rip through all the quest items, find ones whose rewards reference a catalyst, then go back and fix up the items' DimCatalyst info? would need a mapping from item hash to catalyst item hash (which I guess we can match up by name...) const unlocked = !(record.state & DestinyRecordState.Obscured); return { complete, unlocked, objectives: record.objectives }; } ================================================ FILE: src/app/inventory/store/character-utils.ts ================================================ import { D1ManifestDefinitions } from 'app/destiny1/d1-definitions'; import { D1Character, D1StatDefinition } from 'app/destiny1/d1-manifest-types'; import { ArmorTypes } from 'app/destiny1/loadout-builder/types'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { BucketHashes } from 'data/d2/generated-enums'; import { DimCharacterStat } from '../store-types'; /** * D1 specific armor bonus calculation. */ // thanks to /u/iihavetoes for the bonuses at each level // thanks to /u/tehdaw for the spreadsheet with bonuses // https://docs.google.com/spreadsheets/d/1YyFDoHtaiOOeFoqc5Wc_WC2_qyQhBlZckQx5Jd4bJXI/edit?pref=2&pli=1#gid=0 export function getBonus(light: number, bucketHash: ArmorTypes): number { switch (bucketHash) { case BucketHashes.Helmet: return light < 292 ? 15 : light < 307 ? 16 : light < 319 ? 17 : light < 332 ? 18 : 19; case BucketHashes.Gauntlets: return light < 287 ? 13 : light < 305 ? 14 : light < 319 ? 15 : light < 333 ? 16 : 17; case BucketHashes.ChestArmor: return light < 287 ? 20 : light < 300 ? 21 : light < 310 ? 22 : light < 319 ? 23 : light < 328 ? 24 : 25; case BucketHashes.LegArmor: return light < 284 ? 18 : light < 298 ? 19 : light < 309 ? 20 : light < 319 ? 21 : light < 329 ? 22 : 23; case BucketHashes.ClassArmor: case BucketHashes.Ghost: return light < 295 ? 8 : light < 319 ? 9 : 10; case D1BucketHashes.Artifact: return light < 287 ? 34 : light < 295 ? 35 : light < 302 ? 36 : light < 308 ? 37 : light < 314 ? 38 : light < 319 ? 39 : light < 325 ? 40 : light < 330 ? 41 : light < 336 ? 42 : 43; } } /** * Compute D1 character-level stats (int, dis, str). */ export function getCharacterStatsData( defs: D1ManifestDefinitions, data: D1Character['characterBase'], ) { const ret: { [statHash: string]: DimCharacterStat } = {}; for (const statId of ['STAT_DISCIPLINE', 'STAT_INTELLECT', 'STAT_STRENGTH'] as const) { const rawStat = data.stats[statId]; if (!rawStat) { continue; } const statDef = defs.Stat.get(rawStat.statHash); ret[statDef.hash] = characterStatFromStatDef(statDef, rawStat.value); } return ret; } export function characterStatFromStatDef( statDef: D1StatDefinition, value: number, ): DimCharacterStat { return { hash: statDef.statHash, displayProperties: { name: statDef.statName, description: statDef.statDescription, icon: statDef.icon, hasIcon: Boolean(statDef.icon), highResIcon: '', iconSequences: [], iconHash: 0, }, value, }; } ================================================ FILE: src/app/inventory/store/crafted.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { warnLog } from 'app/utils/log'; import { getFirstSocketByCategoryHash } from 'app/utils/socket-utils'; import { HashLookup } from 'app/utils/util-types'; import { DestinyObjectiveProgress, DestinyObjectiveUiStyle } from 'bungie-api-ts/destiny2'; import { DimCrafted, DimItem, DimSocket } from '../item-types'; /** the socket category containing the single socket with weapon crafting objectives */ export const craftedSocketCategoryHash = 3583996951; /** the socket category containing the Mementos */ export const mementoSocketCategoryHash = 3201856887; /** the socket containing the enhancement tier plugs */ export const enhancementSocketHash = 4251072212; const plugHashToEnhancementTier: HashLookup = { 2728416798: 1, 2728416797: 2, 2728416796: 3, }; export function buildCraftedInfo( item: DimItem, defs: D2ManifestDefinitions, ): DimCrafted | undefined { const craftedSocket = getCraftedSocket(item); if (!craftedSocket) { return undefined; } const objectives = craftedSocket.plugged?.plugObjectives; if (!objectives) { return undefined; } const craftingInfo = getCraftingInfo(defs, objectives); if (!craftingInfo) { return undefined; } craftingInfo.enhancementTier = getEnhancementTier(item); return craftingInfo; } /** find the item socket that could contain the "this weapon was crafted" plug with its objectives */ export function getCraftedSocket(item: DimItem): DimSocket | undefined { if (item.bucket.inWeapons && item.sockets) { return getFirstSocketByCategoryHash(item.sockets, craftedSocketCategoryHash); } } function getEnhancementTier(item: DimItem): number { if (item.bucket.inWeapons && item.sockets) { const plugHash = item.sockets.allSockets.find( (s) => s.socketDefinition.socketTypeHash === enhancementSocketHash, )?.plugged?.plugDef.hash; return (plugHash && plugHashToEnhancementTier[plugHash]) || 0; } return 0; } function getCraftingInfo( defs: D2ManifestDefinitions, objectives: DestinyObjectiveProgress[], ): DimCrafted | undefined { let level: number | undefined; let progress: number | undefined; let craftedDate: number | undefined; for (const objective of objectives) { const def = defs.Objective.get(objective.objectiveHash); if (def) { if (def.uiStyle === DestinyObjectiveUiStyle.CraftingWeaponLevel) { level = objective.progress!; } else if (def.uiStyle === DestinyObjectiveUiStyle.CraftingWeaponLevelProgress) { progress = objective.progress! / objective.completionValue; } else if (def.uiStyle === DestinyObjectiveUiStyle.CraftingWeaponTimestamp) { craftedDate = objective.progress!; } } } if (level === undefined || progress === undefined || craftedDate === undefined) { warnLog('Item is missing one of level, progress, craftedDate', { level, progress, craftedDate, }); return undefined; } return { level, progress, craftedDate, enhancementTier: 0 }; } ================================================ FILE: src/app/inventory/store/d1-item-factory.ts ================================================ import { D1DamageTypeDefinition, D1InventoryItemDefinition, D1ItemComponent, D1ProgressionDefinition, D1StatDefinition, D1StatHashes, D1TalentGridDefinition, } from 'app/destiny1/d1-manifest-types'; import { ArmorTypes } from 'app/destiny1/loadout-builder/types'; import { t } from 'app/i18next-t'; import { D1BucketHashes, D1_StatHashes } from 'app/search/d1-known-values'; import { ItemRarityMap } from 'app/search/d2-known-values'; import { lightStats } from 'app/search/search-filter-values'; import { filterMap, maxOf, minOf, sumBy, uniqBy } from 'app/utils/collections'; import { chainComparator, compareBy } from 'app/utils/comparators'; import { getItemYear } from 'app/utils/item-utils'; import { errorLog, warnLog } from 'app/utils/log'; import { BucketCategory, DamageType, DestinyAmmunitionType, DestinyClass, DestinyDamageTypeDefinition, DestinyDisplayPropertiesDefinition, DestinyInventoryItemStatDefinition, ItemBindStatus, ItemLocation, ItemState, TierType, TransferStatuses, } from 'bungie-api-ts/destiny2'; import missingSources from 'data/d1/missing_sources.json'; import { BucketHashes, ItemCategoryHashes, StatHashes } from 'data/d2/generated-enums'; import { clamp, memoize } from 'es-toolkit'; import { vaultTypes } from '../../destiny1/d1-buckets'; import { D1ManifestDefinitions, DefinitionTable } from '../../destiny1/d1-definitions'; import { reportException } from '../../utils/sentry'; import { InventoryBuckets } from '../inventory-buckets'; import { D1GridNode, D1Item, D1Stat, D1TalentGrid } from '../item-types'; import { D1Store, DimStore } from '../store-types'; import { getQualityRating } from './armor-quality'; import { getBonus } from './character-utils'; import { createItemIndex } from './item-index'; const TAG = 'd1-stores'; /** * Process an entire list of items into DIM items. * @param owner the ID of the owning store. * @param items a list of "raw" items from the Destiny API * @return a promise for the list of items */ export function processItems( owner: D1Store | undefined, items: D1ItemComponent[], defs: D1ManifestDefinitions, buckets: InventoryBuckets, ): D1Item[] { const result: D1Item[] = []; for (const item of items) { let createdItem: D1Item | null = null; try { createdItem = makeItem(defs, buckets, item, owner); } catch (e) { errorLog(TAG, 'Error processing item', item, e); reportException('Processing D1 item', e); } if (createdItem !== null) { if (owner) { createdItem.owner = owner.id; } result.push(createdItem); } else { // the item failed to be created for some reason. 2 things can currently cause this: // an exception occurred while creating the item, or it has a definition but lacks a name // not all of these should cause the store to consider itself hadErrors. // dummies and invisible items are not a big deal const bucketDef = defs.InventoryBucket.get(item.bucket); // if it's a named, non-invisible bucket, it may be a problem that the item wasn't generated if (owner && bucketDef.category !== BucketCategory.Invisible && bucketDef.bucketName) { owner.hadErrors = true; } } } return result; } const getClassTypeNameLocalized = memoize( ([type, defs]: [type: DestinyClass, defs: D1ManifestDefinitions]): string => { const klass = Object.values(defs.Class.getAll()).find((c) => c.classType === type); if (klass) { return klass.className; } else { return t('Loadouts.Any'); } }, { getCacheKey: ([type]) => `${type}`, }, ); /** * Convert a D1DamageType to the D2 definition, so we don't have to maintain both codepaths */ const toD2DamageType = memoize( (damageType: D1DamageTypeDefinition | undefined): DestinyDamageTypeDefinition | undefined => damageType && { displayProperties: { name: damageType.damageTypeName, description: damageType.description, icon: damageType.iconPath, hasIcon: true, highResIcon: '', iconSequences: [], iconHash: 0, }, transparentIconPath: damageType.transparentIconPath, hash: damageType.hash, showIcon: damageType.showIcon, enumValue: damageType.enumValue, index: damageType.index, redacted: damageType.redacted, color: { red: 0, green: 0, blue: 0, alpha: 0, }, }, ); export function makeFakeItem( defs: D1ManifestDefinitions, buckets: InventoryBuckets, itemHash: number, itemInstanceId = '0', ) { return makeItem( defs, buckets, { itemHash, itemInstanceId, bindStatus: ItemBindStatus.NotBound, location: ItemLocation.Vendor, transferStatus: TransferStatuses.NotTransferrable, lockable: false, state: ItemState.None, isEquipped: false, itemLevel: 0, stackSize: 1, qualityLevel: 0, canEquip: false, equipRequiredLevel: 0, unlockFlagHashRequiredToEquip: 0, stats: [], cannotEquipReason: 0, damageType: DamageType.None, damageTypeHash: 0, damageTypeNodeIndex: 0, damageTypeStepIndex: 0, talentGridHash: 0, nodes: [], useCustomDyes: false, isEquipment: false, isGridComplete: false, perks: [], locked: false, objectives: [], bucket: 0, }, undefined, ); } /** * Process a single raw item into a DIM item. * @param defs the manifest definitions * @param buckets the bucket definitions * @param previousItems a set of item IDs representing the previous store's items * @param newItems a set of item IDs representing the previous list of new items * @param item "raw" item from the Destiny API * @param owner the ID of the owning store. */ function makeItem( defs: D1ManifestDefinitions, buckets: InventoryBuckets, item: D1ItemComponent, owner: DimStore | undefined, ) { const itemDef = defs.InventoryItem.get(item.itemHash); // Missing definition? if (!itemDef) { return null; } if (!itemDef.icon) { itemDef.redacted = true; itemDef.classType = 3; } if (!itemDef.icon) { itemDef.icon = '/img/misc/missing_icon.png'; } if (!itemDef.itemTypeName) { itemDef.itemTypeName = 'Unknown'; } if (itemDef.redacted) { warnLog( 'd1-stores', 'Missing Item Definition:\n\n', item, '\n\nThis item is not in the current manifest and will be added at a later time by Bungie.', ); } if (!itemDef.itemName) { return null; } const numStats = itemDef.stats ? Object.keys(itemDef.stats).length : 0; // fix itemDef for defense items with missing nodes if (item.primaryStat?.statHash === D1_StatHashes.Defense && numStats > 0 && numStats !== 5) { const defaultMinMax = Object.values(itemDef.stats).find((stat) => [D1StatHashes.Intellect, D1StatHashes.Discipline, D1StatHashes.Strength].includes( stat.statHash, ), ); if (defaultMinMax) { for (const val of [D1StatHashes.Intellect, D1StatHashes.Discipline, D1StatHashes.Strength]) { if (!itemDef.stats[val]) { itemDef.stats[val] = { maximum: defaultMinMax.maximum, minimum: defaultMinMax.minimum, statHash: val, value: 0, }; } } } } // def.bucketTypeHash is where it goes normally let normalBucket = buckets ? buckets.byHash[itemDef.bucketTypeHash] : undefined; // item.bucket is where it IS right now let currentBucket = buckets.byHash[item.bucket] || normalBucket; if (!normalBucket) { currentBucket = normalBucket = buckets.unknown; buckets.setHasUnknown(); } // We cheat a bit for items in the vault, since we treat the // vault as a character. So put them in the bucket they would // have been in if they'd been on a character. if (currentBucket.hash in vaultTypes) { if (itemDef.redacted && itemDef.itemTypeName === 'Unknown') { switch (currentBucket.hash) { case 4046403665: currentBucket = buckets.byHash[BucketHashes.PowerWeapons]; break; case 3003523923: currentBucket = buckets.byHash[BucketHashes.ClassArmor]; break; case 138197802: currentBucket = buckets.byHash[D1BucketHashes.Artifact]; break; default: break; } } else { currentBucket = normalBucket; } } const element = (item.damageTypeHash && toD2DamageType(defs.DamageType.get(item.damageTypeHash))) || null; itemDef.sourceHashes ||= []; const missingSource = missingSources[itemDef.hash] || []; if (missingSource.length) { itemDef.sourceHashes = [...new Set([...itemDef.sourceHashes, ...missingSource])]; } const createdItem: D1Item = { owner: owner?.id || 'unknown', // figure out what year this item is probably from destinyVersion: 1, // The bucket the item is currently in location: currentBucket, // The bucket the item normally resides in (even though it may be in the vault/postmaster) bucket: normalBucket, hash: item.itemHash, itemCategoryHashes: itemDef.itemCategoryHashes || [], rarity: ItemRarityMap[itemDef.tierType] || 'Common', isExotic: itemDef.tierType === TierType.Exotic, name: itemDef.itemName, description: itemDef.itemDescription || '', // Added description for Bounties for now JFLAY2015 icon: itemDef.icon, secondaryIcon: itemDef.secondaryIcon, notransfer: Boolean( currentBucket.inPostmaster || itemDef.nonTransferrable || !itemDef.allowActions || itemDef.redacted, ), id: item.itemInstanceId, instanced: item.itemInstanceId !== '0', equipped: item.isEquipped, equipment: item.isEquipment, equippingLabel: item.isEquipment && itemDef.tierType === TierType.Exotic ? normalBucket.sort : undefined, complete: item.isGridComplete, amount: item.stackSize || 1, primaryStat: null, typeName: itemDef.itemTypeName, isEngram: (itemDef.itemCategoryHashes || []).includes(34), equipRequiredLevel: item.equipRequiredLevel, maxStackSize: itemDef.maxStackSize > 0 ? itemDef.maxStackSize : 1, // 0: titan, 1: hunter, 2: warlock, 3: any classType: itemDef.classType, classTypeNameLocalized: getClassTypeNameLocalized([itemDef.classType, defs]), element, ammoType: getAmmoType(normalBucket.hash), sourceHashes: itemDef.sourceHashes, lockable: normalBucket.hash !== BucketHashes.Subclass && ((currentBucket.inPostmaster && item.isEquipment) || currentBucket.inWeapons || item.lockable), trackable: Boolean( currentBucket.inProgress && (currentBucket.hash === D1BucketHashes.Bounties || currentBucket.hash === D1BucketHashes.Quests), ), tracked: item.state === 2, locked: item.locked, classified: Boolean(itemDef.redacted), // These get filled in later or aren't relevant to D1 items percentComplete: 0, talentGrid: null, stats: null, objectives: undefined, quality: null, sockets: null, breakerType: null, hidePercentage: false, taggable: false, comparable: false, wishListEnabled: false, power: 0, index: '', infusable: false, infusionFuel: false, masterworkInfo: null, infusionCategoryHashes: null, canPullFromPostmaster: false, uniqueStack: false, masterwork: false, crafted: false, highlightedObjective: false, missingSockets: false, energy: null, pursuit: null, featured: false, tier: 0, adept: false, holofoil: false, }; // *able createdItem.taggable = Boolean(createdItem.lockable && !createdItem.isEngram); createdItem.comparable = Boolean( createdItem.equipment && createdItem.lockable && createdItem.bucket.hash !== BucketHashes.Ships, ); // Moving rare masks destroys them if ( createdItem.itemCategoryHashes.includes(ItemCategoryHashes.Mask) && createdItem.rarity !== 'Legendary' ) { createdItem.notransfer = true; } if (item.primaryStat) { const statDef = defs.Stat.get(item.primaryStat.statHash); createdItem.primaryStat = item.primaryStat; createdItem.primaryStatDisplayProperties = { name: statDef.statName, description: statDef.statDescription, icon: statDef.icon, hasIcon: Boolean(statDef.icon), } as DestinyDisplayPropertiesDefinition; if (lightStats.includes(createdItem.primaryStat.statHash)) { createdItem.power = createdItem.primaryStat.value; } } try { createdItem.talentGrid = buildTalentGrid(item, defs.TalentGrid, defs.Progression); } catch (e) { errorLog(TAG, `Error building talent grid for ${createdItem.name}`, item, itemDef, e); } createdItem.infusable = Boolean(createdItem.talentGrid?.infusable); // An item can be used as infusion fuel if it is equipment, and has a primary stat that isn't Speed createdItem.infusionFuel = Boolean( createdItem.equipment && createdItem.primaryStat?.statHash !== StatHashes.Speed, ); try { createdItem.stats = buildStats( item, itemDef, defs.Stat, createdItem.talentGrid, createdItem.bucket.hash, ); if (createdItem.stats?.length === 0) { createdItem.stats = buildStats( item, item, defs.Stat, createdItem.talentGrid, createdItem.bucket.hash, ); } } catch (e) { errorLog(TAG, `Error building stats for ${createdItem.name}`, item, itemDef, e); } createdItem.objectives = item.objectives?.length > 0 ? item.objectives.map((o) => ({ objectiveHash: o.objectiveHash, complete: o.isComplete, progress: o.progress, completionValue: defs.Objective.get(o.objectiveHash).completionValue, visible: true, })) : undefined; if (createdItem.talentGrid && createdItem.infusable && item.primaryStat) { try { createdItem.quality = getQualityRating( createdItem.stats, item.primaryStat, createdItem.bucket.hash, ); } catch (e) { errorLog( 'd1-stores', `Error building quality rating for ${createdItem.name}`, item, itemDef, e, ); } } // More objectives properties if (createdItem.objectives) { const objectives = createdItem.objectives; createdItem.complete = (!createdItem.talentGrid || createdItem.complete) && createdItem.objectives.every((o) => o.complete); createdItem.percentComplete = sumBy(createdItem.objectives, (objective) => { if (objective.completionValue) { return ( Math.min(1, (objective.progress || 0) / objective.completionValue) / objectives.length ); } else { return 0; } }); } else if (createdItem.talentGrid) { createdItem.percentComplete = Math.min( 1, createdItem.talentGrid.totalXP / createdItem.talentGrid.totalXPRequired, ); createdItem.complete = getItemYear(createdItem) === 1 ? createdItem.talentGrid.totalXP === createdItem.talentGrid.totalXPRequired : createdItem.talentGrid.complete; } // "The Life Exotic" perk means you can equip other exotics, so clear out the equipping label if (createdItem.isExotic && createdItem.talentGrid?.nodes.some((n) => n.hash === 4044819214)) { createdItem.equippingLabel = undefined; } createdItem.index = createItemIndex(createdItem); return createdItem; } function getAmmoType(bucketHash: BucketHashes) { switch (bucketHash) { case BucketHashes.KineticWeapons: return DestinyAmmunitionType.Primary; case BucketHashes.EnergyWeapons: return DestinyAmmunitionType.Special; case BucketHashes.PowerWeapons: return DestinyAmmunitionType.Heavy; default: return DestinyAmmunitionType.None; } } function buildTalentGrid( item: D1ItemComponent, talentDefs: DefinitionTable, progressDefs: DefinitionTable, ): D1TalentGrid | null { const talentGridDef = item.talentGridHash && talentDefs.get(item.talentGridHash); if ( !item.progression || !talentGridDef || !item.nodes?.length || !progressDefs.get(item.progression.progressionHash) ) { return null; } const totalXP = item.progression.currentProgress; const totalLevel = item.progression.level; // Can be way over max // progressSteps gives the XP needed to reach each level, with // the last element repeating infinitely. const progressSteps = progressDefs.get(item.progression.progressionHash).steps; // Total XP to get to specified level function xpToReachLevel(level: number) { if (level === 0) { return 0; } let totalXPRequired = 0; for (let step = 1; step <= level; step++) { totalXPRequired += progressSteps[Math.min(step, progressSteps.length) - 1].progressTotal; } return totalXPRequired; } const possibleNodes = talentGridDef.nodes; let gridNodes = filterMap(item.nodes, (node): D1GridNode | undefined => { const talentNodeGroup = possibleNodes[node.nodeHash]; const talentNodeSelected = talentNodeGroup.steps[node.stepIndex]; if (!talentNodeSelected) { return undefined; } const nodeName = talentNodeSelected.nodeStepName; // Filter out some weird bogus nodes if (!nodeName || nodeName.length === 0 || talentNodeGroup.column < 0) { return undefined; } // Only one node in this column can be selected (scopes, etc) const exclusiveInColumn = Boolean(talentNodeGroup.exlusiveWithNodes?.length); // Unlocked is whether or not the material cost has been paid // for the node const unlocked = node.isActivated || talentNodeGroup.autoUnlocks || // If only one can be activated, the cost only needs to be // paid once per row. (exclusiveInColumn && talentNodeGroup.exlusiveWithNodes.some((nodeIndex) => item.nodes[nodeIndex].isActivated)); // Calculate relative XP for just this node const startProgressionBarAtProgress = talentNodeSelected.startProgressionBarAtProgress; const activatedAtGridLevel = talentNodeSelected.activationRequirement.gridLevel; const xpRequired = xpToReachLevel(activatedAtGridLevel) - startProgressionBarAtProgress; const xp = clamp(totalXP - startProgressionBarAtProgress, 0, xpRequired); // hacky way to determine if the node is a weapon ornament let ornamentComplete = false; if (talentNodeGroup.column > 1 && !xpRequired && !exclusiveInColumn && item.primaryStat) { ornamentComplete = node.isActivated; } // There's a lot more here, but we're taking just what we need return { name: nodeName, ornament: ornamentComplete, hash: talentNodeSelected.nodeStepHash, description: talentNodeSelected.nodeStepDescription ?? '', icon: talentNodeSelected.icon, // XP put into this node xp, // XP needed for this node to unlock xpRequired, // Position in the grid column: talentNodeGroup.column, row: talentNodeGroup.row, // Is the node selected (lit up in the grid) activated: node.isActivated, // The item level at which this node can be unlocked activatedAtGridLevel, // Only one node in this column can be selected (scopes, etc) exclusiveInColumn, // Whether there's enough XP in the item to buy the node xpRequirementMet: activatedAtGridLevel <= totalLevel, // Whether or not the material cost has been paid for the node unlocked, // Some nodes don't show up in the grid, like purchased ascend nodes hidden: node.hidden, // Whether (and in which order) this perk should be // "featured" on an abbreviated info panel, as in the // game. 0 = not featured, positive numbers signify the // order of the featured perks. // featuredPerk: (featuredPerkNames.indexOf(nodeName) + 1) // This list of material requirements to unlock the // item are a mystery. These hashes don't exist anywhere in // the manifest database. Also, the activationRequirement // object doesn't say how much of the material is // needed. There's got to be some missing DB somewhere with // this info. // materialsNeeded: talentNodeSelected.activationRequirement.materialRequirementHashes // These are useful for debugging or searching for new properties, // but they don't need to be included in the result. // talentNodeGroup: talentNodeGroup, // talentNodeSelected: talentNodeSelected, // itemNode: node }; }); // We need to unique-ify because Ornament nodes show up twice! gridNodes = uniqBy(gridNodes, (n) => n.hash); if (!gridNodes.length) { return null; } // This can be handy for visualization/debugging // var columns = Object.groupBy(gridNodes, 'column'); const maxLevelRequired = maxOf(gridNodes, (n) => n.activatedAtGridLevel); const totalXPRequired = xpToReachLevel(maxLevelRequired); const ascendNode = gridNodes.find((n) => n.hash === 1920788875); // Fix for stuff that has nothing in early columns const minColumn = minOf( gridNodes.filter((n) => !n.hidden), (n) => n.column, ); if (minColumn > 0) { for (const node of gridNodes) { node.column -= minColumn; } } const maxColumn = maxOf(gridNodes, (n) => n.column); return { nodes: gridNodes.sort( chainComparator( compareBy((node) => node.column), compareBy((node) => node.row), ), ), xpComplete: totalXPRequired <= totalXP, totalXPRequired, totalXP: Math.min(totalXPRequired, totalXP), hasAscendNode: Boolean(ascendNode), ascended: Boolean(ascendNode?.activated), infusable: gridNodes.some((n) => n.hash === 1270552711), complete: totalXPRequired <= totalXP && gridNodes.every((n) => n.unlocked || (n.xpRequired === 0 && n.column === maxColumn)), }; } function buildStats( item: D1ItemComponent, itemDef: D1InventoryItemDefinition | D1ItemComponent, statDefs: DefinitionTable, grid: D1TalentGrid | null, bucketHash: ArmorTypes, ): D1Stat[] | null { if (!item.stats?.length || !itemDef.stats) { return null; } let armorNodes: D1GridNode[] = []; let activeArmorNode: D1GridNode | { hash: number }; if (grid?.nodes && item.primaryStat?.statHash === D1_StatHashes.Defense) { armorNodes = grid.nodes.filter( (node) => [1034209669, 1263323987, 193091484].includes(node.hash), // ['Increase Intellect', 'Increase Discipline', 'Increase Strength'] ); if (armorNodes) { activeArmorNode = armorNodes.find((n) => n.activated) || { hash: 0 }; } } return filterMap( Object.values(itemDef.stats), (stat: D1Stat | DestinyInventoryItemStatDefinition) => { const def = statDefs.get(stat.statHash); if (!def) { return undefined; } const identifier = def.statIdentifier; // Only include these hidden stats, in this order const secondarySort = ['STAT_AIM_ASSISTANCE', 'STAT_EQUIP_SPEED']; let secondaryIndex = -1; let sort = item.stats.findIndex((s) => s.statHash === stat.statHash); let itemStat; if (sort < 0) { secondaryIndex = secondarySort.indexOf(identifier); sort = 50 + secondaryIndex; } else { itemStat = item.stats[sort]; // Always at the end if (identifier === 'STAT_MAGAZINE_SIZE' || identifier === 'STAT_ATTACK_ENERGY') { sort = 100; } } if (!itemStat && secondaryIndex < 0) { return undefined; } let maximumValue = 100; if (itemStat?.maximumValue) { maximumValue = itemStat.maximumValue; } const val: number = (itemStat ? itemStat.value : stat.value) || 0; let base = val; let bonus = 0; const primaryStatDef = item.primaryStat && statDefs.get(item.primaryStat.statHash); if ( item.primaryStat && primaryStatDef?.statIdentifier === 'STAT_DEFENSE' && ((identifier === 'STAT_INTELLECT' && armorNodes.find((n) => n.hash === 1034209669 /* Increase Intellect */)) || (identifier === 'STAT_DISCIPLINE' && armorNodes.find((n) => n.hash === 1263323987 /* Increase Discipline */)) || (identifier === 'STAT_STRENGTH' && armorNodes.find((n) => n.hash === 193091484 /* Increase Strength */))) ) { bonus = getBonus(item.primaryStat.value, bucketHash); if ( activeArmorNode && ((identifier === 'STAT_INTELLECT' && activeArmorNode.hash === 1034209669) || (identifier === 'STAT_DISCIPLINE' && activeArmorNode.hash === 1263323987) || (identifier === 'STAT_STRENGTH' && activeArmorNode.hash === 193091484)) ) { base = Math.max(0, val - bonus); } } return { base, bonus, investmentValue: base, statHash: stat.statHash, displayProperties: { name: def.statName, description: def.statDescription, } as DestinyDisplayPropertiesDefinition, sort, value: val, maximumValue, bar: identifier !== 'STAT_MAGAZINE_SIZE' && identifier !== 'STAT_ATTACK_ENERGY', // energy == magazine for swords smallerIsBetter: [447667954, 2961396640].includes(stat.statHash), additive: primaryStatDef?.statIdentifier === 'STAT_DEFENSE', isConditionallyActive: false, }; }, ).sort(compareBy((s) => s.sort)); } ================================================ FILE: src/app/inventory/store/d1-store-factory.ts ================================================ import { D1CharacterData } from 'app/destiny1/d1-manifest-types'; import { t } from 'app/i18next-t'; import { HashLookup } from 'app/utils/util-types'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import vaultBackground from 'images/vault-background.svg'; import vaultIcon from 'images/vault.svg'; import { D1ManifestDefinitions } from '../../destiny1/d1-definitions'; import { D1Store, DimStore } from '../store-types'; import { getCharacterStatsData } from './character-utils'; // Label isn't used, but it helps us understand what each one is const progressionMeta: HashLookup<{ label: string; order: number }> = { 529303302: { label: 'Cryptarch', order: 0 }, 3233510749: { label: 'Vanguard', order: 1 }, 1357277120: { label: 'Crucible', order: 2 }, 2778795080: { label: 'Dead Orbit', order: 3 }, 1424722124: { label: 'Future War Cult', order: 4 }, 3871980777: { label: 'New Monarchy', order: 5 }, 2161005788: { label: 'Iron Banner', order: 6 }, 174528503: { label: "Crota's Bane", order: 7 }, 807090922: { label: "Queen's Wrath", order: 8 }, 3641985238: { label: 'House of Judgment', order: 9 }, 2335631936: { label: 'Gunsmith', order: 10 }, 2576753410: { label: 'SRL', order: 11 }, }; export function makeCharacter( characterComponent: D1CharacterData, defs: D1ManifestDefinitions, mostRecentLastPlayed: Date, ) { const character = characterComponent.character; const race = defs.Race.get(character.characterBase.raceHash); const klass = defs.Class.get(character.characterBase.classHash); let genderRace: string; let className: string; let raceName: string; let gender: DimStore['gender']; let genderName: DimStore['genderName']; if (character.characterBase.genderType === 0) { gender = 'male'; genderName = 'male'; genderRace = race.raceNameMale; raceName = race.raceNameMale; className = klass.classNameMale; } else { gender = 'female'; genderName = 'female'; genderRace = race.raceNameFemale; raceName = race.raceNameFemale; className = klass.classNameFemale; } const lastPlayed = new Date(character.characterBase.dateLastPlayed); const progressions = characterComponent.progression?.progressions ?? []; for (const prog of progressions) { Object.assign( prog, defs.Progression.get(prog.progressionHash), progressionMeta[prog.progressionHash], ); const faction = Object.values(defs.Faction.getAll()).find( (f) => f.progressionHash === prog.progressionHash, ); if (faction) { prog.faction = faction; } } const store: D1Store = { destinyVersion: 1, id: characterComponent.id, name: t('ItemService.StoreName', { genderRace, className, }), icon: `https://www.bungie.net/${character.emblemPath}`, current: mostRecentLastPlayed.getTime() === lastPlayed.getTime(), lastPlayed, background: `https://www.bungie.net/${character.backgroundPath}`, level: character.characterLevel, powerLevel: character.characterBase.powerLevel, stats: getCharacterStatsData(defs, character.characterBase), classType: character.characterBase.classType, className, gender, race: raceName, genderRace, genderName, percentToNextLevel: character.percentToNextLevel / 100, progressions, advisors: characterComponent.advisors!, isVault: false, items: [], hadErrors: false, }; return store; } export function makeVault() { const store: D1Store = { destinyVersion: 1, id: 'vault', name: t('Bucket.Vault'), classType: DestinyClass.Unknown, current: false, genderName: '', className: t('Bucket.Vault'), lastPlayed: new Date('2005-01-01T12:00:01Z'), icon: vaultIcon, background: vaultBackground, items: [], isVault: true, progressions: [], advisors: { activities: {}, activityCategories: {}, bounties: {}, quests: {}, progressions: {}, recordBooks: {}, }, level: 0, percentToNextLevel: 0, powerLevel: 0, gender: '', race: '', genderRace: '', stats: [], hadErrors: false, }; return store; } ================================================ FILE: src/app/inventory/store/d2-item-factory.ts ================================================ import { CustomStatDef } from '@destinyitemmanager/dim-api-types'; import { D2Categories } from 'app/destiny2/d2-bucket-categories'; import { t } from 'app/i18next-t'; import { createCollectibleFinder } from 'app/records/collectible-matching'; import { ItemRarityMap, SOME_OTHER_DUMMY_BUCKET, THE_FORBIDDEN_BUCKET, d2MissingIcon, uniqueEquipBuckets, } from 'app/search/d2-known-values'; import { lightStats } from 'app/search/search-filter-values'; import { sumBy } from 'app/utils/collections'; import { emptyArray, emptyObject } from 'app/utils/empty'; import { errorLog, warnLog } from 'app/utils/log'; import { countEnhancedPerks } from 'app/utils/socket-utils'; import { BucketCategory, ComponentPrivacySetting, DestinyAmmunitionType, DestinyClass, DestinyDisplayPropertiesDefinition, DestinyInventoryItemDefinition, DestinyItemComponent, DestinyItemComponentSetOfint64, DestinyItemInstanceComponent, DestinyItemResponse, DestinyItemSubType, DestinyItemTooltipNotification, DestinyObjectiveProgress, DestinyProfileResponse, DestinyVendorSaleItemComponent, DictionaryComponentResponse, ItemBindStatus, ItemLocation, ItemPerkVisibility, ItemState, SingleComponentResponse, TierType, TransferStatuses, } from 'bungie-api-ts/destiny2'; import enhancedIntrinsics from 'data/d2/crafting-enhanced-intrinsics'; import extendedBreaker from 'data/d2/extended-breaker.json'; import extendedFoundry from 'data/d2/extended-foundry.json'; import extendedICH from 'data/d2/extended-ich.json'; import { BucketHashes, ItemCategoryHashes, StatHashes, TraitHashes } from 'data/d2/generated-enums'; import { keyBy, memoize } from 'es-toolkit'; import { Draft } from 'immer'; import memoizeOne from 'memoize-one'; import { D2ManifestDefinitions } from '../../destiny2/d2-definitions'; import { reportException } from '../../utils/sentry'; import { InventoryBuckets } from '../inventory-buckets'; import { DimItem, DimPursuitExpiration, DimQuestLine } from '../item-types'; import { DimStore } from '../store-types'; import { getVault } from '../stores-helpers'; import { buildCatalystInfo } from './catalyst'; import { buildCraftedInfo } from './crafted'; import { buildDeepsightInfo } from './deepsight'; import { createItemIndex } from './item-index'; import { buildMasterwork } from './masterwork'; import { buildObjectives, isTrialsPassage, isWinsObjective } from './objectives'; import { buildPatternInfo } from './patterns'; import { buildSockets } from './sockets'; import { buildStats } from './stats'; const TAG = 'd2-stores'; const collectiblesByItemHash = memoizeOne( (Collectible: ReturnType) => keyBy(Object.values(Collectible), (c) => c.itemHash), ); /** * Process an entire list of items into DIM items. */ export function processItems( context: ItemCreationContext, owner: DimStore, items: DestinyItemComponent[], ): DimItem[] { const result: DimItem[] = []; for (const item of items) { let createdItem: DimItem | undefined; try { createdItem = makeItem(context, item, owner); } catch (e) { errorLog(TAG, 'Error processing item', item, e); reportException('Processing Dim item', e); } if ( createdItem !== undefined && // we want to allow makeItem to generate dummy items. they're useful in vendors, as consumables, etc. // but processItems is for building stores, and we don't want dummy weapons or armor, // which can invisibly interfere with allItems calculations and measurements createdItem.location.hash !== THE_FORBIDDEN_BUCKET && createdItem.location.hash !== SOME_OTHER_DUMMY_BUCKET ) { createdItem.owner = owner.id; result.push(createdItem); } else { // the item failed to be created for some reason. 2 things can currently cause this: // an exception occurred, or the item lacks a definition // not all of these should cause the store to consider itself hadErrors. // dummies and invisible items are not a big deal const defs = context.defs; const bucketDef = defs.InventoryBucket.get(item.bucketHash); // if it's a named, non-invisible bucket, it may be a problem that the item wasn't generated if ( bucketDef && bucketDef.category !== BucketCategory.Invisible && bucketDef.displayProperties.name ) { const itemDef = defs.InventoryItem.get(item.itemHash); reportException('setting store hadErrors', new Error('setting store hadErrors'), { itemHash: item.itemHash, hasDefinition: Boolean(itemDef), hasName: Boolean(itemDef?.displayProperties.name), hasQuestLineName: Boolean(itemDef?.setData?.questLineName), itemBucketHash: item.bucketHash, defBucketHash: itemDef?.inventory?.bucketTypeHash, bucketName: bucketDef.displayProperties.name, }); owner.hadErrors = true; } } } return result; } export const getClassTypeNameLocalized = memoizeOne((defs: D2ManifestDefinitions) => memoize((type: DestinyClass): string => { const klass = Object.values(defs.Class.getAll()).find((c) => c.classType === type); if (klass) { return klass.displayProperties.name; } else { return t('Loadouts.Any'); } }), ); const getDamageDefsByDamageType = memoizeOne((defs: D2ManifestDefinitions) => keyBy(Object.values(defs.DamageType.getAll()), (d) => d.enumValue), ); /** Make a "fake" item from other information - used for Collectibles, etc. */ export function makeFakeItem( context: ItemCreationContext, itemHash: number, { itemInstanceId = '0', quantity = 1, allowWishList = false, itemValueVisibility, tooltipNotificationIndexes = [], }: { itemInstanceId?: string; quantity?: number; allowWishList?: boolean; /** if available, this should be passed in from a vendor saleItem (DestinyVendorSaleItemComponent) */ itemValueVisibility?: DestinyVendorSaleItemComponent['itemValueVisibility']; tooltipNotificationIndexes?: number[] | undefined; } = emptyObject(), ): DimItem | undefined { const item = makeItem( context, { itemHash, itemInstanceId: itemInstanceId, quantity: quantity ?? 1, bindStatus: ItemBindStatus.NotBound, location: ItemLocation.Vendor, bucketHash: 0, transferStatus: TransferStatuses.NotTransferrable, itemValueVisibility, lockable: false, state: ItemState.None, isWrapper: false, tooltipNotificationIndexes: tooltipNotificationIndexes ?? [], metricObjective: {} as DestinyObjectiveProgress, versionNumber: context.defs.InventoryItem.get(itemHash)?.quality?.currentVersion, }, undefined, ); if (item && !allowWishList) { item.wishListEnabled = false; } return item; } /** * Create a single item from a DestinyItemResponse, either from getItemDetails or an AWA result. * We can use this item to refresh a single item in the store from this response. */ export function makeItemSingle( context: ItemCreationContext, itemResponse: DestinyItemResponse, stores: DimStore[], ): DimItem | undefined { if (!itemResponse.item.data) { return undefined; } const owner = itemResponse.characterId ? stores.find((s) => s.id === itemResponse.characterId) : getVault(stores); const itemId = itemResponse.item.data.itemInstanceId; // Convert a single component response into a dictionary component response const empty = { privacy: ComponentPrivacySetting.Public, data: {} }; const m: (v: SingleComponentResponse) => DictionaryComponentResponse = itemId ? (v) => (v ? { privacy: v.privacy, data: v.data ? { [itemId]: v.data } : {} } : empty) : () => empty; // We'll override our item components with these ones const itemComponents = { instances: m(itemResponse.instance), perks: m(itemResponse.perks), renderData: m(itemResponse.renderData), stats: m(itemResponse.stats), sockets: m(itemResponse.sockets), reusablePlugs: m(itemResponse.reusablePlugs), plugObjectives: m(itemResponse.plugObjectives), talentGrids: m(itemResponse.talentGrid), plugStates: empty, objectives: m(itemResponse.objectives), }; // Make it look like a full response return makeItem({ ...context, itemComponents }, itemResponse.item.data, owner); } /** * Stuff that's required to create a DimItem. */ export interface ItemCreationContext { defs: D2ManifestDefinitions; buckets: InventoryBuckets; profileResponse: DestinyProfileResponse; customStats: CustomStatDef[]; /** * Sometimes comes from the profile response, but also sometimes from vendors response or mocked out. * If not present, the itemComponents from the DestinyProfileResponse should be used. */ itemComponents?: Partial; } /** * Process a single raw item into a DIM item. */ export function makeItem( { defs, buckets, itemComponents, customStats, profileResponse }: ItemCreationContext, item: DestinyItemComponent, /** the ID of the owning store - can be undefined for fake collections items */ owner: DimStore | undefined, ): DimItem | undefined { if (owner) { // only makes sense to use the profile's itemComponents if it's a real item itemComponents ??= profileResponse.itemComponents; } const itemDef = defs.InventoryItem.get(item.itemHash); // Fish relevant data out of the profile. const profileRecords = profileResponse.profileRecords?.data; const characterRecords = profileResponse.characterRecords?.data; const characterProgressions = owner && !owner?.isVault ? profileResponse.characterProgressions?.data?.[owner.id] : undefined; const itemInstanceData: Partial = item.itemInstanceId ? (itemComponents?.instances?.data?.[item.itemInstanceId] ?? emptyObject()) : emptyObject(); // Missing definition if (!itemDef) { return undefined; } if (itemDef.redacted) { warnLog( 'd2-stores', 'Missing Item Definition:\n\n', { item, itemDef, itemInstanceData }, '\n\nThis item is not in the current manifest and will be added at a later time by Bungie.', ); } if (!(itemDef.displayProperties.name || itemDef.setData?.questLineName)) { if (item.itemHash === 3377778206) { // https://d2.destinygamewiki.com/wiki/Gift_of_the_Lighthouse (itemDef.displayProperties as Draft).name = 'Gift of the Lighthouse'; } else { (itemDef.displayProperties as Draft).name = '???'; } } let displayProperties = itemDef.displayProperties; if (itemDef.redacted) { // Fill in display info from the collectible, sometimes it's not redacted there! const collectibleDef = collectiblesByItemHash(defs.Collectible.getAll())[item.itemHash]; if (collectibleDef) { displayProperties = collectibleDef.displayProperties; } } // shaders currently claim they belong in the Forbidden Bucket, but they can be physically // present in the Vault, taking up space. so: we'll claim they are Consumables. // (so that they show up in DIM, but don't cause a whole separate inventory section) // they aren't transferrable, and stop existing if removed from the vault, so they won't // interfere with the 50 consumables bucket limit. const needsShaderFix = itemDef.inventory!.bucketTypeHash === THE_FORBIDDEN_BUCKET && itemDef.itemCategoryHashes?.includes(ItemCategoryHashes.Shaders); // The same thing can happen with mods and transmat effects (included in Mods)! const needsModsFix = itemDef.inventory!.bucketTypeHash === THE_FORBIDDEN_BUCKET && itemDef.itemCategoryHashes?.includes(ItemCategoryHashes.Mods_Mod); // this is where the item would go normally (if not vaulted/postmastered). // it is stored in DimItem.bucket const normalBucketHash = needsShaderFix ? BucketHashes.Consumables : needsModsFix ? BucketHashes.Modifications : itemDef.inventory!.bucketTypeHash; let normalBucket = buckets.byHash[normalBucketHash]; // this is where the item IS, right now. // it is stored in DimItem.location let currentBucket = buckets.byHash[item.bucketHash] || normalBucket; if (!normalBucket) { currentBucket = normalBucket = buckets.unknown; buckets.setHasUnknown(); } // We cheat a bit for items in the vault, since we treat the // vault as a character. So put them in the bucket they would // have been in if they'd been on a character. if (!currentBucket.inPostmaster && (owner?.isVault || item.location === ItemLocation.Vault)) { currentBucket = normalBucket; } const isEngram = normalBucket.hash === BucketHashes.Engrams || itemDef.itemCategoryHashes?.includes(ItemCategoryHashes.Engrams) || itemDef.traitHashes?.includes(TraitHashes.ItemEngram) || false; if (isEngram && normalBucket.hash !== BucketHashes.Engrams) { normalBucket = buckets.byHash[BucketHashes.Engrams]; } // https://github.com/Bungie-net/api/issues/134, class items had a primary stat let primaryStat: DimItem['primaryStat'] = null; if ( itemInstanceData.primaryStat && normalBucket.hash !== BucketHashes.Subclass && normalBucket.hash !== BucketHashes.SeasonalArtifact && !itemDef.stats?.disablePrimaryStatDisplay ) { primaryStat = itemInstanceData.primaryStat; } if ( // engrams have a weird primary stat but their quality has their PL isEngram || // classified items have no Stats, but their quality has their PL (!primaryStat && itemDef.redacted && itemInstanceData.itemLevel && itemInstanceData.quality !== undefined && (D2Categories.Weapons.includes(item.bucketHash) || D2Categories.Armor.includes(item.bucketHash))) ) { primaryStat = { statHash: StatHashes.Power, value: (itemInstanceData.itemLevel ?? 0) * 10 + (itemInstanceData.quality ?? 0), }; } // if a damageType isn't found, use the item's energy capacity element instead const element = (itemInstanceData.damageTypeHash !== undefined && defs.DamageType.get(itemInstanceData.damageTypeHash)) || (itemDef.defaultDamageTypeHash !== undefined && defs.DamageType.get(itemDef.defaultDamageTypeHash)) || // Subclasses have their elemental damage type in the talent grid (normalBucket.hash === BucketHashes.Subclass && itemDef.talentGrid?.hudDamageType !== undefined && getDamageDefsByDamageType(defs)[itemDef.talentGrid.hudDamageType]) || null; const hiddenOverlay = itemDef.iconWatermark; const tooltipNotifications = item.tooltipNotificationIndexes?.length ? item.tooltipNotificationIndexes // guarding against our special case for Synth bounties in case things change .filter((i) => i >= 0 && i < itemDef.tooltipNotifications?.length) // why the optional chain? well, somehow, an item can return tooltipNotificationIndexes, // but have no tooltipNotifications in its def .map((i) => itemDef.tooltipNotifications?.[i]) : emptyArray(); // null out falsy values like a blank string for a url const iconOverlay = (itemDef.isFeaturedItem && itemDef.iconWatermarkFeatured) || (item.versionNumber !== undefined && itemDef.quality?.displayVersionWatermarkIcons?.[item.versionNumber]) || itemDef.iconWatermark || itemDef.iconWatermarkShelved || undefined; const collectible = createCollectibleFinder(defs)(itemDef); // Do we need this now? const source = collectible?.sourceHash; // items' appearance can be overridden at bungie's request let overrideStyleItem = item.overrideStyleItemHash ? defs.InventoryItem.get(item.overrideStyleItemHash) : null; if (overrideStyleItem?.plug?.isDummyPlug) { overrideStyleItem = null; } // Quest steps display their title as the quest line name, and their step name in the type position let name = displayProperties.name; let typeName = itemDef.itemTypeDisplayName || 'Unknown'; if ( itemDef.setData?.questLineName && itemDef.setData?.questLineName !== itemDef.displayProperties.name ) { typeName = itemDef.displayProperties.name; name = itemDef.setData.questLineName; } else if (itemDef.objectives?.questlineItemHash) { const questLineItem = defs.InventoryItem.get(itemDef.objectives.questlineItemHash); if (questLineItem && questLineItem.displayProperties.name !== itemDef.displayProperties.name) { typeName = itemDef.displayProperties.name; name = questLineItem.displayProperties.name; } } const itemCategoryHashes = getItemCategoryHashes(itemDef); let classType = itemDef.classType; // We cannot trust the defined class of redacted items, // and adjust their claimed classType based on their status in the user's inventory. if (itemDef.redacted) { classType = normalBucket.inArmor ? itemInstanceData?.isEquipped && owner ? // equipped armor gets marked as that character's class owner.classType : // unequipped armor gets marked "no class" (assume/allow nothing) DestinyClass.Classified : // other items are marked "any class"/unknown DestinyClass.Unknown; } const iconDef = displayProperties.iconHash ? defs.Icon.get(displayProperties.iconHash) : undefined; const ornamentIconDef = overrideStyleItem?.displayProperties.iconHash ? defs.Icon.get(overrideStyleItem.displayProperties.iconHash) : undefined; const isExotic = itemDef.inventory!.tierType === TierType.Exotic; const createdItem: DimItem = { owner: owner?.id || 'unknown', // figure out what year this item is probably from destinyVersion: 2, // The bucket the item is currently in location: currentBucket, // The bucket the item normally resides in (even though it may be in the vault/postmaster) bucket: normalBucket, hash: item.itemHash, itemCategoryHashes, rarity: ItemRarityMap[itemDef.inventory!.tierType] || 'Common', isExotic, name, description: displayProperties.description, icon: overrideStyleItem?.displayProperties.icon || displayProperties.icon || d2MissingIcon, hiddenOverlay, iconOverlay, secondaryIcon: overrideStyleItem?.secondaryIcon || itemDef.secondaryIcon || itemDef.screenshot, iconDef, ornamentIconDef, notransfer: Boolean( itemDef.nonTransferrable || item.transferStatus === TransferStatuses.NotTransferrable, ), canPullFromPostmaster: !itemDef.doesPostmasterPullHaveSideEffects, id: item.itemInstanceId || '0', // zero for non-instanced is legacy hack instanced: Boolean(item.itemInstanceId && item.itemInstanceId !== '0'), equipped: Boolean(itemInstanceData.isEquipped), // TODO: equippingBlock has a ton of good info for the item move logic equipment: (itemDef.equippingBlock || // redacted items seem to have a correct boolean but no detailed equipping block info (itemDef.redacted && itemDef.equippable)) && !uniqueEquipBuckets.includes(normalBucket.hash), equippingLabel: itemDef.equippingBlock?.uniqueLabel, complete: false, amount: item.quantity || 1, primaryStat: primaryStat, typeName, equipRequiredLevel: itemInstanceData.equipRequiredLevel ?? 0, maxStackSize: Math.max(itemDef.inventory!.maxStackSize, 1), uniqueStack: Boolean(itemDef.inventory!.stackUniqueLabel?.length), classType, classTypeNameLocalized: getClassTypeNameLocalized(defs)(classType), element, energy: itemInstanceData.energy ?? null, lockable: item.lockable, trackable: Boolean(item.itemInstanceId && itemDef.objectives?.questlineItemHash), tracked: Boolean(item.state & ItemState.Tracked), locked: Boolean(item.state & ItemState.Locked), masterwork: Boolean(item.state & ItemState.Masterwork) && normalBucket.hash !== BucketHashes.Subclass, crafted: item.state & ItemState.Crafted ? 'crafted' : false, highlightedObjective: Boolean(item.state & ItemState.HighlightedObjective), classified: Boolean(itemDef.redacted), isEngram, loreHash: itemDef.loreHash, previewVendor: itemDef.preview?.previewVendorHash, ammoType: itemDef.equippingBlock ? itemDef.equippingBlock.ammoType : DestinyAmmunitionType.None, source, collectibleHash: collectible?.hash, missingSockets: false, displaySource: itemDef.displaySource, plug: itemDef.plug && { energyCost: itemDef.plug.energyCost?.energyCost || 0, }, metricHash: item.metricHash, metricObjective: item.metricObjective, availableMetricCategoryNodeHashes: itemDef.metrics?.availableMetricCategoryNodeHashes, // These get filled in later breakerType: null, percentComplete: 0, hidePercentage: false, stats: null, objectives: undefined, pursuit: null, taggable: false, comparable: false, wishListEnabled: false, power: 0, index: '', infusable: false, infusionFuel: false, sockets: null, masterworkInfo: null, infusionCategoryHashes: null, tooltipNotifications, featured: itemDef.isFeaturedItem, tier: itemInstanceData.gearTier ?? 0, traitHashes: itemDef.traitHashes, adept: itemDef.isAdept, holofoil: itemDef.isHolofoil, }; // *able createdItem.taggable = Boolean( createdItem.lockable || createdItem.classified || // Shaders can be tagged from collections itemDef.itemSubType === DestinyItemSubType.Shader || // Mods can be tagged from collections... (createdItem.itemCategoryHashes.includes(ItemCategoryHashes.Mods_Mod) && // ... but not catalysts !itemDef.traitHashes?.includes(TraitHashes.ItemExoticCatalyst)), ); createdItem.comparable = Boolean( createdItem.equipment && createdItem.lockable && createdItem.bucket.hash !== BucketHashes.Emblems, ); if (createdItem.primaryStat) { createdItem.primaryStatDisplayProperties = defs.Stat.get( createdItem.primaryStat.statHash, ).displayProperties; } try { const socketInfo = buildSockets(item, itemComponents, defs, itemDef); createdItem.sockets = socketInfo.sockets; createdItem.missingSockets = socketInfo.missingSockets; } catch (e) { errorLog(TAG, `Error building sockets for ${createdItem.name}`, item, itemDef, e); reportException('Sockets', e, { itemHash: item.itemHash }); } createdItem.wishListEnabled = Boolean( createdItem.sockets && (createdItem.bucket.inWeapons || // Exotic class items can be wishlisted (createdItem.bucket.hash === BucketHashes.ClassArmor && createdItem.isExotic)), ); // Extract weapon crafting info from the crafted socket but // before building stats because the weapon level affects stats. createdItem.craftedInfo = buildCraftedInfo(createdItem, defs); // Crafting pattern createdItem.patternUnlockRecord = buildPatternInfo( createdItem, itemDef, defs, profileRecords, characterRecords, ); // don't worry, craftable weapons can't be enhanced if (createdItem.crafted && !createdItem.patternUnlockRecord) { createdItem.crafted = 'enhanced'; } // Deepsight Resonance createdItem.deepsightInfo = buildDeepsightInfo(createdItem); // Catalyst if (createdItem.isExotic && createdItem.bucket.inWeapons) { createdItem.catalystInfo = buildCatalystInfo( createdItem.hash, profileRecords, profileResponse.characterRecords?.data, ); } try { createdItem.stats = buildStats(defs, createdItem, customStats, itemDef); } catch (e) { errorLog(TAG, `Error building stats for ${createdItem.name}`, item, itemDef, e); reportException('Stats', e, { itemHash: item.itemHash }); } try { const itemInstancedObjectives = item.itemInstanceId ? itemComponents?.objectives?.data?.[item.itemInstanceId]?.objectives : undefined; const uninstancedItemObjectives = characterProgressions?.uninstancedItemObjectives; const itemUninstancedObjectives = uninstancedItemObjectives?.[item.itemHash]; createdItem.objectives = buildObjectives( itemDef, defs, itemInstancedObjectives, itemUninstancedObjectives, ); } catch (e) { errorLog(TAG, `Error building objectives for ${createdItem.name}`, item, itemDef, e); } if (itemDef.perks?.length) { const uninstancedItemPerks = characterProgressions?.uninstancedItemPerks; const itemUninstancedPerks = uninstancedItemPerks?.[item.itemHash]?.perks; const perks = itemDef.perks.filter( (p, i) => p.perkVisibility === ItemPerkVisibility.Visible && itemUninstancedPerks?.[i]?.visible !== false && defs.SandboxPerk.get(p.perkHash)?.isDisplayable, ); if (perks.length) { createdItem.perks = perks; } } // Compute complete / completion percentage if (createdItem.objectives) { const length = createdItem.objectives.length; if (length > 0) { createdItem.complete = createdItem.objectives.every((o) => o.complete); createdItem.percentComplete = sumBy(createdItem.objectives, (objective) => { if (objective.completionValue) { const checkTrialsPassage = isTrialsPassage(createdItem.hash); // Only the "Wins" objective should count towards completion if (checkTrialsPassage && !isWinsObjective(objective.objectiveHash)) { return 0; } return ( Math.min(1, (objective.progress || 0) / objective.completionValue) / (checkTrialsPassage ? 1 : length) ); } else { return 0; } }); } else { createdItem.hidePercentage = true; } } // a weapon can have an inherent breaker type, or gain one from socketed mods // (or armor mods can sort of add them but let's not go there quite yet) // this is presented as an else-type dichotomy here, but who knows what the future holds if (itemDef.breakerTypeHash) { createdItem.breakerType = defs.BreakerType.get(itemDef.breakerTypeHash); } else if (createdItem.bucket.inWeapons && createdItem.sockets) { const breakerTypeHash = createdItem.sockets.allSockets.find( (s) => s.plugged?.plugDef.breakerTypeHash, )?.plugged?.plugDef.breakerTypeHash; if (breakerTypeHash) { createdItem.breakerType = defs.BreakerType.get(breakerTypeHash); } } if (createdItem.hash in extendedBreaker) { createdItem.breakerType = defs.BreakerType.get(extendedBreaker[createdItem.hash]!); } // TODO: compute this on demand createdItem.foundry = extendedFoundry[createdItem.hash] ?? itemDef.traitIds ?.find((trait) => trait.startsWith('foundry.')) // tex_mechanica ?.replace('_', '-') ?.replace('foundry.', ''); // linear fusion rifles always seem to contain the "fusion rifle" category as well. // it's a fascinating "did you know", but ultimately not useful to us, so we remove it // because we don't want to filter FRs and see LFRs if (createdItem.itemCategoryHashes.includes(ItemCategoryHashes.LinearFusionRifles)) { const fusionRifleLocation = createdItem.itemCategoryHashes.indexOf( ItemCategoryHashes.FusionRifle, ); fusionRifleLocation !== -1 && createdItem.itemCategoryHashes.splice(fusionRifleLocation, 1); } // Infusion createdItem.infusionFuel = Boolean(itemDef.quality?.infusionCategoryHashes?.length); createdItem.infusable = createdItem.infusionFuel && isLegendaryOrBetter(createdItem); createdItem.infusionCategoryHashes = itemDef.quality?.infusionCategoryHashes || null; // Masterwork try { createdItem.masterworkInfo = buildMasterwork(createdItem, defs); } catch (e) { errorLog( 'd2-stores', `Error building masterwork info for ${createdItem.name}`, item, itemDef, e, ); reportException('MasterworkInfo', e, { itemHash: item.itemHash }); } // A crafted weapon with an enhanced intrinsic and two enhanced traits is masterworked // https://github.com/Bungie-net/api/issues/1662 if (createdItem.crafted && createdItem.sockets) { const containsEnhancedIntrinsic = createdItem.sockets.allSockets.some( (s) => s.plugged && enhancedIntrinsics.has(s.plugged.plugDef.hash), ); if ( (containsEnhancedIntrinsic || createdItem.masterworkInfo?.tier === 10) && countEnhancedPerks(createdItem.sockets) >= 2 ) { createdItem.masterwork = true; } } try { buildPursuitInfo(createdItem, item, itemDef); } catch (e) { errorLog(TAG, `Error building Quest info for ${createdItem.name}`, item, itemDef, e); if (e instanceof Error) { reportException('Quest', e, { itemHash: item.itemHash }); } } if (createdItem.primaryStat && lightStats.includes(createdItem.primaryStat.statHash)) { createdItem.power = createdItem.primaryStat.value; } createdItem.index = createItemIndex(createdItem); if (itemDef.equippingBlock?.equipableItemSetHash) { createdItem.setBonus = defs.EquipableItemSet.get(itemDef.equippingBlock.equipableItemSetHash); } return createdItem; } function isLegendaryOrBetter(item: DimItem) { return item.rarity === 'Legendary' || item.rarity === 'Exotic'; } export function getQuestLineInfo( itemDef: DestinyInventoryItemDefinition, ): DimQuestLine | undefined { if (itemDef.inventory?.bucketTypeHash === BucketHashes.Quests && itemDef.setData?.itemList) { const thisStepIndex = itemDef.setData.itemList.findIndex((i) => i.itemHash === itemDef.hash); if (thisStepIndex !== -1) { return { description: itemDef.setData.questLineDescription, questStepNum: thisStepIndex + 1, questStepsTotal: itemDef.setData.itemList.length, }; } } } function getExpirationInfo( item: DestinyItemComponent, itemDef: DestinyInventoryItemDefinition, ): DimPursuitExpiration | undefined { if (item.expirationDate) { return { expirationDate: new Date(item.expirationDate), suppressExpirationWhenObjectivesComplete: Boolean( itemDef.inventory!.suppressExpirationWhenObjectivesComplete, ), expiredInActivityMessage: itemDef.inventory!.expiredInActivityMessage, }; } } function buildPursuitInfo( createdItem: DimItem, item: DestinyItemComponent, itemDef: DestinyInventoryItemDefinition, ) { const rewards = itemDef.value ? itemDef.value.itemValue.filter( (v, i) => v.itemHash && (item.itemValueVisibility?.[i] ?? true), ) : []; const questLine = getQuestLineInfo(itemDef); const expiration = getExpirationInfo(item, itemDef); if (rewards.length || item.expirationDate || questLine) { createdItem.pursuit = { expiration, questLine, rewards, modifierHashes: [], }; } } function getItemCategoryHashes(itemDef: DestinyInventoryItemDefinition): ItemCategoryHashes[] { let itemCategoryHashes: ItemCategoryHashes[] = itemDef.itemCategoryHashes || emptyArray(); if ( itemCategoryHashes.includes(ItemCategoryHashes.Weapon) && !itemCategoryHashes.includes(ItemCategoryHashes.Dummies) && itemCategoryHashes.includes(ItemCategoryHashes.GrenadeLaunchers) && !itemCategoryHashes.includes(ItemCategoryHashes.PowerWeapon) ) { // Special grenade launchers itemCategoryHashes = [ // Special grenade launchers are not heavy grenade launchers ...itemCategoryHashes.filter((ich) => ich !== ItemCategoryHashes.GrenadeLaunchers), -ItemCategoryHashes.GrenadeLaunchers, ]; } if (itemDef.hash in extendedICH) { const additionalICH = extendedICH[itemDef.hash]!; itemCategoryHashes = [...itemCategoryHashes, additionalICH]; // Masks are helmets too if (additionalICH === ItemCategoryHashes.Mask) { itemCategoryHashes = [ ...itemCategoryHashes, ItemCategoryHashes.Helmets, ItemCategoryHashes.Armor, ]; } } return itemCategoryHashes; } ================================================ FILE: src/app/inventory/store/d2-store-factory.ts ================================================ import { startSpan } from '@sentry/browser'; import { t } from 'app/i18next-t'; import { armorStats } from 'app/search/d2-known-values'; import { DimError } from 'app/utils/dim-error'; import { errorLog } from 'app/utils/log'; import { DestinyCharacterComponent, DestinyClass, DestinyGender, DestinyItemComponent, DestinyProfileRecordsComponent, DestinyProfileResponse, DestinyRecordState, } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import vaultBackground from 'images/vault-background.svg'; import vaultIcon from 'images/vault.svg'; import { D2ManifestDefinitions } from '../../destiny2/d2-definitions'; import { bungieNetPath } from '../../dim-ui/BungieImage'; import { DimCharacterStat, DimStore, DimTitle } from '../store-types'; import { ItemCreationContext, processItems } from './d2-item-factory'; export function buildStores(itemCreationContext: ItemCreationContext): DimStore[] { // TODO: components may be hidden (privacy) const { profileResponse } = itemCreationContext; return startSpan({ name: 'processItems' }, () => { if ( !profileResponse.profileInventory.data || !profileResponse.characterInventories.data || !profileResponse.characters.data ) { const additionalErrorMessage = $DIM_FLAVOR === 'dev' ? 'Vault or character inventory was missing - you likely forgot to select \ the required application scopes when registering the app on Bungie.net. \ Please carefully read the instructions in docs/CONTRIBUTING.md -> \ "Get your own API key" and select the required scopes' : undefined; errorLog( 'd2-stores', 'Vault or character inventory was missing - bailing in order to avoid corruption', ); throw new DimError('BungieService.MissingInventory', additionalErrorMessage); } const lastPlayedDate = findLastPlayedDate(profileResponse); const vault = processVault(itemCreationContext); const characters = Object.keys(profileResponse.characters.data).map((characterId) => processCharacter(itemCreationContext, characterId, lastPlayedDate), ); const stores = [...characters, vault]; return stores; }); } /** * Process a single character from its raw form to a DIM store, with all the items. */ function processCharacter( itemCreationContext: ItemCreationContext, characterId: string, lastPlayedDate: Date, ): DimStore { const { defs, buckets, profileResponse } = itemCreationContext; const character = profileResponse.characters.data![characterId]; const characterInventory = profileResponse.characterInventories.data?.[characterId]?.items || []; const profileInventory = profileResponse.profileInventory.data?.items || []; const characterEquipment = profileResponse.characterEquipment.data?.[characterId]?.items || []; const profileRecords = profileResponse.profileRecords?.data; const store = makeCharacter(defs, character, lastPlayedDate, profileRecords); // We work around the weird account-wide buckets by assigning them to the current character const items = characterInventory.concat(characterEquipment); if (store.current) { for (const i of profileInventory) { const bucket = buckets.byHash[i.bucketHash]; // items that can be stored in a vault if (bucket && (bucket.vaultBucket || bucket.hash === BucketHashes.SpecialOrders)) { items.push(i); } } } store.items = processItems(itemCreationContext, store, items); return store; } function processVault(itemCreationContext: ItemCreationContext): DimStore { const { buckets, profileResponse } = itemCreationContext; const profileInventory = profileResponse.profileInventory.data ? profileResponse.profileInventory.data.items : []; const store = makeVault(); const items: DestinyItemComponent[] = []; for (const i of profileInventory) { const bucket = buckets.byHash[i.bucketHash]; // items that cannot be stored in the vault, and are therefore *in* a vault if (bucket && !bucket.vaultBucket && bucket.hash !== BucketHashes.SpecialOrders) { items.push(i); } } store.items = processItems(itemCreationContext, store, items); return store; } /** * Find the date of the most recently played character. */ function findLastPlayedDate(profileInfo: DestinyProfileResponse) { const dateLastPlayed = profileInfo.profile.data?.dateLastPlayed; if (dateLastPlayed) { return new Date(dateLastPlayed); } return new Date(0); } /** * A factory service for producing "stores" (characters or the vault). * The job of filling in their items is left to other code - this is just the basic store itself. */ const genderTypeToEnglish = { [DestinyGender.Male]: 'male', [DestinyGender.Female]: 'female', [DestinyGender.Unknown]: '', } as const; function makeCharacter( defs: D2ManifestDefinitions, character: DestinyCharacterComponent, mostRecentLastPlayed: Date, profileRecords: DestinyProfileRecordsComponent | undefined, ): DimStore { const race = defs.Race.get(character.raceHash); const raceLocalizedName = race.displayProperties.name; const gender = defs.Gender.get(character.genderHash); const classy = defs.Class.get(character.classHash); const genderRace = race.genderedRaceNamesByGenderHash[gender.hash]; const className = classy.genderedClassNamesByGenderHash[gender.hash]; const genderLocalizedName = gender.displayProperties.name; const lastPlayed = new Date(character.dateLastPlayed); return { destinyVersion: 2, id: character.characterId, icon: bungieNetPath(character.emblemPath), name: t('ItemService.StoreName', { genderRace: raceLocalizedName, className, }), current: mostRecentLastPlayed.getTime() === lastPlayed.getTime(), lastPlayed, background: bungieNetPath(character.emblemBackgroundPath), level: character.levelProgression.level, // Maybe? percentToNextLevel: character.levelProgression.progressToNextLevel / character.levelProgression.nextLevelAt, powerLevel: character.light, stats: getCharacterStatsData(defs, character.stats), classType: classy.classType, className, gender: genderLocalizedName, race: raceLocalizedName, genderRace, genderName: genderTypeToEnglish[gender.genderType] ?? '', genderHash: character.genderHash, isVault: false, color: character.emblemColor, titleInfo: character.titleRecordHash ? getTitleInfo(character.titleRecordHash, defs, profileRecords, character.genderHash) : undefined, items: [], hadErrors: false, }; } function makeVault(): DimStore { const vaultName = t('Bucket.Vault'); return { destinyVersion: 2, id: 'vault', name: vaultName, classType: DestinyClass.Unknown, current: false, className: vaultName, genderName: '', lastPlayed: new Date(-1), icon: vaultIcon, background: vaultBackground, items: [], isVault: true, color: { red: 49, green: 50, blue: 51, alpha: 1 }, level: 0, percentToNextLevel: 0, powerLevel: 0, gender: '', race: '', genderRace: '', stats: [], hadErrors: false, }; } /** * Compute character-level stats. */ export function getCharacterStatsData( defs: D2ManifestDefinitions, stats: { [key: number]: number; }, ): { [hash: number]: DimCharacterStat } { const statAllowList = armorStats; const ret: { [hash: number]: DimCharacterStat } = {}; // TODO: Fill in effect and countdown for D2 stats // Fill in missing stats for (const statHash of statAllowList) { const def = defs.Stat.get(statHash); const value = stats[statHash] || 0; const stat: DimCharacterStat = { hash: statHash, displayProperties: def.displayProperties, value, }; ret[statHash] = stat; } return ret; } export function getTitleInfo( titleRecordHash: number, defs: D2ManifestDefinitions, profileRecords: DestinyProfileRecordsComponent | undefined, genderHash: number, ): DimTitle | undefined { // Titles can be classified, in which case `titleInfo` is missing const titleInfo = defs?.Record.get(titleRecordHash)?.titleInfo; if (!titleInfo) { return undefined; } const title = titleInfo.titlesByGenderHash?.[genderHash]; if (!title) { return undefined; } let gildedNum = 0; let isGildedForCurrentSeason = false; const isCompleted = Boolean( (profileRecords?.records[titleRecordHash]?.state ?? 0) & DestinyRecordState.RecordRedeemed, ); // Gilding information is stored per-profile, not per-character if (titleInfo.gildingTrackingRecordHash) { const gildedRecord = profileRecords?.records[titleInfo.gildingTrackingRecordHash]; if (gildedRecord?.completedCount) { gildedNum = gildedRecord.completedCount; } isGildedForCurrentSeason = Boolean( gildedRecord && !(gildedRecord.state & DestinyRecordState.ObjectiveNotCompleted), ); } return { title, isCompleted, gildedNum, isGildedForCurrentSeason }; } ================================================ FILE: src/app/inventory/store/deepsight.ts ================================================ import { THE_FORBIDDEN_BUCKET } from 'app/search/d2-known-values'; import { socketContainsPlugWithCategory } from 'app/utils/socket-utils'; import { PlugCategoryHashes } from 'data/d2/generated-enums'; import { DimItem, DimSocket } from '../item-types'; export function buildDeepsightInfo(item: DimItem): boolean { const resonanceSocket = getResonanceSocket(item); return Boolean(resonanceSocket?.visibleInGame); } function getResonanceSocket(item: DimItem): DimSocket | undefined { if (item.sockets && (item.bucket.inWeapons || item.bucket.hash === THE_FORBIDDEN_BUCKET)) { return item.sockets.allSockets.find(isDeepsightResonanceSocket); } } export function isDeepsightResonanceSocket(socket: DimSocket): boolean { return Boolean( socketContainsPlugWithCategory(socket, PlugCategoryHashes.CraftingPlugsWeaponsModsMemories), ); } export function isHarmonizable(item: DimItem): boolean | undefined { const isItemHarmonizable = item.sockets?.allSockets.some( (s) => s.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.CraftingPlugsWeaponsModsExtractors && s.visibleInGame, ); return isItemHarmonizable; } ================================================ FILE: src/app/inventory/store/energy.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { compareBy } from 'app/utils/comparators'; import { getFirstSocketByCategoryHash } from 'app/utils/socket-utils'; import { DestinyInventoryItemDefinition, DestinyItemQuantity, PlugAvailabilityMode, } from 'bungie-api-ts/destiny2'; import { PlugCategoryHashes, SocketCategoryHashes } from 'data/d2/generated-enums'; import { DimItem, PluggableInventoryItemDefinition } from '../item-types'; /** * OK, the rules are worse than this. An item gets a few options it can choose from - * upgrade by 1. It does this using mod items that are duplicated (different mods for regular items and exotics), * so you need to find the right mod from a set of possible identical copies. We can do this by looking at the socket's * reusablePlugSetHash. */ export function getEnergyUpgradePlugs(item: DimItem) { const tierSocket = item.sockets && (getFirstSocketByCategoryHash(item.sockets, SocketCategoryHashes.ArmorTier) || getFirstSocketByCategoryHash(item.sockets, SocketCategoryHashes.GhostTier)); if (!tierSocket?.plugSet || !item.energy) { return []; } const oldEnergyType = item.energy.energyType; const energyMods: PluggableInventoryItemDefinition[] = []; for (const dimPlug of tierSocket.plugSet.plugs) { const capacity = dimPlug.plugDef.plug.energyCapacity; if (!capacity) { continue; } const plugAvailability = dimPlug.plugDef.plug.plugAvailability; // We're looking for all the upgrade mods between here and there if ( (dimPlug.plugDef.plug.plugCategoryHash === PlugCategoryHashes.V460PlugsArmorMasterworksStatResistance2 || dimPlug.plugDef.plug.plugCategoryHash === PlugCategoryHashes.PlugsGhostsMasterworks) && capacity.energyType === oldEnergyType && plugAvailability === PlugAvailabilityMode.AvailableIfSocketContainsMatchingPlugCategory ) { energyMods.push(dimPlug.plugDef); } } return energyMods.sort(compareBy((i) => i.plug?.energyCapacity?.capacityValue ?? 0)); } export function getEnergyUpgradeHashes(item: DimItem, newEnergyCapacity: number) { const oldEnergyCapacity = item.energy?.energyCapacity ?? 1; return getEnergyUpgradePlugs(item) .filter( (plug) => plug.plug.energyCapacity!.capacityValue <= newEnergyCapacity && plug.plug.energyCapacity!.capacityValue > oldEnergyCapacity, ) .map((p) => p.hash); } export function sumModCosts( defs: D2ManifestDefinitions, mods: DestinyInventoryItemDefinition[], ): DestinyItemQuantity[] { const costs: { [itemHash: number]: number } = {}; for (const mod of mods) { if (!mod.plug) { continue; } const materials = defs.MaterialRequirementSet.get(mod.plug.insertionMaterialRequirementHash); for (const material of materials.materials) { costs[material.itemHash] ||= 0; costs[material.itemHash] += material.count; } } return Object.entries(costs).map(([itemHashStr, quantity]) => ({ itemHash: parseInt(itemHashStr, 10), quantity, hasConditionalVisibility: false, })); } ================================================ FILE: src/app/inventory/store/enhanced-info.d.ts ================================================ declare module 'data/d2/extended-breaker.json' { const x: { readonly [hash: number]: number | undefined }; export default x; } declare module 'data/d2/extended-foundry.json' { const x: { readonly [hash: number]: string | undefined }; export default x; } declare module 'data/d2/extended-ich.json' { const x: { readonly [hash: number]: number | undefined }; export default x; } ================================================ FILE: src/app/inventory/store/exotic-class-item.ts ================================================ // Exotic class items' exotic intrinsic sockets don't correspond to plug sets. // Thus we have to maintain manual lists of what can roll for each. export const exoticClassItemPlugs: { [itemHash: number]: { [socketIndex: number]: number[] | undefined } | undefined; } = { 266021826: { 10: [ 1476923952, 1476923953, 1476923954, 3573490509, 3573490508, 3573490511, 3573490510, 3573490505, ], 11: [ 1476923955, 1476923956, 1476923957, 3573490504, 3573490507, 3573490506, 3573490501, 3573490500, ], }, 2273643087: { 10: [1476923952, 1476923953, 1476923954, 183430248, 183430255, 183430252, 183430253, 183430250], 11: [1476923955, 1476923956, 1476923957, 183430251, 183430254, 183430249, 183430246, 183430247], }, 2809120022: { 10: [ 1476923952, 1476923953, 1476923954, 3751917999, 3751917998, 3751917997, 3751917996, 3751917995, ], 11: [ 1476923955, 1476923956, 1476923957, 3751917994, 3751917993, 3751917992, 3751917991, 3751917990, ], }, }; ================================================ FILE: src/app/inventory/store/exotic-to-catalyst-record.d.ts ================================================ declare module 'data/d2/exotic-to-catalyst-record.json' { const x: { readonly [hash: number]: number | undefined }; export default x; } ================================================ FILE: src/app/inventory/store/hooks.ts ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import { getSetBonusStatus } from 'app/item-popup/SetBonus'; import { useD2Definitions } from 'app/manifest/selectors'; import { refresh$ } from 'app/shell/refresh-events'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { useEventBusListener } from 'app/utils/hooks'; import { useCallback, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import { itemMoved } from '../actions'; import { CrossTabMessage, useCrossTabUpdates } from '../cross-tab'; import { loadStores as d1LoadStores } from '../d1-stores'; import { loadStores as d2LoadStores } from '../d2-stores'; import { equippedItemsSelector, storesLoadedSelector } from '../selectors'; /** * A simple hook (probably too simple!) that loads and refreshes stores. This is * meant for use by top level pages. useDispatch() is cheap because it just * listens to a context that never changes. */ export function useLoadStores(account: DestinyAccount | undefined) { const dispatch = useThunkDispatch(); const loaded = useSelector(storesLoadedSelector); useEffect(() => { if (account && !loaded) { if (account?.destinyVersion === 2) { dispatch(d2LoadStores()); } else { dispatch(d1LoadStores()); } } }, [account, dispatch, loaded]); useEventBusListener( refresh$, useCallback(() => { if (account) { if (account?.destinyVersion === 2) { return dispatch(d2LoadStores()); } else { return dispatch(d1LoadStores()); } } }, [account, dispatch]), ); const { pathname } = useLocation(); const onOptimizerPage = pathname.endsWith('/optimizer'); const onMessage = useCallback( (msg: CrossTabMessage) => { switch (msg.type) { case 'stores-updated': // This is only implemented for D2 if (account?.destinyVersion === 2 && !onOptimizerPage) { return dispatch(d2LoadStores({ fromOtherTab: true })); } break; case 'item-moved': if (account?.destinyVersion === 2) { dispatch(itemMoved(msg)); } break; } }, [account?.destinyVersion, dispatch, onOptimizerPage], ); useCrossTabUpdates(onMessage); return loaded; } export function useCurrentSetBonus(storeId: string) { const equippedItems = useSelector(equippedItemsSelector(storeId)); const defs = useD2Definitions()!; return useMemo(() => getSetBonusStatus(defs, equippedItems), [equippedItems, defs]); } ================================================ FILE: src/app/inventory/store/item-index.ts ================================================ import { DimItem } from '../item-types'; let _idTracker: { [id: string]: number } = {}; export function resetItemIndexGenerator() { _idTracker = {}; } /** Set an ID for the item that should be unique across all items */ export function createItemIndex(item: DimItem): string { // Try to make a unique, but stable ID. This isn't always possible, such as in the case of consumables. if (item.id === '0') { _idTracker[item.hash] ||= 0; _idTracker[item.hash]++; return `${item.hash}-t${_idTracker[item.hash]}`; } return item.id; } ================================================ FILE: src/app/inventory/store/masterwork.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { isEmpty } from 'app/utils/collections'; import { isArmor3MasterworkSocket } from 'app/utils/item-utils'; import { getFirstSocketByCategoryHash, isWeaponMasterworkSocket } from 'app/utils/socket-utils'; import { DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2'; import enhancedIntrinsics from 'data/d2/crafting-enhanced-intrinsics'; import { ItemCategoryHashes, PlugCategoryHashes, SocketCategoryHashes, StatHashes, } from 'data/d2/generated-enums'; import masterworksWithCondStats from 'data/d2/masterworks-with-cond-stats.json'; import { DimItem, DimMasterwork, DimSockets } from '../item-types'; /** * These are the utilities that deal with figuring out Masterwork info. * * This is called from within d2-item-factory.service.ts */ const maxTier = 10; /** * This builds the masterwork info - this isn't whether an item is masterwork, but instead what * "type" of masterwork it is, what the kill tracker value is, etc. Exotic weapons can start having * kill trackers before they're masterworked. */ export function buildMasterwork( createdItem: DimItem, defs: D2ManifestDefinitions, ): DimMasterwork | null { if (!createdItem.sockets) { return null; } return buildMasterworkInfo(createdItem, createdItem.sockets, defs); } /** * Figure out what tier the masterwork is at, if any, and what stats are affected. */ function buildMasterworkInfo( createdItem: DimItem, sockets: DimSockets, defs: D2ManifestDefinitions, ): DimMasterwork | null { // For crafted weapons, the enhanced intrinsic provides masterwork-like stats let masterworkPlug = (createdItem.crafted && getFirstSocketByCategoryHash(sockets, SocketCategoryHashes.IntrinsicTraits)?.plugged) || sockets.allSockets.find(isWeaponMasterworkSocket)?.plugged; // Look for the Edge of Fate masterwork socket if (!masterworkPlug && createdItem.bucket.inArmor) { masterworkPlug = createdItem.sockets?.allSockets.find(isArmor3MasterworkSocket)?.plugged; } if (!masterworkPlug) { return null; } const plugStats = masterworkPlug.stats; const exoticWeapon = createdItem.isExotic && createdItem.bucket?.sort === 'Weapons'; if (!plugStats || isEmpty(plugStats)) { if (exoticWeapon) { return { tier: maxTier, stats: undefined, }; } return null; } const stats: DimMasterwork['stats'] = []; const primaryMWStatHash = enhancedIntrinsics.has(masterworkPlug.plugDef.hash) || masterworksWithCondStats.includes(masterworkPlug.plugDef.hash) ? masterworkPlug.plugDef.investmentStats[0]?.statTypeHash : undefined; for (const [statHash_, stat] of Object.entries(plugStats)) { const statHash = parseInt(statHash_, 10); if (!createdItem.stats?.some((s) => s.statHash === statHash)) { continue; } stats.push({ hash: statHash, name: defs.Stat.get(statHash).displayProperties.name, value: stat.value, isPrimary: primaryMWStatHash === undefined || primaryMWStatHash === statHash, }); } const isArmor3 = masterworkPlug.plugDef.plug.plugCategoryHash === PlugCategoryHashes.V460PlugsArmorMasterworks; const tier = exoticWeapon ? maxTier : isArmor3 ? // Pick any stat that's not the energy stat masterworkPlug.plugDef.investmentStats.find((s) => s.isConditionallyActive)?.value || 0 : Math.abs(masterworkPlug.plugDef.investmentStats[0].value); return { tier, stats, }; } /** * Determine if a masterwork with this primary stat would be a valid * masterwork for this item. */ export function isValidMasterworkStat( defs: D2ManifestDefinitions, itemDef: DestinyInventoryItemDefinition, statHash: number, ) { // Bows have a charge time stat that nobody asked for if ( statHash === StatHashes.ChargeTime && itemDef.itemCategoryHashes?.includes(ItemCategoryHashes.Bows) ) { return false; } // Only swords have an impact masterwork if ( statHash === StatHashes.Impact && !itemDef.itemCategoryHashes?.includes(ItemCategoryHashes.Sword) ) { return false; } const statGroupHash = itemDef.stats!.statGroupHash!; const statGroupDef = defs.StatGroup.get(statGroupHash); return statGroupDef.scaledStats.some((s) => s.statHash === statHash); } ================================================ FILE: src/app/inventory/store/missing-sources.d.ts ================================================ declare module 'data/d1/missing_sources.json' { const x: { readonly [hash: number]: number[] | undefined }; export default x; } ================================================ FILE: src/app/inventory/store/objectives.ts ================================================ import { D1ObjectiveDefinition } from 'app/destiny1/d1-manifest-types'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { HashLookup } from 'app/utils/util-types'; import { DestinyInventoryItemDefinition, DestinyObjectiveDefinition, DestinyObjectiveProgress, DestinyObjectiveUiStyle, DestinyUnlockValueUIStyle, } from 'bungie-api-ts/destiny2'; import trialsHashes from 'data/d2/d2-trials-objectives.json'; /** * These are the utilities that deal with figuring out Objectives for items. * * This is called from within d2-item-factory.service.ts */ /** * Build regular item-level objectives. */ export function buildObjectives( itemDef: DestinyInventoryItemDefinition, defs: D2ManifestDefinitions, itemInstancedObjectives: DestinyObjectiveProgress[] | undefined, itemUninstancedObjectives: DestinyObjectiveProgress[] | undefined, ): DestinyObjectiveProgress[] | undefined { const objectives = itemInstancedObjectives ?? itemUninstancedObjectives ?? []; if (!objectives?.length) { // fill in objectives from its definition. not sure why if there's no available progression data? what case does this catch? if (itemDef.objectives) { return itemDef.objectives.objectiveHashes.map((o) => ({ objectiveHash: o, complete: false, visible: true, completionValue: defs.Objective.get(o).completionValue, })); } return; } // TODO: we could make a tooltip with the location + activities for each objective (and maybe offer a ghost?) return objectives.filter((o) => o.visible && defs.Objective.get(o.objectiveHash)); } export function getValueStyle( objectiveDef: DestinyObjectiveDefinition | D1ObjectiveDefinition | undefined, progress: number, completionValue = 0, ): DestinyUnlockValueUIStyle { return objectiveDef ? ((progress < completionValue ? 'inProgressValueStyle' in objectiveDef ? objectiveDef.inProgressValueStyle : undefined : 'completedValueStyle' in objectiveDef ? objectiveDef.completedValueStyle : undefined) ?? objectiveDef.valueStyle) : DestinyUnlockValueUIStyle.Automatic; } /** * D2 objectiveDef + allowOvercompletion + completionValue 1 mean that * this objective gilds if any non-zero value is set, so the goal of 1 * is just a placeholder and shouldn't be shown. */ export function isObjectiveWithPlaceholderGoal( objectiveDef: DestinyObjectiveDefinition | D1ObjectiveDefinition | undefined, completionValue: number, ) { return ( objectiveDef && 'allowOvercompletion' in objectiveDef && objectiveDef.allowOvercompletion && completionValue === 1 ); } export function isBooleanObjective( objectiveDef: DestinyObjectiveDefinition | D1ObjectiveDefinition, progress: number | undefined, completionValue: number, ) { const isD2Def = 'allowOvercompletion' in objectiveDef; // shaping dates weirdly claim they shouldn't be shown const isShapingDate = isD2Def && objectiveDef.uiStyle === DestinyObjectiveUiStyle.CraftingWeaponTimestamp; // objectives that increment just once const singleTick = !isD2Def || !objectiveDef.allowOvercompletion || !objectiveDef.showValueOnComplete; return ( // if its value style is a checkbox, obviously it's boolean getValueStyle(objectiveDef, progress ?? 0, completionValue) === DestinyUnlockValueUIStyle.Checkbox || // or if it's completed after 1 tick and isn't a shaping date (completionValue === 1 && singleTick && !isShapingDate) ); } export function isTrialsPassage(itemHash: number) { return trialsHashes.passages.includes(itemHash); } /** * Checks if the trials passage is flawless */ export function isFlawlessPassage(objectives: DestinyObjectiveProgress[] | undefined) { return objectives?.some((obj) => isFlawlessObjective(obj.objectiveHash) && obj.complete); } const trialsObjectives: HashLookup = trialsHashes.objectives; export function isFlawlessObjective(objectiveHash: number) { return trialsObjectives[objectiveHash] === 'Flawless'; } export function isWinsObjective(objectiveHash: number) { return trialsObjectives[objectiveHash] === 'Wins'; } export function isRoundsWonObjective(objectiveHash: number) { return trialsObjectives[objectiveHash] === 'Rounds Won'; } ================================================ FILE: src/app/inventory/store/override-sockets.ts ================================================ import { UNSET_PLUG_HASH } from 'app/loadout/known-values'; import { DEFAULT_ORNAMENTS } from 'app/search/d2-known-values'; import { isEmpty } from 'app/utils/collections'; import { errorLog } from 'app/utils/log'; import { enhancedVersion } from 'app/utils/perk-utils'; import { produce } from 'immer'; import { useCallback, useState } from 'react'; import { DimItem, DimPlug, DimSocket } from '../item-types'; import { ItemCreationContext } from './d2-item-factory'; import { buildDefinedPlug } from './sockets'; import { buildStats } from './stats'; /** * Socket overrides are a map from socket index to plug item hash. The plug item hash * should be one of the socket's plugOptions (or at least a valid plug for that socket). */ export interface SocketOverrides { [socketIndex: number]: number; } /** * Transform an item into a new item whose properties (mostly stats) reflect the chosen socket overrides. */ export function applySocketOverrides( // We don't need everything here but I'm assuming over time we'll want to plumb more stuff into stats calculations? { defs, customStats }: ItemCreationContext, item: DimItem, socketOverrides: SocketOverrides | undefined, ): DimItem { if (!socketOverrides || isEmpty(socketOverrides) || !item.sockets) { return item; } let icon = item.icon; const sockets = item.sockets.allSockets.map((s): DimSocket => { const override = socketOverrides[s.socketIndex]; // We need to shallow-clone all the plugs because the stats process will re-set them! // We also do some work here to make sure that we can compare the different plugs by reference. // This happens even for sockets that don't change, because we're going to regenerate their // stats and who knows, they could end up different than the original and we wouldn't want to // overwrite them. let plugOptions: DimPlug[] = s.plugOptions.map((p) => ({ ...p, stats: null })); if (override && override !== UNSET_PLUG_HASH && s.plugged?.plugDef.hash !== override) { let newPlug, actuallyPlugged; if (s.isPerk) { newPlug = plugOptions.find((p) => p.plugDef.hash === override) ?? plugOptions.find((p) => enhancedVersion(p.plugDef.hash) === override); actuallyPlugged = plugOptions.find((p) => p.plugDef.hash === s.plugged?.plugDef.hash); } else { // This is likely a mod selection! const createdPlug = buildDefinedPlug(defs, override); if (createdPlug) { newPlug = createdPlug; // Mod sockets' plugOptions only ever // contain the currently plugged item actuallyPlugged = plugOptions.find((p) => p.plugDef.hash === s.plugged?.plugDef.hash); plugOptions = [newPlug]; } } if (newPlug) { // If this is an ornament, override the item's icon as well if (newPlug.plugDef.plug.plugCategoryIdentifier.includes('skins')) { if (DEFAULT_ORNAMENTS.includes(newPlug.plugDef.hash)) { icon = defs.InventoryItem.get(item.hash).displayProperties.icon; } else { icon = newPlug.plugDef.displayProperties.icon; } } return { ...s, actuallyPlugged, plugged: newPlug, plugOptions, }; } else { errorLog( 'applySocketOverrides', "Tried to override to a socket that didn't exist in the options", override, s.plugOptions, ); } } // Even for sockets we don't change, we have to make new objects so we don't rewrite the stats of the original item's plugs // and we need to make sure they're referentially comparable const plugged = plugOptions.find((p) => p.plugDef.hash === s.plugged?.plugDef.hash) ?? null; return { ...s, plugged, plugOptions, }; }); const updatedItem: DimItem = { ...item, icon, sockets: { ...item.sockets, allSockets: sockets, }, }; // Recalculate the entire item's stats from scratch given the new plugs updatedItem.stats = buildStats(defs, updatedItem, customStats); return updatedItem; } /** * A hook to manage socket overrides for a single item. */ export function useSocketOverrides(): [ socketOverrides: SocketOverrides, onPlugClicked: (value: { item: DimItem; socket: DimSocket; plugHash: number }) => void, resetOverrides: () => void, ] { const [socketOverrides, setSocketOverrides] = useState({}); const onPlugClicked = useCallback( ({ socket, plugHash }: { item: DimItem; socket: DimSocket; plugHash: number }) => { setSocketOverrides( produce((so) => { if (so[socket.socketIndex] && plugHash === socket.actuallyPlugged?.plugDef.hash) { delete so[socket.socketIndex]; } else { so[socket.socketIndex] = plugHash; } }), ); }, [], ); const resetOverrides = useCallback(() => setSocketOverrides({}), []); return [socketOverrides, onPlugClicked, resetOverrides]; } export interface SocketOverridesForItems { [itemId: string]: SocketOverrides; } export type PlugClickedHandler = (value: { item: DimItem; socket: DimSocket; plugHash: number; }) => void; /** * A hook to manage socket overrides for multiple items. */ export function useSocketOverridesForItems( initialOverrides: SocketOverridesForItems = {}, ): [socketOverrides: SocketOverridesForItems, onPlugClicked: PlugClickedHandler] { const [socketOverrides, setSocketOverrides] = useState(initialOverrides); const onPlugClicked = useCallback( ({ item, socket, plugHash }: { item: DimItem; socket: DimSocket; plugHash: number }) => { setSocketOverrides( produce((so) => { if (!so[item.id]) { so[item.id] = {}; } if ( so[item.id][socket.socketIndex] && plugHash === socket.actuallyPlugged?.plugDef.hash ) { delete so[item.id][socket.socketIndex]; } else { so[item.id][socket.socketIndex] = plugHash; } if (isEmpty(so[item.id])) { delete so[item.id]; } }), ); }, [], ); return [socketOverrides, onPlugClicked]; } ================================================ FILE: src/app/inventory/store/patterns.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { THE_FORBIDDEN_BUCKET } from 'app/search/d2-known-values'; import { DestinyInventoryItemDefinition, DestinyProfileRecordsComponent, DestinyProfileResponse, DestinyRecordToastStyle, } from 'bungie-api-ts/destiny2'; import memoizeOne from 'memoize-one'; import { DimItem } from '../item-types'; /** * Generate a table from item name to the record for their crafting pattern. */ const itemNameToCraftingPatternRecordHash = memoizeOne((defs: D2ManifestDefinitions) => { const recordHashesByName: { [itemName: string]: number } = {}; if (defs) { for (const record of Object.values(defs.Record.getAll())) { if (record.completionInfo?.toastStyle === DestinyRecordToastStyle.CraftingRecipeUnlocked) { recordHashesByName[record.displayProperties.name] = record.hash; } } } return recordHashesByName; }); /** * Figure out the associated crafting pattern for this item. */ export function buildPatternInfo( item: DimItem, itemDef: DestinyInventoryItemDefinition, defs: D2ManifestDefinitions, profileRecords: DestinyProfileRecordsComponent | undefined, characterRecords: DestinyProfileResponse['characterRecords']['data'], ) { // Craftable items will have a reference to their recipe item const recipeItemHash = itemDef.inventory?.recipeItemHash; if (!recipeItemHash && itemDef.inventory?.bucketTypeHash !== THE_FORBIDDEN_BUCKET) { return undefined; } // Best we can do so far is to match up crafting patterns to items by their name: https://github.com/DestinyItemManager/DIM/pull/8420#issuecomment-1139188482 const patternRecordHash = itemNameToCraftingPatternRecordHash(defs)[item.name]; if (patternRecordHash) { return ( profileRecords?.records[patternRecordHash] ?? (characterRecords && Object.values(characterRecords)[0].records[patternRecordHash]) ); } return undefined; } ================================================ FILE: src/app/inventory/store/season-d2ai.d.ts ================================================ declare module 'data/d2/source-to-season-v2.json' { const x: { readonly [season: number]: number | undefined }; export default x; } declare module 'data/d2/seasons.json' { const x: { readonly [itemHash: number]: number | undefined }; export default x; } declare module 'data/d2/seasons_backup.json' { const x: { readonly [itemHash: number]: number | undefined }; export default x; } declare module 'data/d2/watermark-to-season.json' { const x: { readonly [watermark: string]: number | undefined }; export default x; } declare module 'data/d2/watermark-to-event.json' { const x: { readonly [watermark: string]: import('data/d2/d2-event-info-v2').D2EventEnum | undefined; }; export default x; } declare module 'data/d2/events.json' { const x: { readonly [itemHash: number]: import('data/d2/d2-event-info-v2').D2EventEnum | undefined; }; export default x; } ================================================ FILE: src/app/inventory/store/season.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2'; import { D2EventEnum, D2EventInfo } from 'data/d2/d2-event-info-v2'; import { D2CalculatedSeason } from 'data/d2/d2-season-info'; import D2Events from 'data/d2/events.json'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import D2Season from 'data/d2/seasons.json'; import D2SeasonFromSource from 'data/d2/source-to-season-v2.json'; import D2EventFromOverlay from 'data/d2/watermark-to-event.json'; import D2SeasonFromOverlay from 'data/d2/watermark-to-season.json'; import { DimItem } from '../item-types'; /** The Destiny season (D2) that a specific item belongs to. */ // TODO: load this lazily with import(). Requires some rework of the filters code. const D2SourcesToEvent = Object.fromEntries( Object.entries(D2EventInfo).flatMap(([index, event]) => event.sources.map((source) => [source, Number(index) as D2EventEnum]), ), ); export function getSeason( item: DimItem | DestinyInventoryItemDefinition, defs?: D2ManifestDefinitions, ): number { const asDimItem = ('destinyVersion' in item && item) || undefined; const asDef = ('displayProperties' in item && item) || undefined; if (asDimItem?.classified || asDef?.redacted) { return D2CalculatedSeason; } if ( !item.itemCategoryHashes || item.itemCategoryHashes.includes(ItemCategoryHashes.Materials) || item.itemCategoryHashes.includes(ItemCategoryHashes.Dummies) || item.itemCategoryHashes.length === 0 ) { return -1; } if (asDef) { const source = asDef.collectibleHash && defs?.Collectible.get(asDef.collectibleHash)?.sourceHash; const currentVersion = asDef.quality?.currentVersion; const iconOverlay = (currentVersion !== undefined && asDef.quality?.displayVersionWatermarkIcons?.[currentVersion]) || asDef.iconWatermark || asDef.iconWatermarkShelved || undefined; return getSeasonFromOverlayAndSource(iconOverlay, source, asDef.hash); } else if (asDimItem) { return getSeasonFromOverlayAndSource( asDimItem.iconOverlay || asDimItem.hiddenOverlay, asDimItem.source, asDimItem.hash, ); } else { return D2CalculatedSeason; } } function getSeasonFromOverlayAndSource( overlay: string | undefined, source: number | undefined, hash: number, ) { if (overlay && D2SeasonFromOverlay[overlay]) { return D2SeasonFromOverlay[overlay]; } if (source && D2SeasonFromSource[source]) { return D2SeasonFromSource[source]; } return D2Season[hash] || D2CalculatedSeason; } /** The Destiny event (D2) that a specific item belongs to. */ export function getEvent(item: DimItem): D2EventEnum | undefined { // hiddenOverlay has precedence for event const overlay = item.hiddenOverlay || item.iconOverlay; if (overlay && D2EventFromOverlay[overlay]) { return D2EventFromOverlay[overlay]; } if (item.source && D2SourcesToEvent[item.source]) { return D2SourcesToEvent[item.source]; } return D2Events[item.hash]; } ================================================ FILE: src/app/inventory/store/selectors.ts ================================================ import { maxLightItemSet } from 'app/loadout-drawer/auto-loadouts'; import { getLight } from 'app/loadout-drawer/loadout-utils'; import { powerLevelByKeyword } from 'app/search/power-levels'; import { RootState } from 'app/store/types'; import { createSelector } from 'reselect'; import { DimItem } from '../item-types'; import { allItemsSelector, storesSelector } from '../selectors'; import { getArtifactBonus } from '../stores-helpers'; /** * Does this store (character) have any classified items that might affect their power level? * Things to consider: * - Classified items don't always lack a power level. * - If a char has an equippable item at pinnacle cap in a particular slot, * who cares if there's a classified item in that slot? Not like it's higher. * * This relies on a precalculated set generated from allItems, using getBucketsWithClassifiedItems. */ function hasAffectingClassified( unrestrictedMaxLightGear: DimItem[], bucketsWithClassifieds: Set, ) { return unrestrictedMaxLightGear.some( (i) => // isn't pinnacle cap i.power !== powerLevelByKeyword.pinnaclecap && // and shares a bucket with a classified item (which might be higher power) bucketsWithClassifieds.has(i.bucket.hash), ); } /** figures out which buckets contain classified items */ function getBucketsWithClassifiedItems(allItems: DimItem[]) { const bucketsWithClassifieds = new Set(); for (const i of allItems) { if (i.classified && !i.power && (i.location.inWeapons || i.location.inArmor)) { bucketsWithClassifieds.add(i.bucket.hash); } } return bucketsWithClassifieds; } export interface StorePowerLevel { /** average of your highest gear, even if not equippable at the same time */ maxGearPower: number; /** average of your highest simultaneously equippable gear */ maxEquippableGearPower: number; /** average of your highest gear in each bucket, even if it's not equippable by this store. destiny's new power algorithm as of The Final Shape */ dropPower: number; /** currently represents the power level bonus provided by the Seasonal Artifact */ powerModifier: number; /** maxGearPower + powerModifier. the highest PL you can get your inventory screen to show */ maxTotalPower: number; /** the highest-power items per bucket, even if not equippable at the same time */ highestPowerItems: DimItem[]; /** the highest-power simultaneously equippable gear */ maxEquippablePowerItems: DimItem[]; /** the highest-power items per bucket, even if it's not equippable by this store. destiny's new power algorithm as of The Final Shape */ dropCalcItems: DimItem[]; /** maxGearPower and maxTotalPower can come with various caveats */ problems: { /** this stat may be inaccurate because it relies on classified items */ hasClassified: boolean; /** mutually excluded exotics are included in the max possible power */ notEquippable: boolean; /** this character is in danger of dropping at a worse Power Level! another character is holding their best item(s) */ notOnStore: boolean; }; } export const allPowerLevelsSelector = createSelector( storesSelector, allItemsSelector, (stores, allItems) => { const bucketsWithClassifieds = getBucketsWithClassifiedItems(allItems); const levels: { [storeId: string]: StorePowerLevel; } = {}; for (const store of stores) { if (store.isVault) { continue; } const { equippable, equipUnrestricted, classUnrestricted } = maxLightItemSet(allItems, store); const dropPowerLevel = getLight(store, classUnrestricted); const unrestrictedMaxGearPower = getLight(store, equipUnrestricted); const equippableMaxGearPower = getLight(store, equippable); const dropPower = getLight(store, classUnrestricted); const notEquippable = unrestrictedMaxGearPower !== equippableMaxGearPower; const notOnStore = dropPowerLevel !== unrestrictedMaxGearPower; const hasClassified = hasAffectingClassified(equipUnrestricted, bucketsWithClassifieds); const artifactPower = getArtifactBonus(store); levels[store.id] = { maxGearPower: unrestrictedMaxGearPower, maxEquippableGearPower: equippableMaxGearPower, maxEquippablePowerItems: equippable, dropPower, powerModifier: artifactPower, maxTotalPower: unrestrictedMaxGearPower + artifactPower, highestPowerItems: equipUnrestricted, dropCalcItems: classUnrestricted, problems: { hasClassified, notEquippable, notOnStore, }, }; } return levels; }, ); export const powerLevelSelector = (state: RootState, storeId: string | undefined) => storeId !== undefined ? allPowerLevelsSelector(state)[storeId] : undefined; export const dropPowerLevelSelector = (storeId: string | undefined) => (state: RootState) => powerLevelSelector(state, storeId)?.dropPower; ================================================ FILE: src/app/inventory/store/sockets.ts ================================================ import { getCraftingTemplate } from 'app/armory/crafting-utils'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DEFAULT_SHADER, GhostActivitySocketTypeHashes, weaponMasterworkY2SocketTypeHash, } from 'app/search/d2-known-values'; import { filterMap } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { emptyArray } from 'app/utils/empty'; import { unenhancedVersion } from 'app/utils/perk-utils'; import { eventArmorRerollSocketIdentifiers, isEnhancedPerk, subclassAbilitySocketCategoryHashes, } from 'app/utils/socket-utils'; import { DestinyInventoryItemDefinition, DestinyItemComponent, DestinyItemComponentSetOfint64, DestinyItemPlugBase, DestinyItemSocketEntryDefinition, DestinyItemSocketEntryPlugItemRandomizedDefinition, DestinyItemSocketState, DestinyObjectiveProgress, DestinyPlugItemCraftingRequirements, DestinySocketCategoryStyle, DestinySocketTypeDefinition, SocketPlugSources, } from 'bungie-api-ts/destiny2'; import { emptyPlugHashes } from 'data/d2/empty-plug-hashes'; import { BucketHashes, ItemCategoryHashes, PlugCategoryHashes, SocketCategoryHashes, } from 'data/d2/generated-enums'; import { partition } from 'es-toolkit'; import { DimItem, DimPlug, DimPlugSet, DimSocket, DimSocketCategory, DimSockets, PluggableInventoryItemDefinition, } from '../item-types'; import { exoticClassItemPlugs } from './exotic-class-item'; // // These are the utilities that deal with Sockets and Plugs on items. Sockets and Plugs // are how perks, mods, and many other things are implemented on items. // // This is called from within d2-item-factory.service.ts // /** * Calculate all the sockets we want to display (or make searchable). Sockets represent perks, * mods, and intrinsic properties of the item. They're really the swiss army knife of item * customization. */ export function buildSockets( item: DestinyItemComponent, itemComponents: Partial | undefined, defs: D2ManifestDefinitions, itemDef: DestinyInventoryItemDefinition, ): { sockets: DimItem['sockets']; missingSockets: DimItem['missingSockets'] } { let sockets: DimSockets | null = null; let missingSockets: DimItem['missingSockets'] = false; if ($featureFlags.simulateMissingSockets) { itemComponents = undefined; } const socketData = (item.itemInstanceId && itemComponents?.sockets?.data?.[item.itemInstanceId]?.sockets) || undefined; const reusablePlugData = (item.itemInstanceId && itemComponents?.reusablePlugs?.data?.[item.itemInstanceId]?.plugs) || undefined; const plugObjectivesData = (item.itemInstanceId && itemComponents?.plugObjectives?.data?.[item.itemInstanceId]?.objectivesPerPlug) || undefined; if (socketData) { sockets = buildInstancedSockets( defs, itemDef, item, socketData, reusablePlugData, plugObjectivesData, ); } // If we didn't have live data (for example, when viewing vendor items or collections), // get sockets from the item definition. if (!sockets && itemDef.sockets) { // a nice long instanceId is a real one and "should" have this data // (short is actually a vendor item index. we'll let that build from defs.) const isInstanced = Boolean(item.itemInstanceId && item.itemInstanceId.length > 14); // If this really *should* have live sockets, but didn't... if (isInstanced && !socketData) { missingSockets = isInstanced ? 'missing' : 'not-loaded'; } sockets = buildDefinedSockets(defs, itemDef); } return { sockets, missingSockets }; } /** * Build sockets that come from the live instance. */ function buildInstancedSockets( defs: D2ManifestDefinitions, itemDef: DestinyInventoryItemDefinition, item: DestinyItemComponent, sockets?: DestinyItemSocketState[], reusablePlugData?: { [key: number]: DestinyItemPlugBase[]; }, plugObjectivesData?: { [key: number]: DestinyObjectiveProgress[]; }, ): DimSockets | null { if (!item.itemInstanceId || !itemDef.sockets?.socketEntries.length || !sockets?.length) { return null; } const createdSockets: DimSocket[] = []; for (let i = 0; i < sockets.length; i++) { const built = buildSocket( defs, sockets[i], itemDef.sockets.socketEntries[i], i, reusablePlugData?.[i], plugObjectivesData, itemDef, ); // There are a bunch of garbage sockets that we ignore if (built) { createdSockets.push(built); } } const categories: DimSocketCategory[] = []; for (const category of itemDef.sockets.socketCategories) { categories.push({ category: defs.SocketCategory.get(category.socketCategoryHash, itemDef), socketIndexes: category.socketIndexes, }); } return { allSockets: createdSockets, // Flat list of sockets categories: itemDef.inventory?.bucketTypeHash === BucketHashes.Subclass ? categories.sort(compareBy((c) => c.category?.index)) : categories, // Sockets organized by category fromDefinitions: false, }; } /** * Build sockets that come from only the definition. We won't be able to tell which ones are selected. */ function buildDefinedSockets( defs: D2ManifestDefinitions, itemDef: DestinyInventoryItemDefinition, ): DimSockets | null { // if we made it here, item has sockets const socketDefEntries = itemDef.sockets!.socketEntries; if (!socketDefEntries?.length) { return null; } const craftingTemplateSockets = getCraftingTemplate(defs, itemDef.hash)?.sockets!.socketEntries; const createdSockets: DimSocket[] = []; // TODO: check out intrinsicsockets as well for (let i = 0; i < socketDefEntries.length; i++) { const socketDef = socketDefEntries[i]; const built = buildDefinedSocket( defs, socketDef, i, craftingTemplateSockets?.[i]?.reusablePlugSetHash, itemDef, ); // There are a bunch of garbage sockets that we ignore if (built) { createdSockets.push(built); } } const categories: DimSocketCategory[] = []; for (const category of itemDef.sockets!.socketCategories) { categories.push({ category: defs.SocketCategory.get(category.socketCategoryHash), socketIndexes: category.socketIndexes, }); } return { allSockets: createdSockets, // Flat list of sockets categories: itemDef.inventory?.bucketTypeHash === BucketHashes.Subclass ? categories.sort(compareBy((c) => c.category?.index)) : categories, // Sockets organized by category fromDefinitions: true, }; } function filterReusablePlug(reusablePlug: DimPlug) { return ( !reusablePlug.plugDef.itemCategoryHashes?.some( (ich) => ich === ItemCategoryHashes.MasterworksMods || ich === ItemCategoryHashes.GhostModsProjections, ) && !reusablePlug.plugDef.plug?.plugCategoryIdentifier.includes('masterworks.stat') ); } /** * Some craftable items have perks that can no longer roll, but the enhanced * version of those perks still shows up in the list and claims to be roll-able. * This helps us filter them out. */ function isUncraftableEnhancedPerk( built: DimPlug, craftingRequirements: DestinyPlugItemCraftingRequirements | undefined, ) { return ( isEnhancedPerk(built.plugDef) && craftingRequirements?.unlockRequirements.length === 0 && craftingRequirements.materialRequirementHashes.length === 0 ); } /** * Build a socket from definitions, without the benefit of live profile info. */ function buildDefinedSocket( defs: D2ManifestDefinitions, socketDef: DestinyItemSocketEntryDefinition, index: number, craftingReusablePlugSetHash: number | undefined, forThisItem?: DestinyInventoryItemDefinition, ): DimSocket | undefined { if (!socketDef) { return undefined; } // a LOT of sockets have socketTypeHash "0", no subsequent data, and should be excluded from consideration const socketTypeDef = socketDef.socketTypeHash && defs.SocketType.get(socketDef.socketTypeHash, forThisItem); if (!socketTypeDef) { return undefined; } const socketCategoryDef = defs.SocketCategory.get(socketTypeDef.socketCategoryHash, forThisItem); if (!socketCategoryDef) { return undefined; } const isReusable = socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.Reusable; // This covers the visible intrinsic perks and armor stat plugs, // for all of which everything but the currently plugged thing are distractions const isIntrinsic = socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.LargePerk; // Is this socket a perk-style socket, or something more general (mod-like)? const isPerk = socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.Unlockable || isIntrinsic || isReusable; const isMod = socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.Consumable || socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.Abilities; // The currently equipped plug, if any const reusablePlugs: DimPlug[] = []; // We only build a larger list of plug options if this is a perk socket, since users would // only want to see (and search) the plug options for perks. For other socket types (mods, shaders, etc.) // we will only populate plugOptions with the currently inserted plug. if (isPerk && !isIntrinsic) { if (socketDef.reusablePlugSetHash) { const plugSet = defs.PlugSet.get(socketDef.reusablePlugSetHash, forThisItem); if (plugSet) { for (const reusablePlug of plugSet.reusablePlugItems) { const built = buildDefinedPlug( defs, reusablePlug.plugItemHash, reusablePlug.currentlyCanRoll, ); if (built && !isUncraftableEnhancedPerk(built, reusablePlug.craftingRequirements)) { reusablePlugs.push(built); } } } } else if (socketDef.randomizedPlugSetHash) { const craftingPlugSetItems = craftingReusablePlugSetHash ? defs.PlugSet.get(craftingReusablePlugSetHash, forThisItem).reusablePlugItems : []; const randomizedPlugSetItems = defs.PlugSet.get(socketDef.randomizedPlugSetHash, forThisItem)?.reusablePlugItems ?? []; // if they're available, process crafted plugs first, because they have level requirements attached // then process things from the randomizedPlugSetItems (can include retired perks) // this would duplicate some plug options, but they are uniqued in a for loop a few lines below // and first (crafted) is preferred in the uniquing const plugSetItems = [...craftingPlugSetItems, ...randomizedPlugSetItems]; if (plugSetItems.length) { // Unique the plugs by hash, but also consider the perk rollable if there's a copy with currentlyCanRoll = true // See https://github.com/DestinyItemManager/DIM/issues/7272 // We use a Map to preserve insertion order - an object would return its values sorted by hash! const plugs = new Map(); for (const randomPlug of plugSetItems) { const existing = plugs.get(randomPlug.plugItemHash); if (!existing || (!existing.currentlyCanRoll && randomPlug.currentlyCanRoll)) { plugs.set(randomPlug.plugItemHash, randomPlug); } } for (const randomPlug of plugs.values()) { const built = buildDefinedPlug( defs, randomPlug.plugItemHash, randomPlug.currentlyCanRoll, ); // we don't want "stat roll" plugs to count as reusablePlugs, but they're almost // indistinguishable from exotic intrinsic armor perks, so we stop them here based // on the fact that they have no name if ( built?.plugDef.displayProperties.name && !isUncraftableEnhancedPerk(built, randomPlug.craftingRequirements) ) { reusablePlugs.push(built); } } } } else if (socketDef.reusablePlugItems && socketDef.reusablePlugItems.length > 0) { for (const reusablePlug of socketDef.reusablePlugItems) { const built = buildDefinedPlug(defs, reusablePlug.plugItemHash); if (built) { reusablePlugs.push(built); } } } else if ( forThisItem && forThisItem.hash in exoticClassItemPlugs && index in exoticClassItemPlugs[forThisItem.hash]! ) { const plugs = exoticClassItemPlugs[forThisItem.hash]![index]!; for (const plugItemHash of plugs) { const built = buildDefinedPlug(defs, plugItemHash); if (built) { reusablePlugs.push(built); } } } else if (socketDef.singleInitialItemHash) { const built = buildDefinedPlug(defs, socketDef.singleInitialItemHash); if (built) { reusablePlugs.push(built); } } } if ( socketDef.singleInitialItemHash && !reusablePlugs.find((rp) => rp.plugDef.hash === socketDef.singleInitialItemHash) ) { const built = buildDefinedPlug(defs, socketDef.singleInitialItemHash); if (built) { reusablePlugs.push({ ...built, unreliablePerkOption: reusablePlugs.length > 0, }); } } const plugOptions: DimPlug[] = []; if (reusablePlugs.length) { for (const reusablePlug of reusablePlugs) { if (filterReusablePlug(reusablePlug)) { plugOptions.push(reusablePlug); } } } // Plugs are already in the right order from the plugset, but some weapons // have retired/collections perks in the middle of the list so we sort them // down. Sort is stable so the existing crafting-cost-order will be preserved. plugOptions.sort( compareBy((p: DimPlug) => // shove retired perks to the bottom (our choice) p.cannotCurrentlyRoll ? 999 : // And collections rolls almost as far p.unreliablePerkOption ? 998 : 0, ), ); // If the socket category is the intrinsic trait, assume that there is only one option and plug it. let plugged: DimPlug | null = null; if ( plugOptions.length === 1 && // this covers weapon intrinsices (socketCategoryDef.hash === SocketCategoryHashes.IntrinsicTraits || // this covers exotic armor perks and stats plugOptions[0].plugDef.plug.plugCategoryHash === PlugCategoryHashes.Intrinsics) ) { plugged = plugOptions[0]; } const plugSet = socketDef.reusablePlugSetHash ? buildCachedDimPlugSet(defs, socketDef.reusablePlugSetHash) : socketDef.randomizedPlugSetHash ? buildCachedDimPlugSet(defs, socketDef.randomizedPlugSetHash) : undefined; return { socketIndex: index, plugged, plugOptions, plugSet, emptyPlugItemHash: findEmptyPlug(socketDef, socketTypeDef, plugSet), reusablePlugItems: emptyArray(), hasRandomizedPlugItems: Boolean(socketDef.randomizedPlugSetHash) || socketTypeDef.alwaysRandomizeSockets, isPerk, isReusable, isMod, socketDefinition: socketDef, }; } /** * verifies a DestinyInventoryItemDefinition is pluggable into a socket * and converts it to a PluggableInventoryItemDefinition */ export function isPluggableItem( itemDef?: DestinyInventoryItemDefinition, ): itemDef is PluggableInventoryItemDefinition { return itemDef?.plug !== undefined; } /** * Converts a list of item hashes to PluggableInventoryItemDefinitions (filtering out ones that aren't plugs) */ export function hashesToPluggableItems( defs: D2ManifestDefinitions, hashes: number[], ): PluggableInventoryItemDefinition[] { return hashes.map((hash) => defs.InventoryItem.get(hash)).filter(isPluggableItem); } function isDestinyItemPlug( plug: DestinyItemPlugBase | DestinyItemSocketState, ): plug is DestinyItemPlugBase { return 'plugItemHash' in plug; } function buildPlug( defs: D2ManifestDefinitions, plug: DestinyItemPlugBase | DestinyItemSocketState, plugObjectivesData: | { [plugItemHash: number]: DestinyObjectiveProgress[]; } | undefined, plugSet: DimPlugSet | undefined, ): DimPlug | null { const destinyItemPlug = isDestinyItemPlug(plug); const plugHash = destinyItemPlug ? plug.plugItemHash : plug.plugHash; if (!plugHash) { return null; } const plugDef = defs.InventoryItem.get(plugHash); if (!plugDef || !isPluggableItem(plugDef)) { return null; } // These are almost never present const failReasons = plug.enableFailIndexes ? filterMap( plug.enableFailIndexes, (index) => plugDef.plug.enabledRules[index]?.failureMessage, ).join('\n') : ''; const enabled = destinyItemPlug ? plug.enabled : plug.isEnabled; const unenhanced = unenhancedVersion(plugDef.hash); return { plugDef, enabled: enabled && (!destinyItemPlug || plug.canInsert), enableFailReasons: failReasons, plugObjectives: plugObjectivesData?.[plugHash] || emptyArray(), stats: null, cannotCurrentlyRoll: plugSet?.plugHashesThatCannotRoll.includes(plugDef.hash) && (!unenhanced || !plugSet?.plugHashesThatCanRoll.includes(unenhanced)), }; } export function buildDefinedPlug( defs: D2ManifestDefinitions, plugHash: number, currentlyCanRoll?: boolean, ): DimPlug | null { const plugDef = plugHash && defs.InventoryItem.get(plugHash); if (!plugDef || !isPluggableItem(plugDef)) { return null; } return { plugDef, enabled: true, enableFailReasons: '', plugObjectives: emptyArray(), stats: null, cannotCurrentlyRoll: currentlyCanRoll === false, }; } function isKnownEmptyPlugItemHash(plugItemHash: number) { return emptyPlugHashes.has(plugItemHash); } // These socket categories never have any empty-able sockets. const noDefaultSocketCategoryHashes: SocketCategoryHashes[] = [ ...subclassAbilitySocketCategoryHashes, SocketCategoryHashes.WeaponPerks_Reusable, SocketCategoryHashes.IntrinsicTraits, SocketCategoryHashes.ArmorPerks_LargePerk, SocketCategoryHashes.ArmorPerks_Reusable, SocketCategoryHashes.ArmorTier, SocketCategoryHashes.GhostTier, SocketCategoryHashes.ClanPerks_Unlockable_ClanBanner, SocketCategoryHashes.GhostShellPerks, SocketCategoryHashes.VehiclePerks, ]; // Because we conservatively fall back to `singleInitialItemHash`, we // may misinterpret some `singleInitialItemHash`es as the empty plug. // If this list gets too large, consider removing the `singleInitialItemHash` fallback, // because it's really just a concession to the fact that D2AI can't ever be 100% complete. const noDefaultPlugIdentifiers: (string | number)[] = [ 'enhancements.exotic', // Exotic Armor Perk sockets (Aeons, all options are equivalent) 'enhancements.artifice.exotic', // This is the dummy "upgrade this armor to artifice" socket ...eventArmorRerollSocketIdentifiers, // Weird rerolling sockets PlugCategoryHashes.ArmorSkinsSharedHead, // FotL Helmet Ornaments ]; /** * DIM sometimes wants to know whether a plug is the "empty" plug so that * it knows not to record an override, or it may choose to reset a socket * back to empty to free up mod space, or it may wish to distinguish the * empty plug in UI sorting. However there's no easy, manifest-driven way * to figure out whether an empty plug exists and if so, what it is. * * The closest thing is the singleInitialItemHash, and that works for many * mod sockets, but it's insufficient in some cases (non-exhaustive): * * 1. The socket may not have a singleInitialItemHash. Artifice artifact mod * slots don't reference any plug in singleInitialItemHash, and the first * entry in the plug set just so happened to be the empty plug. * 2. The socket's singleInitialItemHash may reference a non-empty plug. A lot * of armor references associated shaders here instead. * 3. The singleInitialItemHash is an empty plug, but it's not the proper empty * plug. This happens to void subclass aspect and fragment sockets, and is * really insidious because void subclasses start with these sockets but these * plugs can never be inserted, so we can't use it. */ function findEmptyPlug( socket: DestinyItemSocketEntryDefinition, socketType: DestinySocketTypeDefinition, plugSet: DimPlugSet | undefined, reusablePlugs?: DestinyItemPlugBase[], ) { // First, perform some filtering, both for efficiency and to explicitly // leave emptyPlugItemHash set to undefined for sockets that never have // an empty plug, like abilities etc. if ( // Sockets that ONLY get their items from your inventory necessarily can't be emptied ((socket.plugSources & ~SocketPlugSources.InventorySourced) === 0 && socket.socketTypeHash !== GhostActivitySocketTypeHashes.Locked) || // Y2+ weapon masterworks don't have an "empty" entry. socket.socketTypeHash === weaponMasterworkY2SocketTypeHash || // Socket categories that have no empty plug noDefaultSocketCategoryHashes.includes(socketType.socketCategoryHash) ) { return undefined; } if ( socketType.plugWhitelist.some((whiteListEntry) => noDefaultPlugIdentifiers.some((id) => typeof id === 'number' ? whiteListEntry.categoryHash === id : whiteListEntry.categoryIdentifier.startsWith(id), ), ) ) { return undefined; } // Sometimes the empty plug is a regular plug set entry, sometimes it's one // of the reusablePlugItems. However, reusablePlugItems is thrown away when // there's a PlugSet, so we check the live API response reusablePlugs instead // if available. This is insufficient for shaders on blue items because // neither the API response nor the plugSet have the empty shader. // FIXME #7793: Retain socket.reusablePlugItems when it has unique items // and evaluate whether checking live API response is still necessary const empty = reusablePlugs?.find((p) => isKnownEmptyPlugItemHash(p.plugItemHash))?.plugItemHash || plugSet?.precomputedEmptyPlugItemHash || socket.reusablePlugItems.find((p) => isKnownEmptyPlugItemHash(p.plugItemHash))?.plugItemHash; // Falling back to singleInitialItemHash is the conservative choice: // 1. Before this function existed, we used singleInitialItemHash all the // time and it only broke in specific situations, so we might as well // continue using it when we didn't find a better plug before. // 2. The game has a lot of sockets and we don't want to be updating // D2AI every time a new socket appears -- better to just fix either // the filters above or the D2AI list when something breaks. // // If there's a very good reason to assume a socket can't be emptied, filter it above. return empty ?? (socket.singleInitialItemHash || undefined); } /** * Build information about an individual socket, and its plugs, using live information. */ function buildSocket( defs: D2ManifestDefinitions, socket: DestinyItemSocketState, socketDef: DestinyItemSocketEntryDefinition | undefined, index: number, reusablePlugs?: DestinyItemPlugBase[], plugObjectivesData?: { [plugItemHash: number]: DestinyObjectiveProgress[]; }, forThisItem?: DestinyInventoryItemDefinition, ): DimSocket | undefined { if (!socketDef?.socketTypeHash) { return undefined; } const socketTypeDef = defs.SocketType.get(socketDef.socketTypeHash, forThisItem); if (!socketTypeDef) { return undefined; } const socketCategoryDef = defs.SocketCategory.get(socketTypeDef.socketCategoryHash, forThisItem); if (!socketCategoryDef) { return undefined; } const isReusable = socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.Reusable; // This covers the visible intrinsic perks and armor stat plugs, // for all of which everything but the currently plugged thing are distractions const isIntrinsic = socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.LargePerk; // Is this socket a perk-style socket, or something more general (mod-like)? const isPerk = socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.Unlockable || isIntrinsic || isReusable; const isMod = socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.Consumable || socketCategoryDef.categoryStyle === DestinySocketCategoryStyle.Abilities; const plugSet = socketDef.reusablePlugSetHash ? buildCachedDimPlugSet(defs, socketDef.reusablePlugSetHash) : socketDef.randomizedPlugSetHash ? buildCachedDimPlugSet(defs, socketDef.randomizedPlugSetHash) : undefined; // The currently equipped plug, if any. // This will always be one of the plugOptions -- either it's added // as we look at all available plugs, or it's added after the loops. const plugged = buildPlug(defs, socket, plugObjectivesData, plugSet); let foundPluggedInOptions = false; const plugOptions: DimPlug[] = []; const addPlug = (plug: DimPlug) => { if (!plugOptions.some((p) => p.plugDef.hash === plug.plugDef.hash)) { plugOptions.push(plug); return true; } return false; }; // We only build a larger list of plug options if this is a perk socket, since users would // only want to see (and search) the plug options for perks. For other socket types (mods, shaders, etc.) // we will only populate plugOptions with the currently inserted plug. if (isPerk && !isIntrinsic) { if (reusablePlugs) { // Get options from live info for (const reusablePlug of reusablePlugs) { if (reusablePlug.plugItemHash === plugged?.plugDef.hash) { if (addPlug(plugged)) { foundPluggedInOptions = true; } } else { const built = buildPlug(defs, reusablePlug, plugObjectivesData, plugSet); if (built && filterReusablePlug(built)) { addPlug(built); } } } } else if (socketDef.reusablePlugSetHash) { // Get options from plug set, instead of live info const plugSet = defs.PlugSet.get(socketDef.reusablePlugSetHash, forThisItem); if (plugSet) { for (const reusablePlug of plugSet.reusablePlugItems) { if (reusablePlug.plugItemHash === plugged?.plugDef.hash) { if (addPlug(plugged)) { foundPluggedInOptions = true; } } else { const built = buildDefinedPlug( defs, reusablePlug.plugItemHash, reusablePlug.currentlyCanRoll, ); if (built && filterReusablePlug(built)) { addPlug(built); } } } } } else if (socketDef.reusablePlugItems) { // Get options from definition itself for (const reusablePlug of socketDef.reusablePlugItems) { if (reusablePlug.plugItemHash === plugged?.plugDef.hash) { if (addPlug(plugged)) { foundPluggedInOptions = true; } } else { const built = buildDefinedPlug(defs, reusablePlug.plugItemHash); if (built && filterReusablePlug(built)) { addPlug(built); } } } } } if (plugged && !foundPluggedInOptions) { addPlug(plugged); } // TODO: is this still true? also, should this be ?? instead of || const hasRandomizedPlugItems = Boolean(socketDef?.randomizedPlugSetHash) || socketTypeDef.alwaysRandomizeSockets; return { socketIndex: index, plugged, plugOptions, plugSet, emptyPlugItemHash: findEmptyPlug(socketDef, socketTypeDef, plugSet, reusablePlugs), hasRandomizedPlugItems, reusablePlugItems: reusablePlugs, isPerk, isMod, isReusable, visibleInGame: socket.isVisible, socketDefinition: socketDef, }; } // This cache is used to reuse DimPlugSets across items. If we didn't do this each // item would have their own instances of shaders, which is 100's of plugs. const reusablePlugSetCache: { [plugSetHash: number]: DimPlugSet | undefined } = {}; /** * This builds a DimPlugSet based off the lookup hash for a DestinyPlugSetDefinition. * We cache values so that any DimSocket referring to the same DestinyPlugSetDefinition, * will share the same DimPlugSet instance. */ function buildCachedDimPlugSet(defs: D2ManifestDefinitions, plugSetHash: number): DimPlugSet { const cachedValue = reusablePlugSetCache[plugSetHash]; if (cachedValue) { return cachedValue; } const plugs: DimPlug[] = []; const defPlugSet = defs.PlugSet.get(plugSetHash); let craftingData: DimPlugSet['craftingData']; for (const plugEntry of defPlugSet.reusablePlugItems) { const plug = buildDefinedPlug(defs, plugEntry.plugItemHash, plugEntry.currentlyCanRoll); if (plug) { plugs.push(plug); } if ( plugEntry.craftingRequirements && (plugEntry.craftingRequirements.materialRequirementHashes.length || plugEntry.craftingRequirements.unlockRequirements.length) ) { (craftingData ??= {})[plugEntry.plugItemHash] = plugEntry.craftingRequirements; } } const [cant, can] = partition(plugs, (p) => plugCannotCurrentlyRoll(plugs, p.plugDef.hash)); const dimPlugSet: DimPlugSet = { plugs, hash: plugSetHash, precomputedEmptyPlugItemHash: defPlugSet.reusablePlugItems.find((p) => isKnownEmptyPlugItemHash(p.plugItemHash), )?.plugItemHash, plugHashesThatCannotRoll: cant.map((p) => p.plugDef.hash), plugHashesThatCanRoll: can.map((p) => p.plugDef.hash), craftingData: craftingData, }; reusablePlugSetCache[plugSetHash] = dimPlugSet; return dimPlugSet; } /** * Determine if, given a plugSet, a given plug hash cannot roll. For this to be * true, the plug hash must appear in the list of plugs in the plugSet, and all * versions of that plug in the plugSet cannot currently roll. */ function plugCannotCurrentlyRoll(plugs: DimPlug[], plugHash: number) { // The default shader plug reports that it cannot roll, but it can... if (plugHash === DEFAULT_SHADER) { return false; } let matchingPlugs = false; for (const p of plugs) { if (p.plugDef.hash === plugHash) { matchingPlugs = true; if (!p.cannotCurrentlyRoll) { return false; // we don't need to continue, we know it *can* roll } } } // There is at least one copy of the plug, and all matching copies cannot roll return matchingPlugs; } ================================================ FILE: src/app/inventory/store/stats-conditional.ts ================================================ import { ModsWithConditionalStats } from 'app/search/d2-known-values'; import { filterMap } from 'app/utils/collections'; import { infoLog, warnLog } from 'app/utils/log'; import { weakMemoize } from 'app/utils/memoize'; import { DestinyClass, DestinyItemInvestmentStatDefinition } from 'bungie-api-ts/destiny2'; import enhancedIntrinsics from 'data/d2/crafting-enhanced-intrinsics'; import { PlugCategoryHashes, StatHashes, TraitHashes } from 'data/d2/generated-enums'; import masterworksWithCondStats from 'data/d2/masterworks-with-cond-stats.json'; import { DimItem, DimPlugInvestmentStat, DimStat, PlugStatActivationRule, PluggableInventoryItemDefinition, } from '../item-types'; /** * For a given plug `itemDef` and a `stat`, statically figure out under which conditions * a given `stat` in that itemDef's `investmentStats` is active. Returns undefined * when the stat is always active. */ function getPlugInvestmentStatActivationRule( itemDef: PluggableInventoryItemDefinition, stat: DestinyItemInvestmentStatDefinition, ): PlugStatActivationRule | undefined { // Some Exotic weapon catalysts can be inserted even though the catalyst objectives are incomplete. // In these cases, the catalyst effects are only applied once the objectives are complete. // We'll assume that the item can only be masterworked if its associated catalyst has been completed. if (itemDef.traitHashes?.includes(TraitHashes.ItemExoticCatalyst)) { return { rule: 'masterwork' }; } // Check if this is a tiered weapon masterwork plug stat. The new-style tiered // weapon masterwork plugs have a single unconditional stat at value 10, and // the rest are at value 0, while the old-style masterwork plugs have a single // unconditional stat at value 10, and the rest are at value 3. The new style // masterwork plugs add +tier to *every* stat, even the masterwork stat. if ( itemDef.plug.uiPlugLabel === 'masterwork' && ((stat.isConditionallyActive && stat.value === 0) || (!stat.isConditionallyActive && stat.value === 10 && itemDef.investmentStats.some((s) => s.isConditionallyActive && s.value === 0))) ) { return { rule: 'tieredWeaponMW' }; } // When adding new conditions here that bypass `stat.isConditionallyActive`, update // the fast path below. if (!stat.isConditionallyActive) { // always active return undefined; } // These are preview stats for the Adept enhancing plugs to indicate that enhancing // implicitly upgrades the masterwork to T10 if (itemDef.plug.plugCategoryHash === PlugCategoryHashes.CraftingPlugsWeaponsModsEnhancers) { return { rule: 'never' }; } const defHash = itemDef.hash; // New Armor 3.0 archetypes grant stats only to secondary stats when masterworked. if ( itemDef.plug.plugCategoryHash === PlugCategoryHashes.V460PlugsArmorMasterworks || // The Balanced Tuning mod works the same way - it grants its bonus only to the three lowest stats. defHash === ModsWithConditionalStats.BalancedTuning ) { return { rule: 'archetypeArmorMasterwork' }; } if ( defHash === ModsWithConditionalStats.ElementalCapacitor || defHash === ModsWithConditionalStats.EnhancedElementalCapacitor ) { return { rule: 'never' }; } // It seems unbelievable that these fragments still work the same way as // before Edge of Fate, since they are supposed to affect "class ability // regeneration", and there's now a dedicated stat for that. But no, they're // still conditional and affect different stats based on the class that uses // them. if ( defHash === ModsWithConditionalStats.EchoOfPersistence || defHash === ModsWithConditionalStats.SparkOfFocus ) { // "-10 to the stat that governs your class ability regeneration" const classType = stat.statTypeHash === StatHashes.Weapons ? DestinyClass.Hunter : stat.statTypeHash === StatHashes.Health ? DestinyClass.Titan : stat.statTypeHash === StatHashes.Class ? DestinyClass.Warlock : undefined; if (classType === undefined) { warnLog('plug stats', 'unknown stat effect in', defHash, itemDef.displayProperties?.name); return undefined; } return { rule: 'classType', classType }; } if (masterworksWithCondStats.includes(defHash)) { return { rule: 'adeptWeapon' }; } if (enhancedIntrinsics.has(defHash)) { return { rule: 'enhancedIntrinsic' }; } } /** * This function indicates whether a mod's stat effect is active on the item. * * For example, some subclass plugs reduce a different stat per character class, * which we identify using the passed subclass item, or the classType for static * setups that do not include a particular item. */ export function isPlugStatActive( rule: PlugStatActivationRule, // These options are often necessary to determine if the stat is active. // Providing item and existingStat/statHash is best. { item, classType, existingStat, statHash, }: { item?: DimItem; /** The class we're plugging into, if we aren't plugging an actual item. */ classType?: DestinyClass; /** * The existing stat on the item before applying this plug's stat. Defaults * if item and statHash are provided. */ existingStat?: DimStat; /** The stat being considered, if existingStat isn't provided */ statHash?: number; }, ): boolean { if (!rule) { return true; } const warnMissingItem = () => { warnLog('conditional stats', 'stat condition depends on item but we do not have an item here'); return true; }; switch (rule.rule) { case 'never': return false; case 'archetypeArmorMasterwork': if (!existingStat && statHash && item) { existingStat = item.stats?.find((s) => s.statHash === statHash); } // New Armor 3.0 archetypes grant stats only to secondary stats (base 0) when masterworked, // so if there's already some base stat value, MW will not apply its investmentValue to this stat. return existingStat?.base === 0; case 'classType': classType ??= item?.classType; if (classType === undefined) { warnLog( 'conditional stats', 'stat condition depends on class type but we do not have a class type here', ); return true; } return classType === rule.classType; case 'adeptWeapon': return item?.adept ?? warnMissingItem(); case 'masterwork': return item?.masterwork ?? warnMissingItem(); case 'tieredWeaponMW': // All stats are active for tiered weapon masterworks. return true; case 'enhancedIntrinsic': // Crafted weapons get bonus stats from enhanced intrinsics at Level 20+. // The number 20 isn't in the definitions, so just hardcoding it here. // Alternatively, enhancing an adept weapon gives it an enhanced intrinsic // that gives bonus stats simply because it's an adept weapon, and more if Level 20+. // stats.ts:getPlugStatValue actually takes care of scaling this to the correct bonus. return item ? (item.craftedInfo?.level || 0) >= 20 || item.adept : warnMissingItem(); } } /** * We can't use the investment stats for plugs directly, because some enhanced * perks have multiple entries for the same stat, which need to be added * together. e.g. https://data.destinysets.com/i/InventoryItem:1167468626 This * function combines those entries so that downstream processing can stay * simple. */ function getPlugInvestmentStats( investmentStats: DestinyItemInvestmentStatDefinition[], ): DestinyItemInvestmentStatDefinition[] { const processedStats: DestinyItemInvestmentStatDefinition[] = []; for (const investmentStat of investmentStats) { const existingStatIndex = processedStats.findIndex( (s) => s.statTypeHash === investmentStat.statTypeHash, ); if (existingStatIndex >= 0) { const existingStat = processedStats[existingStatIndex]; // Add the value into the existing stat processedStats[existingStatIndex] = { ...existingStat, value: existingStat.value + investmentStat.value, }; } else { processedStats.push(investmentStat); } } return processedStats; } /** * Turn the investmentStats from `itemDef` into an elaborated form * that more explicitly contains the plug stat activation rules and * fixes some data errors/problems. * * TODO: If/when https://github.com/DestinyItemManager/DIM/issues/9076 * happens and we use DimPlug everywhere, we store the return value in * DimPlug instead of caching here. */ export const mapAndFilterInvestmentStats = weakMemoize( (itemDef: PluggableInventoryItemDefinition): readonly Readonly[] => { let hasDupes: boolean | undefined; const investmentStats = getPlugInvestmentStats(itemDef.investmentStats); // Fast path in case all stats are active and we need no postprocessing. // This needs some knowledge of how `getPlugInvestmentStatActivationRule` works... if ( !itemDef.traitHashes?.includes(TraitHashes.ItemExoticCatalyst) && investmentStats.every((s) => !s.isConditionallyActive) ) { hasDupes = new Set(investmentStats.map((s) => s.statTypeHash)).size !== investmentStats.length; if (!hasDupes) { return investmentStats; } } const stats = filterMap(investmentStats, (stat, index) => { if (itemDef.hash === 2282937672 /* InventoryItem "Bipod" */) { if (investmentStats.length === 4) { // Enhanced Bipod has [-25 blast radius, -15 reload speed, -30 blast radius, -20 reload speed] // investment stats, all conditionally active. Only the lower stats should apply, the others // are from the base perk and included in the defs for whatever reason. if (index >= 2) { return undefined; } } else { warnLog('plug stats', 'enhanced bipod workaround does not apply anymore'); } } const activationRule = getPlugInvestmentStatActivationRule(itemDef, stat); if (activationRule?.rule === 'never') { return undefined; } return { activationRule, statTypeHash: stat.statTypeHash, value: stat.value }; }); // If there are duplicate stats, consolidate them. // This is not particularly efficient, but this should be extraordinarily rare. if (hasDupes) { for (let idx = stats.length - 2; idx >= 0; idx--) { for (let idx2 = stats.length - 1; idx2 > idx; idx2--) { if (stats[idx].statTypeHash === stats[idx2].statTypeHash) { if (stats[idx].activationRule?.rule === stats[idx2].activationRule?.rule) { stats[idx].value += stats[idx2].value; stats.splice(idx2, 1); infoLog('plug stats', 'consolidating stat index', idx2, 'into', idx, itemDef); } else { warnLog( 'plug stats', 'item has duplicated stats with different activity rule, subsequent code will not handle this correctly', itemDef, ); } } } } } return stats; }, ); ================================================ FILE: src/app/inventory/store/stats-custom.ts ================================================ import { CustomStatWeights } from '@destinyitemmanager/dim-api-types'; import { compact } from 'app/utils/collections'; import { DestinyDisplayPropertiesDefinition } from 'bungie-api-ts/destiny2'; import { sum } from 'es-toolkit'; import { DimStat } from '../item-types'; import { getStatSortOrder } from './stats'; /** * given a set of stat weights, builds a valid armor DimStat * by adding up several existing stats (like DIS, MOB, REC) * into a stat with the provided name and description. * * baseOnly true is used to do the classic "custom stat", * a user defined stat meant specifically for comparing armor rolls. * but it can be false to build a combination stat like "Total" */ export function makeCustomStat( stats: DimStat[], statWeights: CustomStatWeights, customStatHash: number, customStatName: string, customStatDesc: string, baseOnly: boolean, ): DimStat | undefined { // the following SEVERAL comment lines are regarding weighted stats, which are not enabled right now // what's averageNonZeroStatWeight for? // we want the effect of all the non-zero multipliers to average out to 1x // like STR x 1 / DIS x 2 / MOB x 3 --- to do this --- STR x .5 / DIS x 1 / MOB x 1.5 // this way, you can exclude a stat by setting it to 0 (which will reduce your total), but // among *included* stats, there's no overall bias toward creating higher or lower total, // except of course when the item has better values in preferred (heavily weighted) stats // also, this way, a weighting that's just 1x's and 0x's, becomes simply // "include these" and "exclude these" which is how original custom total works // as you make weighting more and more lopsided, the highest weighted stat // approaches 2x, and the lowers approach 0, SO: a custom total should top out around // the single stat max (42 base) times a 2x weighting multiplier. let's just call it 100 const nonZeroWeights = compact(Object.values(statWeights)); if (!nonZeroWeights.length) { // everything is zero.... . this is a malformed set of stat weights. skip it. return; } const averageNonZeroStatWeight = sum(nonZeroWeights) / nonZeroWeights.length; let weightedBaseTotal = 0; let weightedBaseMasterworkTotal = 0; let weightedTotal = 0; for (const { base, baseMasterworked, value, statHash } of stats) { const multiplier = statWeights[statHash] || 0; weightedBaseTotal += base * multiplier; weightedBaseMasterworkTotal += (baseMasterworked || 0) * multiplier; weightedTotal += value * multiplier; } weightedBaseTotal = Math.round(weightedBaseTotal / averageNonZeroStatWeight); weightedBaseMasterworkTotal = Math.round(weightedBaseMasterworkTotal / averageNonZeroStatWeight); weightedTotal = Math.round(weightedTotal / averageNonZeroStatWeight); return { investmentValue: 0, statHash: customStatHash, displayProperties: { name: customStatName, description: customStatDesc, } as DestinyDisplayPropertiesDefinition, sort: getStatSortOrder(customStatHash), value: baseOnly ? weightedBaseTotal : weightedTotal, base: weightedBaseTotal, baseMasterworked: weightedBaseMasterworkTotal, maximumValue: 1000, bar: false, smallerIsBetter: false, additive: false, }; } ================================================ FILE: src/app/inventory/store/stats.ts ================================================ import { CustomStatDef } from '@destinyitemmanager/dim-api-types'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { armorStats, evenStatWeights, TOTAL_STAT_HASH } from 'app/search/d2-known-values'; import { compareBy } from 'app/utils/comparators'; import { isArmor3, isClassCompatible } from 'app/utils/item-utils'; import { weakMemoize } from 'app/utils/memoize'; import { DestinyInventoryItemDefinition, DestinyStatAggregationType, DestinyStatCategory, DestinyStatDefinition, DestinyStatDisplayDefinition, DestinyStatGroupDefinition, } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes, StatHashes } from 'data/d2/generated-enums'; import { once, partition } from 'es-toolkit'; import { Draft } from 'immer'; import { socketContainsIntrinsicPlug } from '../../utils/socket-utils'; import { DimItem, DimPlug, DimPlugInvestmentStat, DimSocket, DimStat } from '../item-types'; import { isPlugStatActive, mapAndFilterInvestmentStats } from './stats-conditional'; import { makeCustomStat } from './stats-custom'; /** * These are the utilities that deal with Stats on items - specifically, how to calculate them. * * This is called from within d2-item-factory.service.ts * * the process looks like this: * * buildStats(stats) { * stats = buildInvestmentStats(stats) // based on information from an item's inherent stats * applyPlugsToStats(stats) // mutates stats. adds values provided by sockets (intrinsic armor stats & weapon components) * if (is armor) { * if (any armor stat is missing) fill in missing stats with 0s * synthesize totalStat and add it * synthesize customStat and add it * } * } */ /** * Which stats to display, and in which order. */ export const itemStatAllowList = [ StatHashes.RoundsPerMinute, StatHashes.ChargeTime, StatHashes.DrawTime, StatHashes.BlastRadius, StatHashes.Velocity, StatHashes.Persistence, StatHashes.SwingSpeed, StatHashes.Impact, StatHashes.Range, StatHashes.ShieldDuration, StatHashes.GuardEfficiency, StatHashes.GuardResistance, StatHashes.Accuracy, StatHashes.Stability, StatHashes.Handling, StatHashes.VentSpeed, StatHashes.ChargeRate, StatHashes.GuardEndurance, StatHashes.ReloadSpeed, StatHashes.AimAssistance, StatHashes.AirborneEffectiveness, StatHashes.Zoom, StatHashes.AmmoGeneration, StatHashes.HeatGenerated, StatHashes.CoolingEfficiency, StatHashes.RecoilDirection, StatHashes.Magazine, StatHashes.AmmoCapacity, ...armorStats, TOTAL_STAT_HASH, ]; export function getStatSortOrder(statHash: number) { const order = itemStatAllowList.indexOf(statHash); return order === -1 ? 999999 + Math.abs(statHash) : order; } export function isAllowedItemStat(statHash: number) { return itemStatAllowList.includes(statHash) || statHash < 0; } /** * Stats that are allowed for plugs, in addition to stats their items own. */ const plugStatAllowList = [StatHashes.AspectEnergyCapacity]; export function isAllowedPlugStat(statHash: number) { return plugStatAllowList.includes(statHash); } /** Stats that are measured in milliseconds. */ export const statsMs = [StatHashes.DrawTime, StatHashes.ChargeTime]; /** Stats that should be forced to display without a bar (just a number). */ const statsNoBar = [ StatHashes.RoundsPerMinute, StatHashes.Magazine, StatHashes.RecoilDirection, ...statsMs, ]; /** a dictionary to look up StatDisplay info by statHash */ interface StatDisplayLookup { [statHash: number]: DestinyStatDisplayDefinition | undefined; } /** a dictionary to look up an item's DimStats by statHash */ export interface StatLookup { [statHash: number]: DimStat | undefined; } // apparently worth it, when needing this 100s of times per inv build const memoTotalName = once((): string => t('Stats.Total')); const memoCustomDesc = once((): string => t('Stats.CustomDesc')); const memoStatDisplaysByStatHash = weakMemoize((statGroup: DestinyStatGroupDefinition) => keyByStatHash(statGroup.scaledStats), ); /** Build the full list of stats for an item. If the item has no stats, this returns null. */ export function buildStats( defs: D2ManifestDefinitions, createdItem: DimItem, customStats: CustomStatDef[], itemDef = defs.InventoryItem.get(createdItem.hash), ) { if (!itemDef.stats?.statGroupHash) { return null; } const statGroup = defs.StatGroup.get(itemDef.stats.statGroupHash); if (!statGroup) { return null; } // we re-use this dictionary a bunch of times in subsequent // functions to speed up display info lookups const statDisplaysByStatHash = memoStatDisplaysByStatHash(statGroup); // We use the raw "investment" stats to calculate all item stats (instead of API-reported stats). const investmentStats = buildInvestmentStats(itemDef, defs, statGroup, statDisplaysByStatHash) || []; // Include the contributions from perks and mods applyPlugsToStats(investmentStats, createdItem, statDisplaysByStatHash); if (createdItem.bucket.inArmor) { generateAssumedMasterworkStats(investmentStats, createdItem); // synthesize the "Total" stat for armor // it's effectively just a custom total with 6 stats evenly weighted const tStat = makeCustomStat( investmentStats, evenStatWeights, TOTAL_STAT_HASH, memoTotalName(), '', false, ); investmentStats.push(tStat!); // synthesize custom stats for meaningfully stat-bearing items for (const customStat of customStats) { if (isClassCompatible(customStat.class, createdItem.classType)) { const cStat = makeCustomStat( investmentStats, customStat.weights, customStat.statHash, customStat.label, memoCustomDesc(), true, ); if (cStat) { investmentStats.push(cStat); } } } } return investmentStats.length ? investmentStats.sort(compareBy((s) => s.sort)) : null; } /** * determine if bungie, or our hardcodings, want this stat to be included with the item */ function shouldShowStat( itemDef: DestinyInventoryItemDefinition, statHash: number, statDisplaysByStatHash: StatDisplayLookup, ) { // Bows have a charge time stat that nobody asked for if ( statHash === StatHashes.ChargeTime && itemDef.itemCategoryHashes?.includes(ItemCategoryHashes.Bows) ) { return false; } return Boolean( // Must be on the list of interpolated stats statDisplaysByStatHash[statHash] && // Must be a stat we want to display isAllowedItemStat(statHash), ); } /** * Build stats from the non-pre-sized investment stats. Destiny stats come in two flavors - precalculated * by the API, and "investment stats" which are the raw game values. The latter must be transformed into * what you see in the game, but as a result you can see "hidden" stats at their true value, and calculate * the value that perks and mods contribute to the overall stat value. */ function buildInvestmentStats( itemDef: DestinyInventoryItemDefinition, defs: D2ManifestDefinitions, statGroup: DestinyStatGroupDefinition, statDisplaysByStatHash: StatDisplayLookup, ): DimStat[] { const itemStats = itemDef.investmentStats || []; const ret: DimStat[] = []; for (const itemStat of itemStats) { const statHash = itemStat.statTypeHash; if (!shouldShowStat(itemDef, statHash, statDisplaysByStatHash)) { continue; } const def = defs.Stat.get(statHash); if (!def) { continue; } ret.push( buildStat(itemStat.statTypeHash, itemStat.value, statGroup, def, statDisplaysByStatHash), ); } for (const stat of statGroup.scaledStats) { const statHash = stat.statHash; if (!ret.some((s) => s.statHash === statHash)) { if (!shouldShowStat(itemDef, statHash, statDisplaysByStatHash)) { continue; } const def = defs.Stat.get(statHash); if (!def) { continue; } ret.push(buildStat(statHash, 0, statGroup, def, statDisplaysByStatHash)); } } return ret; } /** * builds and returns a single DimStat, using InvestmentStat information, * stat def, statgroup def, and the item's StatDisplayDefinition, * which determines which stats are displayed and how they are interpolated */ function buildStat( statHash: number, value: number, statGroup: DestinyStatGroupDefinition, statDef: DestinyStatDefinition, statDisplaysByStatHash: StatDisplayLookup, ): DimStat { value ||= 0; const investmentValue = value; let maximumValue = statGroup.maximumValue; let bar = !statsNoBar.includes(statHash); let smallerIsBetter = false; const statDisplay = statDisplaysByStatHash[statHash]; if (statDisplay) { const firstInterp = statDisplay.displayInterpolation[0]; const lastInterp = statDisplay.displayInterpolation.at(-1)!; smallerIsBetter = firstInterp.weight > lastInterp.weight; maximumValue = Math.max(statDisplay.maximumValue, firstInterp.weight, lastInterp.weight); bar = !statDisplay.displayAsNumeric; value = interpolateStatValue(value, statDisplay); } return { investmentValue, statHash, displayProperties: statDef.displayProperties, sort: getStatSortOrder(statHash), value, base: value, maximumValue, bar, smallerIsBetter, // Only set additive for defense stats, because for some reason Zoom is // set to use DestinyStatAggregationType.Character additive: statDef.statCategory === DestinyStatCategory.Defense && statDef.aggregationType === DestinyStatAggregationType.Character, }; } /** * mutates an item's stats according to the item's plugged sockets * (accounting for mods, masterworks, etc) * * also adds the projected stat changes to non-selected DimPlugs */ function applyPlugsToStats( existingStats: DimStat[], // values in this array are mutated createdItem: DimItem, statDisplaysByStatHash: StatDisplayLookup, ) { if (!createdItem.sockets?.allSockets.length) { return; } const existingStatsByHash = keyByStatHash(existingStats); // intrinsic plugs aren't "enhancements", they define the basic stats of armor // we do those first and include them in the stat's base value const [intrinsicSockets, otherSockets] = partition( createdItem.sockets.allSockets, socketContainsIntrinsicPlug, ); const socketLists = [ [true, intrinsicSockets], [false, otherSockets], ] as const; // loop through sockets looking for plugs that modify an item's investmentStats for (const [affectsBase, socketList] of socketLists) { for (const socket of socketList) { // skip this socket+plug if it's disabled or doesn't affect stats if (!socket.plugged?.enabled || !socket.plugged.plugDef.investmentStats) { continue; } for (const pluggedInvestmentStat of mapAndFilterInvestmentStats(socket.plugged.plugDef)) { const affectedStatHash = pluggedInvestmentStat.statTypeHash; const existingStat = existingStatsByHash[affectedStatHash]; // all relevant stats have been added, so if the item doesn't have the stat, we should ignore this if (!existingStat) { continue; } // check special conditionals if ( !isPlugStatActive(pluggedInvestmentStat.activationRule, { item: createdItem, existingStat, }) ) { continue; } // we've ruled out reasons to ignore this investment stat. apply its effects to the investmentValue existingStat.investmentValue += getPlugStatValue(createdItem, pluggedInvestmentStat); // finally, re-interpolate the stat value const statDisplay = statDisplaysByStatHash[affectedStatHash]; const newStatValue = statDisplay ? interpolateStatValue(existingStat.investmentValue, statDisplay) : Math.min(existingStat.investmentValue, existingStat.maximumValue); if (affectsBase) { existingStat.base = newStatValue; } existingStat.value = newStatValue; } } } // We sort the sockets by length so that we count contributions from plugs with fewer options first. // This is because multiple plugs can contribute to the same stat, so we want to sink the non-changeable // stats in first. const sortedSockets = createdItem.sockets.allSockets.toSorted( compareBy((s) => s.plugOptions.length), ); for (const socket of sortedSockets) { attachPlugStats(createdItem, socket, existingStatsByHash, statDisplaysByStatHash); } } /** * Add a baseMasterworked value to armor stats, projecting what they would be if masterworked. * This assumes base stat values are already set. */ function generateAssumedMasterworkStats( existingStats: DimStat[], // values in this array are mutated createdItem: DimItem, ) { // The presence of mod energy identifies armor 2.0 and armor 3.0. if (createdItem.bucket.inArmor && createdItem.energy) { const statsToModify = existingStats.filter((s) => armorStats.includes(s.statHash)); if (isArmor3(createdItem)) { for (const existingStat of statsToModify) { // Armor 3.0 gets a +5 in stats with a base of 0. existingStat.baseMasterworked = existingStat.base || existingStat.base + 5; } } else if (createdItem.energy) { for (const existingStat of statsToModify) { // Armor 2.0 gets 2 stat points in each stat when masterworked existingStat.baseMasterworked = existingStat.base + 2; } } // TODO: What happens to Armor <2? } } function getPlugStatValue(createdItem: DimItem, stat: DimPlugInvestmentStat) { // Adept raid weapons that were randomly acquired can be enhanced to get an // enhanced intrinsic, at which point they're functionally crafted. Their // intrinsic says "conditionally +2 to some stats", but they get +3 because // that's how masterworked adepts behave, and an additional +1 by reaching // weapon level 20. There's no basis for this behavior in the defs, so we // cheat when we calculate live stats and attribute these stats to the // intrinsic since that's the "masterwork". if (stat.activationRule?.rule === 'enhancedIntrinsic' && createdItem.adept) { return stat.value + ((createdItem.craftedInfo?.level ?? 0) >= 20 ? 2 : 1); } // Tiered weapons at max masterwork get +tier to every stat ("Applies // additional stats to this weapon equal to the weapon's tier"). There's // nothing in the defs to indicate this. if (stat.activationRule?.rule === 'tieredWeaponMW') { return stat.value + createdItem.tier; } return stat.value; } /** * Generates the stat modification map for each DimPlug in a DimSocket * and attaches it to the DimPlug's stats property */ function attachPlugStats( createdItem: DimItem, socket: DimSocket, statsByHash: StatLookup, statDisplaysByStatHash: StatDisplayLookup, ) { // The plug that is currently inserted into the socket const activePlug = socket.plugged; // This holds the item's 'base' investment stat values without any plug additions. const baseItemInvestmentStats: { [statHash: number]: number | undefined } = {}; // The active plug is already contributing to the item's stats in statsByHash. Thus we treat it separately // here for two reasons, // 1. We need to calculate the 'base' investment stat value (without this plug's contribution) for the // item's stats so that we can calculate correct values for the inactive plugs. // 2. By utilizing the fact that the item's stats already include this, we can do one less interpolation // per stat to figure out the active plug's stat contribution. if (activePlug) { const activePlugStats: DimPlug['stats'] = {}; for (const plugInvestmentStat of mapAndFilterInvestmentStats(activePlug.plugDef)) { const existingStat = statsByHash[plugInvestmentStat.statTypeHash]; if ( !isPlugStatActive(plugInvestmentStat.activationRule, { item: createdItem, existingStat }) ) { continue; } const plugStatInvestmentValue = getPlugStatValue(createdItem, plugInvestmentStat); const itemStat = statsByHash[plugInvestmentStat.statTypeHash]; const statDisplay = statDisplaysByStatHash[plugInvestmentStat.statTypeHash]; let plugStatValue = plugStatInvestmentValue; if (itemStat) { const baseInvestmentStat = itemStat.investmentValue - plugStatInvestmentValue; baseItemInvestmentStats[plugInvestmentStat.statTypeHash] = baseInvestmentStat; // Figure out what the interpolated stat value would be without the active perk's contribution // and then take the difference between that and the original stat value to find the perk's contribution. if (statDisplay) { // This is an interpolated stat type, so we need to compare interpolated values with and without this perk const valueWithoutPerk = interpolateStatValue(baseInvestmentStat, statDisplay); plugStatValue = itemStat.value - valueWithoutPerk; } else { const valueWithoutPerk = Math.min(baseInvestmentStat, itemStat.maximumValue); plugStatValue = itemStat.value - valueWithoutPerk; } } activePlugStats[plugInvestmentStat.statTypeHash] = { value: plugStatValue, investmentValue: plugStatInvestmentValue, }; } // We can mutate the stats here, as the plug has just been freshly constructed. If that ever changes we will need // to reconsider. (activePlug as Draft).stats = activePlugStats; } for (const plug of socket.plugOptions) { // We already did this plug above and activePlug should be a reference to plug. if (plug.plugDef.hash === socket.plugged?.plugDef.hash) { continue; } const plugStats: DimPlug['stats'] = {}; for (const plugInvestmentStat of mapAndFilterInvestmentStats(plug.plugDef)) { const itemStat = statsByHash[plugInvestmentStat.statTypeHash]; if ( !isPlugStatActive(plugInvestmentStat.activationRule, { item: createdItem, existingStat: itemStat, }) ) { continue; } const plugStatInvestmentValue = getPlugStatValue(createdItem, plugInvestmentStat); const statDisplay = statDisplaysByStatHash[plugInvestmentStat.statTypeHash]; let plugStatValue = plugStatInvestmentValue; if (itemStat) { // User our calculated baseItemInvestment stat, which is the items investment stat value minus // the active plugs investment stat value const baseInvestmentStat = baseItemInvestmentStats[plugInvestmentStat.statTypeHash] ?? itemStat.investmentValue; // This time we use the baseItemInvestment value we computed earlier to calculate the interpolated stat value with // and without the perk's value, using the difference to get its individual contribution to the stat. // These calculations are equivalent to the ones used for the active plug's stats. if (statDisplay) { // This is an interpolated stat type, so we need to compare interpolated values with and without this perk const valueWithoutPerk = interpolateStatValue(baseInvestmentStat, statDisplay); const valueWithPerk = interpolateStatValue( baseInvestmentStat + plugStatInvestmentValue, statDisplay, ); plugStatValue = valueWithPerk - valueWithoutPerk; } else { const baseInvestmentStat = baseItemInvestmentStats[plugInvestmentStat.statTypeHash] ?? itemStat.value; const valueWithoutPerk = Math.min(baseInvestmentStat, itemStat.maximumValue); const valueWithPerk = Math.min( baseInvestmentStat + plugStatInvestmentValue, itemStat.maximumValue, ); plugStatValue = valueWithPerk - valueWithoutPerk; } } plugStats[plugInvestmentStat.statTypeHash] = { value: plugStatValue, investmentValue: plugStatInvestmentValue, }; } // Yes, we are mutating the stats in place! This relies on the plugs being built fresh every time. (plug as Draft).stats = plugStats; } } /** * Some stats have an item-specific interpolation table, which is defined as * a piecewise linear function mapping input stat values to output stat values. */ export function interpolateStatValue(value: number, statDisplay: DestinyStatDisplayDefinition) { // right now, we are not doing stat interpolation for armor. // they're 1:1 in effects, and we are ignoring the clamping if (armorStats.includes(statDisplay.statHash)) { return value; } const interp = statDisplay.displayInterpolation; // Clamp the value to prevent overfilling value = Math.min(value, statDisplay.maximumValue); let endIndex = interp.findIndex((p) => p.value > value); // value < 0 is for mods with negative stats if (endIndex < 0) { endIndex = interp.length - 1; } const startIndex = Math.max(0, endIndex - 1); const start = interp[startIndex]; const end = interp[endIndex]; const range = end.value - start.value; if (range === 0) { return start.weight; } const t = (value - start.value) / (end.value - start.value); const interpValue = start.weight + t * (end.weight - start.weight); // vthorn has a hunch that magazine size doesn't use banker's rounding, but the rest definitely do: // https://github.com/Bungie-net/api/issues/1029#issuecomment-531849137 return statDisplay.statHash === StatHashes.Magazine ? Math.round(interpValue) : bankersRound(interpValue); } /** * "Banker's rounding" rounds numbers that perfectly fall halfway between two integers to the nearest * even integer, instead of always rounding up. */ function bankersRound(x: number) { const r = Math.round(x); return (x > 0 ? x : -x) % 1 === 0.5 ? (r % 2 === 0 ? r : r - 1) : r; } export function keyByStatHash(stats: DimStat[]): StatLookup; export function keyByStatHash(stats: DestinyStatDisplayDefinition[]): StatDisplayLookup; export function keyByStatHash(stats: (DimStat | DestinyStatDisplayDefinition)[]): { [statHash: number]: DimStat | DestinyStatDisplayDefinition | undefined; } { const keyed: { [statHash: number]: any } = {}; for (const stat of stats) { keyed[stat.statHash] = stat; } return keyed; } ================================================ FILE: src/app/inventory/store/well-rested.ts ================================================ import { getSeasonPassStatus } from 'app/progress/SeasonalRank'; import { useCurrentSeasonInfo } from 'app/utils/seasons'; import { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import { D2ManifestDefinitions } from '../../destiny2/d2-definitions'; /** * Figure out whether a character has the "well rested" buff, which applies a 2x XP boost * for the first 5 season levels each week. Ideally this would just come back in the response, * but instead we have to calculate it from the weekly XP numbers. */ export function useIsWellRested( defs: D2ManifestDefinitions, profileInfo: DestinyProfileResponse, ): { /** Is the "well rested" buff active? */ wellRested: boolean; /** How much of the well rested XP has been earned so far this week? */ weeklyProgress?: number; /** * How much XP total needs to be earned in a week before the character is no * longer "well rested"? */ requiredXP?: number; } { const { season, seasonPass } = useCurrentSeasonInfo(defs, profileInfo); if (!season) { return { wellRested: false, }; } const seasonPassProgressionHash = seasonPass?.rewardProgressionHash; const prestigeProgressionHash = seasonPass?.prestigeProgressionHash; if (!seasonPassProgressionHash || !prestigeProgressionHash) { return { wellRested: false, }; } const { weeklyProgress } = getSeasonPassStatus(defs, profileInfo, seasonPass, season); // 5 levels worth of XP at 100k each. We used to calculate this dynamically // but this has been 500k consistently and the calculation is no longer easy // as the definitions don't agree with the game (Ranks 101-110 are displayed // in game as 5 segments, each of which is equivalent to one regular levels at // 100k, but in the defs they are a single 500k level). const requiredXP = 500_000; return { wellRested: weeklyProgress < requiredXP, weeklyProgress, requiredXP, }; } ================================================ FILE: src/app/inventory/store-types.ts ================================================ import { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { D1FactionDefinition, D1GetAdvisorsResponse, D1LevelProgression, D1ProgressionStep, } from 'app/destiny1/d1-manifest-types'; import { DestinyClass, DestinyColor, DestinyDisplayPropertiesDefinition, } from 'bungie-api-ts/destiny2'; import { D1Item, DimItem } from './item-types'; /** * A generic DIM character or vault - a "store" of items. This completely * represents any D2 store, and most properties of D1 stores, though you can * specialize down to the D1Store type for some special D1 properties and * overrides. */ export interface DimStore { // Static data - these properties will never change after the character/store is created /** An ID for the store. Character ID or 'vault'. */ id: string; /** Localized name for the store. */ name: string; /** Is this the vault? */ isVault: boolean; /** The Destiny version this store came from. */ destinyVersion: DestinyVersion; /** Enum class type. */ classType: DestinyClass; /** Localized class name. */ className: string; /** Localized gender. */ gender: string; /** The character's gender hash. */ genderHash?: number; /** Localized race. */ race: string; /** Localized gender and race together. */ genderRace: string; /** String gender name: 'male' | 'female' | '', used exclusively for i18n when translating to gendered languages */ genderName: 'male' | 'female' | ''; // "Mutable" data - this may be changed by moving the item around, lock/unlock, etc. Any place DIM updates its view of the world without a profile refresh. /** All items in the store, across all buckets. */ items: readonly Item[]; // Dynamic data - this may change between profile updates, (whether that's full or partial profile update) /** An icon (emblem) for the store. */ icon: string; /** Is this the most-recently-played character? */ current: boolean; /** The date the character was last played. */ lastPlayed: Date; /** Emblem background image */ background: string; /** The background or dominant color of the equipped emblem, if available. */ color?: DestinyColor; /** Character level. */ level: number; /** Progress towards the next level (or "prestige level") */ percentToNextLevel: number; /** The Bungie.net-reported power level */ powerLevel: number; /** The record corresponding to the currently equipped Title. */ titleInfo?: DimTitle; /** Character stats. */ stats: { [hash: number]: DimCharacterStat; }; /** Did any of the items in the last inventory build fail? */ hadErrors: boolean; } export interface DimTitle { title: string; isCompleted: boolean; gildedNum: number; isGildedForCurrentSeason: boolean; } /** Account-wide currency counts, e.g. glimmer */ export interface AccountCurrency { readonly itemHash: number; readonly displayProperties: DestinyDisplayPropertiesDefinition; readonly quantity: number; } export type DimCharacterStatSource = 'armorStats' | 'armorPlug' | 'subclassPlug' | 'runtimeEffect'; export const statSourceOrder: DimCharacterStatSource[] = [ 'armorStats', 'subclassPlug', 'armorPlug', 'runtimeEffect', ]; export interface DimCharacterStatChange { /** What contributed this stat. */ source: DimCharacterStatSource; /** The name of the thing that contributed this stat. */ name: string; /** A unique key for merging and display */ hash: number; /** The icon associated with the source (subclass plug icon, armor mod icon...) */ icon: string | undefined; /** How many copies of a mod were used */ count: number | undefined; /** How many stat points this contributes to the stat's total value. */ value: number; } /** A character-level stat. */ export interface DimCharacterStat { /** The DestinyStatDefinition hash for the stat. */ hash: number; displayProperties: DestinyDisplayPropertiesDefinition; /** The current value of the stat. */ value: number; /** How this stat exactly was calculated. */ breakdown?: DimCharacterStatChange[]; } export interface D1Progression extends D1LevelProgression { name: string; scope: number; repeatLastStep: boolean; steps: D1ProgressionStep[]; visible: boolean; hash: number; index: number; redacted: boolean; identifier?: string; icon?: string; label?: string; order: number; faction: D1FactionDefinition; description?: string; source?: string; } /** * A D1 character. Use this when you need D1-specific properties or D1-specific items. */ export interface D1Store extends DimStore { progressions: D1Progression[]; advisors: D1GetAdvisorsResponse['data']; } ================================================ FILE: src/app/inventory/stores-helpers.ts ================================================ import { count, sumBy } from 'app/utils/collections'; import { emptyArray } from 'app/utils/empty'; /** * Generic helpers for working with whole stores (character inventories) or lists of stores. */ import { weakMemoize } from 'app/utils/memoize'; import { DimItem } from './item-types'; import { D1Store, DimStore } from './store-types'; /** * Get whichever character was last played. */ export const getCurrentStore = (stores: readonly Store[]) => stores.find((s) => s.current); /** * Get a store from a list by ID. */ export const getStore = (stores: readonly Store[], id: string) => stores.find((s) => s.id === id); /** * Get the Vault from a list of stores. */ export const getVault = (stores: readonly DimStore[]): DimStore | undefined => stores.find((s) => s.isVault); /** * This is a memoized function that generates a map of items by their bucket * location. It could be a redux selector but I didn't want to have to thread it * through everywhere, so instead we do our own weak memoization to avoid * recalculation and memory leaks. The items by buckets used to be stored * directly on DimStore, but that violates the rules that allow Immer to work. * An alternative to this would be to have items stored in a map based on their * index and then have buckets reference items by index. This is slightly worse * than what we had before, because it recreates all buckets for the store whenever * the store changes for any reason. */ const itemsByBucket = weakMemoize((store: DimStore) => Object.groupBy(store.items, (i) => i.location.hash), ); /** * Find items in a specific store bucket, taking into account that there may not be any in that bucket! */ export const findItemsByBucket = (store: DimStore, bucketId: number): DimItem[] => itemsByBucket(store)[bucketId] ?? emptyArray(); /** * Get the bonus power from the Seasonal Artifact. * Destiny 2 is not currently using the artifact to provide power. * Bungie.net is currently reporting 1 instead of 0 thus this fix. * Leaving this code in place for any future power modifier would be nice. */ export function getArtifactBonus(_store: DimStore) { // const artifact = findItemsByBucket(store, BucketHashes.SeasonalArtifact).find((i) => i.equipped); // return artifact?.primaryStat?.value || 0; return 0; } /** * Get the total amount of this item in the store, across all stacks, * excluding stuff in the postmaster. */ export function amountOfItem(store: DimStore, item: { hash: number }) { return sumBy(store.items, (i) => i.hash === item.hash && !i.location?.inPostmaster ? i.amount : 0, ); } /** * How much of items like this item can fit in this store? For * stackables, this is in stacks, not individual pieces. */ export function capacityForItem(store: DimStore, item: DimItem) { if (store.isVault) { const vaultBucket = item.bucket.vaultBucket; return vaultBucket ? vaultBucket.capacity : 0; } return item.bucket.capacity; } /** * How many *more* items like this item can fit in this store? * This takes into account stackables, so the answer will be in * terms of individual pieces. */ export function spaceLeftForItem(store: DimStore, item: DimItem, stores: DimStore[]) { return potentialSpaceLeftForItem(store, item, stores).guaranteed; } export interface SpaceLeft { /** The space definitely available. For stackables this is in individual pieces, not stacks. */ guaranteed: number; /** Whether there's maybe a way space could be made for more than the guaranteed. */ couldMakeSpace: boolean; } /** * Determine how much space there may be for an item being moved into a target * store - this figures out both how much open space there is, and whether we * could make space by moving things to the vault. */ export function potentialSpaceLeftForItem( store: DimStore, item: DimItem, stores: DimStore[], ): SpaceLeft { // Calculate how many full stacks (slots, where multiple items in a stack // count as 1) are occupied in the bucket this item would go into. let occupiedStacks: number; if (store.isVault) { if (!item.bucket.vaultBucket) { return { guaranteed: 0, couldMakeSpace: false }; } const vaultBucket = item.bucket.vaultBucket; // In the vault, all items are together in one big bucket, so we look at how much space that bucket has open occupiedStacks = item.bucket.vaultBucket ? count(store.items, (i) => Boolean(i.bucket.vaultBucket?.hash === vaultBucket.hash)) : 0; } else { // Account-wide buckets (mods, etc) are only on the first character if (item.bucket.accountWide && !store.current) { return { guaranteed: 0, couldMakeSpace: false }; } occupiedStacks = findItemsByBucket(store, item.bucket.hash).length; } // The open stacks are just however many you *could* fit, minus how many are occupied const openStacks = Math.max(0, capacityForItem(store, item) - occupiedStacks); // Some things can't have multiple stacks (unique stacks) and must be handled // specially. This only matters if they're not in the vault (pull from postmaster scenario) if (item.uniqueStack) { // If the item lives in an account-wide bucket (like modulus reports) // we need to check out how much space is left in that bucket, which is // only on the current store. const checkStore = item.bucket.accountWide && !store.isVault ? getCurrentStore(stores)! : store; const existingAmount = amountOfItem(checkStore, item); if (existingAmount === 0) { // This is the first stack. If we have no open stacks, we don't have space, but could make more space // by moving other items from that bucket to the vault. Otherwise, the only way to circumvent // the uniqueStack rule is to move the items themselves to a different bucket. return openStacks > 0 ? { guaranteed: item.maxStackSize, couldMakeSpace: !item.notransfer } : { guaranteed: 0, couldMakeSpace: Boolean(item.bucket.vaultBucket) }; } else { // We have a stack, so we can fill that stack up, and may be able to store even more // by moving the items themselves to a different bucket. return { guaranteed: Math.max(item.maxStackSize - existingAmount, 0), couldMakeSpace: !item.notransfer, }; } } // Convert back from stacks to individual items, keeping in mind that we may // be able to move some amount into an existing stack. const maxStackSize = item.maxStackSize || 1; if (maxStackSize === 1) { // Stacks and individual items are the same, no conversion required return { guaranteed: openStacks, couldMakeSpace: Boolean(item.bucket.vaultBucket) }; } else { // Get the existing amount in individual pieces, not stacks let existingAmount = amountOfItem(store, item); // This helps us figure out the remainder that gets added back in from a // partial stack - it'll end up negative, and when subtracted from the open // stacks' worth, it will *add* in the remainder. while (existingAmount > 0) { existingAmount -= maxStackSize; } return { guaranteed: Math.max(openStacks * maxStackSize - existingAmount, 0), couldMakeSpace: Boolean(item.bucket.vaultBucket), }; } } /** * Is this store a Destiny 1 store? Use this when you want the store to * automatically be typed as D1 store in the "true" branch of a conditional. * Otherwise you can just check "destinyVersion === 1". */ export function isD1Store(store: DimStore): store is D1Store { return store.destinyVersion === 1; } ================================================ FILE: src/app/inventory/subclass.ts ================================================ import { damageNamesByEnum } from 'app/search/search-filter-values'; import { invert } from 'app/utils/collections'; import { getFirstSocketByCategoryHash } from 'app/utils/socket-utils'; import { LookupTable } from 'app/utils/util-types'; import { DamageType } from 'bungie-api-ts/destiny2'; import { emptyPlugHashes } from 'data/d2/empty-plug-hashes'; import { ItemCategoryHashes, SocketCategoryHashes } from 'data/d2/generated-enums'; import subclassArc from 'images/subclass-arc.png'; import subclassPrismatic from 'images/subclass-prismatic.png'; import subclassSolar from 'images/subclass-solar.png'; import subclassStasis from 'images/subclass-stasis.png'; import subclassStrand from 'images/subclass-strand.png'; import subclassVoid from 'images/subclass-void.png'; import { DimItem, PluggableInventoryItemDefinition } from './item-types'; const baseImagesByDamageType: LookupTable = { [DamageType.Arc]: subclassArc, [DamageType.Thermal]: subclassSolar, [DamageType.Void]: subclassVoid, [DamageType.Stasis]: subclassStasis, [DamageType.Strand]: subclassStrand, [DamageType.Kinetic]: subclassPrismatic, }; interface SubclassIconInfo { base: string | undefined; super: string; } export function getSubclassIconInfo(item: DimItem): SubclassIconInfo | undefined { if (item.sockets) { const superSocket = getFirstSocketByCategoryHash(item.sockets, SocketCategoryHashes.Super); const superPlug = superSocket?.plugged?.plugDef; const superIcon = superPlug?.displayProperties?.icon; if (superIcon) { const damageType = item.element?.enumValue; if (damageType && baseImagesByDamageType[damageType]) { const base = baseImagesByDamageType[damageType]; return { base: base, super: superIcon, }; } } } } const nameToDamageType: Record = invert(damageNamesByEnum); export function getDamageTypeForSubclassPlug( item: PluggableInventoryItemDefinition, ): DamageType | null { // ignore empty plugs because they'll be present across all subclasses if (emptyPlugHashes.has(item.hash)) { return null; } // early out to avoid building subclass plug categories if (!item.itemCategoryHashes?.includes(ItemCategoryHashes.SubclassMods)) { return null; } for (const name in nameToDamageType) { if (item.plug.plugCategoryIdentifier.includes(name)) { return nameToDamageType[name]; } } return null; } ================================================ FILE: src/app/inventory-page/CategoryStrip.m.scss ================================================ @use '../variables' as *; .options { $vpadding: 15px; display: flex; background-color: var(--theme-mobile-background); position: fixed; align-items: center; bottom: 0; top: inherit; left: 0; right: 0; justify-content: space-around; // https://css-tricks.com/when-sass-and-new-css-features-collide/ padding-bottom: Max(#{($vpadding - 1)}, env(safe-area-inset-bottom)); z-index: 7; > div { display: flex; align-items: center; justify-content: center; text-transform: uppercase; font-size: 12px; letter-spacing: 1.2px; border-bottom: 2px solid transparent; padding: $vpadding 0 5px; } } .selected { color: var(--theme-accent-primary); border-bottom: 2px solid var(--theme-accent-primary); } ================================================ FILE: src/app/inventory-page/CategoryStrip.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'options': string; 'selected': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/CategoryStrip.tsx ================================================ import { t } from 'app/i18next-t'; import { BucketSortType, InventoryBuckets } from 'app/inventory/inventory-buckets'; import clsx from 'clsx'; import * as styles from './CategoryStrip.m.scss'; /** * The selector at the bottom of the mobile interface that allows us to select weapons, armor, etc. */ export default function CategoryStrip({ buckets, category: selectedCategoryId, onCategorySelected, }: { buckets: InventoryBuckets; category: string; onCategorySelected: (category: string) => void; }) { return (
{Object.keys(buckets.byCategory).map( (category) => category !== 'Postmaster' && (
onCategorySelected(category)} className={clsx({ [styles.selected]: category === selectedCategoryId })} > {t(`Bucket.${category as BucketSortType}`, { metadata: { keys: 'buckets' } })}
), )}
); } ================================================ FILE: src/app/inventory-page/D1Reputation.m.scss ================================================ .reputationBucket { composes: sub-bucket from global; width: 100%; } .factionrep { position: relative; svg { width: var(--item-size); height: var(--item-size); } } ================================================ FILE: src/app/inventory-page/D1Reputation.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'factionrep': string; 'reputationBucket': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/D1Reputation.tsx ================================================ import { D1Store } from 'app/inventory/store-types'; import { compareBy } from 'app/utils/comparators'; import { bungieNetPath } from '../dim-ui/BungieImage'; import DiamondProgress from '../dim-ui/DiamondProgress'; import { PressTip, Tooltip } from '../dim-ui/PressTip'; import * as styles from './D1Reputation.m.scss'; export default function D1Reputation({ store }: { store: D1Store }) { if (!store.progressions.length) { return null; } const progressions = store.progressions.toSorted(compareBy((p) => p.order)); return (
{progressions.map( (rep) => rep.order >= 0 && ( {rep.progressToNextLevel} / {rep.nextLevelAt} } >
), )}
); } ================================================ FILE: src/app/inventory-page/D1ReputationSection.tsx ================================================ import { t } from 'app/i18next-t'; import { D1Store, DimStore } from 'app/inventory/store-types'; import CollapsibleTitle from '../dim-ui/CollapsibleTitle'; import D1Reputation from './D1Reputation'; export default function D1ReputationSection({ stores }: { stores: DimStore[] }) { return (
{stores.map((store) => (
))}
); } ================================================ FILE: src/app/inventory-page/DesktopStores.m.scss ================================================ @use '../variables.scss' as *; .content { width: 100%; // Prevent collapsing at smaller than iPad landscape sizes min-width: calc( (var(--num-characters) + 1) * (var(--inventory-column-padding) * 2 + var(--character-column-width)) ); } .buttons { display: grid; grid-template-rows: 46px 1fr; padding: 16px var(--inventory-column-padding) 6px 0; place-items: center; } .singleCharacter { --num-characters: 1; } .singleCharacterButton { composes: resetButton from '../dim-ui/common.m.scss'; color: #9e9db5; font-size: 26px; width: min-content; transition: transform 100ms ease-in-out, color 100ms ease-in-out; display: flex; align-items: center; @include interactive($hover: true) { transform: scale(1.25); color: var(--theme-accent-primary); } } .vaultUnderButton { composes: singleCharacterButton; font-size: 18px; &.under > span { transform: rotate(90deg); } } .inventoryContainer { :global(.issue-banner-shown) & { @include desktop { padding-bottom: $issue-banner-height; } } } ================================================ FILE: src/app/inventory-page/DesktopStores.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttons': string; 'content': string; 'inventoryContainer': string; 'singleCharacter': string; 'singleCharacterButton': string; 'under': string; 'vaultUnderButton': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/DesktopStores.tsx ================================================ import { itemPop } from 'app/dim-ui/scroll'; import { t } from 'app/i18next-t'; import { BucketSortType, InventoryBucket, InventoryBuckets } from 'app/inventory/inventory-buckets'; import { locateItem$ } from 'app/inventory/locate-item'; import { DimStore } from 'app/inventory/store-types'; import { findItemsByBucket, getCurrentStore, getVault } from 'app/inventory/stores-helpers'; import IssueAwarenessBanner from 'app/issue-awareness-banner/IssueAwarenessBanner'; import ItemFeedSidebar from 'app/item-feed/ItemFeedSidebar'; import { useSetSetting, useSetting } from 'app/settings/hooks'; import { AppIcon, levelDownIcon, levellingIcon, maximizeIcon, minimizeIcon } from 'app/shell/icons'; import StoreStats from 'app/store-stats/StoreStats'; import { useEventBusListener } from 'app/utils/hooks'; import { isClassCompatible } from 'app/utils/item-utils'; import clsx from 'clsx'; import { useMemo } from 'react'; import StoreHeading from '../character-tile/StoreHeading'; import D1ReputationSection from './D1ReputationSection'; import * as styles from './DesktopStores.m.scss'; import HeaderShadowDiv from './HeaderShadowDiv'; import InventoryCollapsibleTitle from './InventoryCollapsibleTitle'; import { StoreBuckets } from './StoreBuckets'; import './Stores.scss'; interface Props { stores: DimStore[]; buckets: InventoryBuckets; singleCharacter: boolean; } /** * Display inventory and character headers for all characters and the vault. * * This is the desktop view only. */ export default function DesktopStores({ stores, buckets, singleCharacter }: Props) { const vault = getVault(stores); const currentStore = getCurrentStore(stores); const setSetting = useSetSetting(); useEventBusListener(locateItem$, itemPop); const [vaultUnder, setVaultUnder] = useSetting('vaultBelow'); // Hide the single character toggle for players with only one character // unless they own items that cannot be used by their only character. const singleCharacterHasEffect = useMemo( () => stores.length > 2 || (currentStore && stores.some((s) => s.items.some((i) => !isClassCompatible(i.classType, currentStore.classType)), )), [stores, currentStore], ); if (!stores.length || !buckets || !vault || !currentStore) { return null; } let headerStores = stores; if (singleCharacter) { headerStores = [currentStore, vault]; } const toggleSingleCharacter = () => setSetting('singleCharacter', !singleCharacter); const toggleVaultUnder = () => setVaultUnder(!vaultUnder); return (
{headerStores.map((store) => (
))}
{singleCharacterHasEffect && ( )}
{$featureFlags.issueBanner && }
{$featureFlags.itemFeed && }
); } /** Is there any store that has an item in any of the buckets in this category? */ function categoryHasItems( allBuckets: InventoryBuckets, category: string, stores: DimStore[], currentStore: DimStore, ): boolean { const buckets = allBuckets.byCategory[category]; return buckets.some((bucket) => { const storesToSearch = // Account-wide bucket shows up for every character (on mobile) but is stored on the current store bucket.accountWide && // On mobile, stores can be just [vault] when you're viewing the vault !stores[0].isVault ? [currentStore] : stores; return storesToSearch.some((s) => findItemsByBucket(s, bucket.hash).length > 0); }); } interface InventoryContainerProps { buckets: InventoryBuckets; stores: DimStore[]; currentStore: DimStore; vault: DimStore; singleCharacter: boolean; vaultUnder: boolean; } function CollapsibleContainer({ buckets, category, stores, currentStore, inventoryBucket, vault, singleCharacter, vaultUnder, }: { category: string; inventoryBucket: InventoryBucket[]; } & InventoryContainerProps) { if (!categoryHasItems(buckets, category, stores, currentStore)) { return null; } return ( {inventoryBucket.map((bucket) => ( ))} ); } function StoresInventory({ buckets, stores, currentStore, vault, singleCharacter, vaultUnder, }: InventoryContainerProps) { return ( <> {Object.entries(buckets.byCategory).map(([category, inventoryBucket]) => ( ))} {stores[0].destinyVersion === 1 && } ); } ================================================ FILE: src/app/inventory-page/HeaderShadowDiv.m.scss ================================================ @use '../variables.scss' as *; .shadow { height: 1px; box-shadow: 0 1px 4px 0 black; width: 100%; position: sticky; left: 0; top: calc(var(--store-header-height) + var(--header-height) - 1px); z-index: 9; } .cover { height: calc(var(--store-header-height) + 7px); width: 100%; position: absolute; left: 0; z-index: 9; background: var(--theme-header-characters-bg); background-position: center top; background-repeat: no-repeat; background-size: 100% 100vh; @include below-header; } ================================================ FILE: src/app/inventory-page/HeaderShadowDiv.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'cover': string; 'shadow': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/HeaderShadowDiv.tsx ================================================ import { useSetCSSVarToHeight } from 'app/utils/hooks'; import React, { memo, useRef } from 'react'; import * as styles from './HeaderShadowDiv.m.scss'; // Also sets `--store-header-height` to the height of `children` export default memo(({ children, ...divProps }: React.HTMLAttributes) => { const ref = useRef(null); useSetCSSVarToHeight(ref, '--store-header-height'); return ( <>
{children}
); }); ================================================ FILE: src/app/inventory-page/Inventory.tsx ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import GearPower from 'app/gear-power/GearPower'; import { t } from 'app/i18next-t'; import DragPerformanceFix from 'app/inventory/DragPerformanceFix'; import { useLoadStores } from 'app/inventory/store/hooks'; import { MaterialCountsSheet } from 'app/material-counts/MaterialCountsWrappers'; import { usePageTitle } from 'app/utils/hooks'; import Stores from './Stores'; export default function Inventory({ account }: { account: DestinyAccount }) { const storesLoaded = useLoadStores(account); usePageTitle(t('Header.Inventory')); if (!storesLoaded) { return ; } return ( <> {account.destinyVersion === 2 && } {account.destinyVersion === 2 && } ); } ================================================ FILE: src/app/inventory-page/InventoryCollapsibleTitle.m.scss ================================================ @use '../variables.scss' as *; .spanColumns { grid-column: 1 / -1; } .clickToExpand { font-weight: normal; font-size: 12px; opacity: 0.7; } .bucketSize { font-size: 12px; font-weight: normal; letter-spacing: normal; } .postmasterFull { background-color: rgb(151, 4, 1, 0.75) !important; } // The wrapping H3 element .title { margin: 4px 0 0 0; display: flex; flex-direction: row; align-items: center; gap: 4px; text-transform: uppercase; letter-spacing: 2px; font-size: 14px; // The interactive button within > button { // Reset button all: unset; cursor: pointer; display: flex; flex-direction: row; align-items: center; min-height: 24px; gap: 6px; @include interactive($hover: true, $focus: true) { color: var(--theme-accent-primary); @include phone-portrait { color: var(--theme-text); } } } &.collapsed { font-weight: bold; min-height: 32px; padding-bottom: 4px; } } ================================================ FILE: src/app/inventory-page/InventoryCollapsibleTitle.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bucketSize': string; 'clickToExpand': string; 'collapsed': string; 'postmasterFull': string; 'spanColumns': string; 'title': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/InventoryCollapsibleTitle.tsx ================================================ import { collapsedSelector } from 'app/dim-api/selectors'; import { CollapseIcon, CollapsedSection } from 'app/dim-ui/CollapsibleTitle'; import { t } from 'app/i18next-t'; import { DimStore } from 'app/inventory/store-types'; import { POSTMASTER_SIZE, postmasterAlmostFull, postmasterSpaceUsed, } from 'app/loadout-drawer/postmaster'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import clsx from 'clsx'; import React, { useCallback, useEffect, useId, useRef } from 'react'; import { useSelector } from 'react-redux'; import { toggleCollapsedSection } from '../settings/actions'; import * as styles from './InventoryCollapsibleTitle.m.scss'; export default function InventoryCollapsibleTitle({ sectionId, title, children, stores, }: { sectionId: string; title: React.ReactNode; children?: React.ReactNode; stores: DimStore[]; }) { const dispatch = useThunkDispatch(); const collapsed = Boolean(useSelector(collapsedSelector(sectionId))); const toggle = useCallback( () => dispatch(toggleCollapsedSection(sectionId)), [dispatch, sectionId], ); const checkPostmaster = sectionId === 'Postmaster'; if (!checkPostmaster) { // Only the postmaster needs a header per store, the rest span across all stores stores = [stores[0]]; } const initialMount = useRef(true); useEffect(() => { initialMount.current = false; }, [initialMount]); const id = useId(); const contentId = `content-${id}`; const headerId = `header-${id}`; return ( <>
{stores .filter((s) => !s.isVault) .map((store, index) => { const storeIsDestiny2 = store.destinyVersion === 2; const isPostmasterAlmostFull = postmasterAlmostFull(store); const postMasterSpaceUsed = postmasterSpaceUsed(store); const showPostmasterFull = checkPostmaster && storeIsDestiny2 && isPostmasterAlmostFull; const text = postMasterSpaceUsed < POSTMASTER_SIZE ? t('ItemService.PostmasterAlmostFull') : t('ItemService.PostmasterFull'); return (

{index === 0 ? ( <> {checkPostmaster && ( ({postMasterSpaceUsed}/{POSTMASTER_SIZE}) )} {collapsed && !checkPostmaster && ( {t('Inventory.ClickToExpand')} )} ) : ( <> {showPostmasterFull && text} {checkPostmaster && ( ({postMasterSpaceUsed}/{POSTMASTER_SIZE}) )} )}

); })}
{children} ); } ================================================ FILE: src/app/inventory-page/PhoneStores.m.scss ================================================ @use '../variables.scss' as *; .content { width: 100%; // Give room for the category selector strip (height 53px) padding-bottom: calc(#{53px + 10px} + env(safe-area-inset-bottom)); } ================================================ FILE: src/app/inventory-page/PhoneStores.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'content': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/PhoneStores.tsx ================================================ import { itemPop, scrollToPosition } from 'app/dim-ui/scroll'; import { t } from 'app/i18next-t'; import { locateItem$ } from 'app/inventory/locate-item'; import { DimStore } from 'app/inventory/store-types'; import StoreStats from 'app/store-stats/StoreStats'; import { wrap } from 'app/utils/collections'; import { useEventBusListener } from 'app/utils/hooks'; import { PanInfo, motion } from 'motion/react'; import { useCallback, useState } from 'react'; import { InventoryBucket, InventoryBuckets } from '../inventory/inventory-buckets'; import { getCurrentStore, getStore, getVault } from '../inventory/stores-helpers'; import CategoryStrip from './CategoryStrip'; import D1ReputationSection from './D1ReputationSection'; import HeaderShadowDiv from './HeaderShadowDiv'; import * as styles from './PhoneStores.m.scss'; import PhoneStoresHeader from './PhoneStoresHeader'; import { StoreBuckets } from './StoreBuckets'; import './Stores.scss'; /** * Display inventory and character headers for all characters and the vault. * * This is the phone (portrait) view only. */ export default function PhoneStores({ stores, buckets, singleCharacter, }: { stores: DimStore[]; buckets: InventoryBuckets; singleCharacter: boolean; }) { const vault = getVault(stores); const currentStore = getCurrentStore(stores); const [{ selectedStoreId, direction }, setSelectedStoreId] = useState({ selectedStoreId: currentStore?.id, direction: 0, }); const [selectedCategoryId, setSelectedCategoryId] = useState('Weapons'); // Handle scrolling the right store into view when locating an item useEventBusListener( locateItem$, useCallback( (item) => { if (selectedStoreId !== item.owner) { setSelectedStoreId({ selectedStoreId: item.owner, direction: 1, }); setTimeout(() => itemPop(item), 500); } else { itemPop(item); } }, [selectedStoreId], ), ); if (!stores.length || !buckets || !vault || !currentStore) { return null; } let headerStores = stores; if (singleCharacter) { headerStores = [currentStore, vault]; } const selectedStore = selectedStoreId ? (getStore(stores, selectedStoreId) ?? currentStore) : currentStore; const handleSwipe = (_e: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { // Velocity is in px/ms if (Math.abs(info.offset.x) < 10 || Math.abs(info.velocity.x) < 300) { return; } const direction = -Math.sign(info.velocity.x); const selectedStoreIndex = selectedStoreId ? headerStores.findIndex((s) => s.id === selectedStoreId) : headerStores.findIndex((s) => s.current); if (direction > 0) { setSelectedStoreId({ selectedStoreId: headerStores[wrap(selectedStoreIndex + 1, headerStores.length)].id, direction: 1, }); } else if (direction < 0) { setSelectedStoreId({ selectedStoreId: headerStores[wrap(selectedStoreIndex - 1, headerStores.length)].id, direction: -1, }); } }; const handleCategoryChange = (category: string) => { if (category === selectedCategoryId) { // If user selects the category they are already on, scroll to top scrollToPosition({ top: 0 }); return; } setSelectedCategoryId(category); }; return (
setSelectedStoreId({ selectedStoreId, direction }) } />
); } function StoresInventory({ selectedCategoryId, buckets, stores, currentStore, vault, singleCharacter, }: { selectedCategoryId: string; buckets: InventoryBuckets; stores: DimStore[]; currentStore: DimStore; vault: DimStore; singleCharacter: boolean; }) { const showPostmaster = (currentStore.destinyVersion === 2 && selectedCategoryId === 'Inventory') || (currentStore.destinyVersion === 1 && selectedCategoryId === 'General'); const renderBucket = (bucket: InventoryBucket) => ( ); const store = stores[0]; return ( <> {((!store.isVault && selectedCategoryId === 'Armor') || (store.isVault && selectedCategoryId === 'Inventory')) && (
)} {showPostmaster && buckets.byCategory.Postmaster.map(renderBucket)} {buckets.byCategory[selectedCategoryId].map(renderBucket)} {store.destinyVersion === 1 && !store.isVault && selectedCategoryId === 'Progress' && ( )} ); } ================================================ FILE: src/app/inventory-page/PhoneStoresHeader.m.scss ================================================ @use '../variables.scss' as *; .frame { // Emblem plus loadout button plus margin max-width: $emblem-width + 16px + 10px; margin: 0 auto; overflow: visible !important; position: relative; } .track { display: block; // Don't let the browser handle touches, we'll do it ourselves touch-action: none; } .character { display: inline-block; padding: 8px 0; box-sizing: border-box; vertical-align: top; > * { margin: 0 5px; } } ================================================ FILE: src/app/inventory-page/PhoneStoresHeader.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'character': string; 'frame': string; 'track': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/PhoneStoresHeader.tsx ================================================ import { DimStore } from 'app/inventory/store-types'; import { hideItemPopup } from 'app/item-popup/item-popup'; import { wrap } from 'app/utils/collections'; import { animate, motion, PanInfo, Transition, useMotionValue, useTransform } from 'motion/react'; import { useEffect, useRef } from 'react'; import StoreHeading from '../character-tile/StoreHeading'; import * as styles from './PhoneStoresHeader.m.scss'; const spring: Transition = { type: 'spring', stiffness: 100, damping: 20, mass: 1, restSpeed: 0.01, restDelta: 0.01, }; /** * The swipable header for the mobile (phone portrait) Inventory view. */ export default function PhoneStoresHeader({ selectedStore, stores, setSelectedStoreId, direction, }: { selectedStore: DimStore; stores: DimStore[]; // The direction we changed stores in - positive for an increasing index, negative for decreasing direction: number; setSelectedStoreId: (id: string, direction: number) => void; }) { const onIndexChanged = (index: number, dir: number) => { const originalIndex = stores.indexOf(selectedStore); setSelectedStoreId(stores[wrap(originalIndex + index, stores.length)].id, dir); hideItemPopup(); }; // TODO: wrap StoreHeading in a div? // TODO: optional external motion control const index = stores.indexOf(selectedStore); const lastIndex = useRef(index); const trackRef = useRef(null); // The track is divided into "segments", with one item per segment const numSegments = 5; // since we wrap the items, we're always showing a virtual repeating window from index -2 to +2 const numItems = stores.length; // This is a floating-point, animated representation of the position within the segments, relative to the current store const offset = useMotionValue(0); // Keep track of the starting point when we begin a gesture const startOffset = useRef(0); useEffect(() => { const velocity = offset.getVelocity(); const newOffset = offset.get() - direction; offset.set(newOffset); animate(offset, 0, { ...spring, velocity }); lastIndex.current = index; }, [index, direction, offset, numItems]); // We want a bit more control than Framer Motion's drag gesture can give us, so fall // back to the pan gesture and implement our own elasticity, etc. const onPanStart = () => { startOffset.current = offset.get(); }; const onPan = (_e: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { if (!trackRef.current) { return; } const trackWidth = trackRef.current.clientWidth; // The offset as a proportion of segments const newValue = startOffset.current + -info.offset.x / (trackWidth / numSegments); offset.set(newValue); }; const onPanEnd = (_e: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { if (!trackRef.current) { return; } // Animate to one of the settled whole-number indexes let newIndex = Math.round(offset.get()); const scale = trackRef.current.clientWidth / numSegments; const direction = -Math.sign(info.velocity.x); if (newIndex === 0) { const swipe = (info.velocity.x * info.offset.x) / (scale * scale); if (swipe > 0.05) { newIndex = newIndex + direction; } } if (newIndex !== 0) { onIndexChanged(newIndex, direction); } else { animate(offset, 0, spring); } }; // Expand out from the selected index in each direction 2 items const segments: DimStore[] = []; for (let i = index - 2; i <= index + 2; i++) { segments.push(stores[wrap(i, stores.length)]); } // Transform the segment-relative offset back into percents const offsetPercent = useTransform(offset, (o) => `${(100 / segments.length) * -(o + 2)}%`); const keys: { [key: string]: number } = {}; const makeKey = (key: string) => { keys[key] ||= 0; keys[key]++; return `${key}:${keys[key]}`; }; return (
{segments.map((store, index) => (
setSelectedStoreId(id, index - 2)} />
))}
); } ================================================ FILE: src/app/inventory-page/StoreBucket.m.scss ================================================ @use '../variables.scss' as *; // Align non-equipped bucket items with the grid of equipped items above them. :global(.sub-bucket).notEquippable { // This should be 6px to be the same as the gap between characters, but 4px // lets us fit an extra column of tiles in. padding-left: 4px; } .armorClassIcon { box-sizing: border-box !important; width: var(--item-size); height: calc((var(--item-size) + ((var(--item-size) / 5) + 4px) - 1px)); font-size: calc(var(--item-size) * 0.71); padding: 8px; color: #999; } .weaponGroupingIconWrapper { box-sizing: border-box !important; display: flex; align-items: center; justify-content: center; width: var(--item-size); height: calc((var(--item-size) + ((var(--item-size) / 5) + 4px) - 1px)); font-size: calc(var(--item-size) * 0.4); color: #999; } .vaultGroup { display: flex; flex-flow: row wrap; align-items: flex-start; align-content: flex-start; gap: var(--item-margin); width: 100%; padding-bottom: var(--item-margin); border-bottom: 1px solid rgb(150, 150, 150, 0.5); &:last-of-type { border-bottom: none; padding-bottom: 0; } .inlineGroups & { display: contents; } } .pullItemButton { font-size: calc((var(--item-size) / 3)); margin: calc((var(--item-size) / 8) - 4px) auto calc((var(--item-size) / 8) - 4px) auto; padding: 4px; opacity: 0.3; transition: opacity 0.3s ease-in-out; @include interactive($hover: true) { opacity: 1; } } // Resize all the items in the engram bucket to be engram-sized // This is also .sub-bucket .engrams { --item-size: var(--engram-size); gap: 0; padding: 4px 0 8px 0; @include phone-portrait { padding: 4px 0; } } // Placeholder hexagons for empty engram slots. A stripped-down version of .item .emptyEngram { border: $item-border-width solid transparent; box-sizing: border-box; height: var(--item-size); width: var(--item-size); } .subClass { // hide normal "equipped" effects :global(.equipped-item) { border-color: transparent; padding-top: 0; } :global(.item-drag-container) { @include interactive($hover: true) { // don't display the default outline when hovering over a draggable subclass item outline: none; // allow the pseudo-element to render outside the bounds of the item contain: layout style; // render a diamond-shaped pseudo-element to act as the border &::before { content: ''; position: absolute; width: var(--item-size); height: var(--item-size); transform: rotate(45deg) scale(0.7); outline-width: 2px; box-sizing: border-box; @include draggable-hover-border; } } } } ================================================ FILE: src/app/inventory-page/StoreBucket.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'armorClassIcon': string; 'emptyEngram': string; 'engrams': string; 'inlineGroups': string; 'notEquippable': string; 'pullItemButton': string; 'subClass': string; 'vaultGroup': string; 'weaponGroupingIconWrapper': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/StoreBucket.scss ================================================ @use '../variables.scss' as *; @layer base { .sub-bucket { min-height: var(--item-size); display: grid; grid-template-columns: repeat(auto-fill, var(--item-size)); gap: var(--item-margin); align-content: flex-start; align-items: flex-start; padding: 4px 0 calc(var(--item-margin) + 4px) 0; position: relative; &:empty { min-height: 0; padding: 0; } } } ================================================ FILE: src/app/inventory-page/StoreBucket.tsx ================================================ import { DestinyVersion, VaultWeaponGroupingStyle } from '@destinyitemmanager/dim-api-types'; import ClassIcon from 'app/dim-ui/ClassIcon'; import WeaponGroupingIcon from 'app/dim-ui/WeaponGroupingIcon'; import { t } from 'app/i18next-t'; import { InventoryBucket } from 'app/inventory/inventory-buckets'; import { DimItem } from 'app/inventory/item-types'; import { pullItem } from 'app/inventory/move-item'; import { currentStoreSelector, sortedStoresSelector, storesSelector, } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { findItemsByBucket } from 'app/inventory/stores-helpers'; import { useItemPicker } from 'app/item-picker/item-picker'; import { characterOrderSelector } from 'app/settings/character-sort'; import { itemSorterSelector } from 'app/settings/item-sort'; import { vaultArmorGroupingStyleSelector, vaultWeaponGroupingEnabledSelector, vaultWeaponGroupingSelector, vaultWeaponGroupingStyleSelector, } from 'app/settings/vault-grouping'; import { AppIcon, addIcon } from 'app/shell/icons'; import { vaultGroupingValueWithType } from 'app/shell/item-comparators'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { compareBy } from 'app/utils/comparators'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { BucketHashes } from 'data/d2/generated-enums'; import emptyEngram from 'destiny-icons/general/empty-engram.svg'; import { shallowEqual } from 'fast-equals'; import { memo, useCallback, useRef } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import * as styles from './StoreBucket.m.scss'; import './StoreBucket.scss'; import StoreBucketDropTarget from './StoreBucketDropTarget'; import StoreInventoryItem from './StoreInventoryItem'; /** * Given an array of objects, return the same version of the array * (reference-equal with previous versions) as long as the contents of the * passed in array is the same as other arrays. This prevents re-renders when we * have to generate new arrays but the contents are the same. * * This is conceptually similar to useMemo except instead of memoizing on the * inputs, it memoizes on the outputs. */ function useStableArray(arr: T[]) { const lastItems = useRef([]); if (!shallowEqual(lastItems.current, arr)) { lastItems.current = arr; } return lastItems.current; } /** * A single bucket of items (for a single store). The arguments for this * component are the bare minimum needed, so that we can memoize it to avoid * unnecessary re-renders of unaffected buckets when moving items around. The * StoreBucket component does the heavy lifting of picking apart these input * props for StoreBucketInner. */ const StoreBucketInner = memo(function StoreBucketInner({ items, bucket, storeId, destinyVersion, storeName, storeClassType, isVault, }: { bucket: InventoryBucket; destinyVersion: DestinyVersion; storeId: string; storeName: string; storeClassType: DestinyClass; isVault: boolean; items: DimItem[]; }) { const dispatch = useThunkDispatch(); const sortItems = useSelector(itemSorterSelector); const groupWeapons = useSelector(vaultWeaponGroupingSelector); const vaultWeaponGroupingEnabled = useSelector(vaultWeaponGroupingEnabledSelector); const weaponGroupingStyle = useSelector(vaultWeaponGroupingStyleSelector); const showItemPicker = useItemPicker(); const pickEquipItem = useCallback(() => { dispatch(pullItem(storeId, bucket, showItemPicker)); }, [bucket, dispatch, showItemPicker, storeId]); const equippedItem = isVault ? undefined : items.find((i) => i.equipped); const unequippedItems = isVault && bucket.inWeapons ? groupWeapons(sortItems(items)) : sortItems(isVault ? items : items.filter((i) => !i.equipped)); // represents whether there's *supposed* to be an equipped item here, aka armor/weapon/artifact, etc const isEquippable = Boolean(equippedItem || bucket.equippable); // Engrams. D1 uses this same bucket hash for "Missions" const isEngrams = destinyVersion === 2 && bucket.hash === BucketHashes.Engrams; // Only D2 has special subclass display const isSubclass = destinyVersion === 2 && bucket.hash === BucketHashes.Subclass; return ( <> {(equippedItem || isEquippable) && !isVault && ( {equippedItem && (
)} {bucket.hasTransferDestination && ( )}
)} {unequippedItems.map((groupOrItem) => 'id' in groupOrItem ? ( ) : (
{groupOrItem.items.map((item) => ( ))}
), )} {isEngrams && !isVault && Array.from( // lower bound of 0, in case this bucket becomes overfilled { length: Math.max(0, bucket.capacity - unequippedItems.length) }, (_, index) => ( ), )}
); }); /** * The classes of each character, in the user's preferred order. */ const storeClassListSelector = createSelector( sortedStoresSelector, (stores) => stores.map((s) => s.classType).filter((c) => c !== DestinyClass.Unknown), // Use shallow equality on the returned array so it only changes when the // actual list of class types change { memoizeOptions: { resultEqualityCheck: shallowEqual } }, ); /** * For armor in the vault, we separate items by which class they belong to. The * arguments for this component are the bare minimum needed, so that we can * memoize it to avoid unnecessary re-renders of unaffected buckets when moving * items around. The StoreBucket component does the heavy lifting of picking * apart these input props for VaultBucketDividedByClass. */ const VaultBucketDividedByClass = memo(function SingleCharacterVaultBucket({ items, bucket, storeId, storeClassType, }: { bucket: InventoryBucket; storeId: string; storeClassType: DestinyClass; items: DimItem[]; }) { const storeClassList = useSelector(storeClassListSelector); const characterOrder = useSelector(characterOrderSelector); const sortItems = useSelector(itemSorterSelector); const armorGroupingStyle = useSelector(vaultArmorGroupingStyleSelector); // The vault divides armor by class const itemsByClass = Map.groupBy(items, (item) => item.classType); const classTypeOrder = [...itemsByClass.keys()].sort( compareBy((classType) => { const index = storeClassList.findIndex((s) => s === classType); return index === -1 ? 999 : characterOrder === 'mostRecentReverse' ? -index : index; }), ); return ( {classTypeOrder.map((classType) => (
{sortItems(itemsByClass.get(classType)!).map((item) => ( ))}
))}
); }); /** * The items for a single bucket on a single store. */ export default function StoreBucket({ store, bucket, singleCharacter, }: { store: DimStore; bucket: InventoryBucket; singleCharacter: boolean; }) { const currentStore = useSelector(currentStoreSelector); const stores = useSelector(storesSelector); let items = findItemsByBucket(store, bucket.hash); // Single character mode collapses all items from other characters into "the // vault" (but only those items that could be used by the current character) if (singleCharacter && store.isVault && (bucket.vaultBucket || bucket.inPostmaster)) { for (const otherStore of stores) { if (!otherStore.current && !otherStore.isVault) { items = [...items, ...findItemsByBucket(otherStore, bucket.hash)]; } } // TODO: When we switch accounts this suffers from the "zombie child" problem where the redux store has already // updated (so currentStore is cleared) but the store from props is still around because its redux subscription // hasn't fired yet. items = items.filter( (i) => i.classType === DestinyClass.Unknown || i.classType === currentStore?.classType, ); } const stableItems = useStableArray(items); // TODO: move grouping here? // The vault divides armor by class if (store.isVault && bucket.inArmor && !singleCharacter) { return ( ); } return ( ); } ================================================ FILE: src/app/inventory-page/StoreBucketDropTarget.m.scss ================================================ @use '../variables.scss' as *; :global(.sub-bucket) { &.canDrop { background-color: rgb(255, 255, 255, 0.2); } &.over { box-shadow: inset 0 0 6px 0 rgb(255, 255, 255, 0.7); } &.grouped { display: flex; flex-wrap: wrap; } &.equipped { display: flex; flex-direction: column; width: calc(var(--item-size) + #{$equipped-item-total-outset}); margin-right: var(--item-margin); } &.unequipped { flex: 1; width: 100%; } } ================================================ FILE: src/app/inventory-page/StoreBucketDropTarget.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'canDrop': string; 'equipped': string; 'grouped': string; 'over': string; 'unequipped': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/StoreBucketDropTarget.tsx ================================================ import { hideDragFixOverlay } from 'app/inventory/DragPerformanceFix'; import { InventoryBucket } from 'app/inventory/inventory-buckets'; import { DimItem } from 'app/inventory/item-types'; import { dropItem } from 'app/inventory/move-item'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { itemCanBeEquippedByStoreId } from 'app/utils/item-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import React from 'react'; import { useDrop } from 'react-dnd'; import './StoreBucket.scss'; import * as styles from './StoreBucketDropTarget.m.scss'; interface Props { bucket: InventoryBucket; storeId: string; storeClassType: DestinyClass; equip?: boolean; children?: React.ReactNode; className?: string; grouped: boolean; } export default function StoreBucketDropTarget({ storeId, children, equip, className, storeClassType, bucket, grouped, }: Props) { const dispatch = useThunkDispatch(); const [{ isOver, canDrop }, dropRef] = useDrop< DimItem, unknown, { isOver: boolean; canDrop: boolean } >( () => ({ accept: bucket.inPostmaster ? [] : [bucket.hash.toString(), `${storeId}-${bucket.hash}`, 'postmaster'], collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop() }), drop: (item) => dispatch(dropItem(item, storeId, Boolean(equip))), canDrop: (item) => { // You can drop anything that can be transferred into a non-equipped bucket if (!equip) { return true; } // But equipping has requirements return itemCanBeEquippedByStoreId(item, storeId, storeClassType); }, }), [storeId, bucket, storeClassType, equip], ); // TODO: I don't like that we're managing the classes for sub-bucket here return (
{ dropRef(el); }} className={clsx('sub-bucket', className, equip ? styles.equipped : styles.unequipped, { [styles.over]: canDrop && isOver, [styles.canDrop]: canDrop, [styles.grouped]: grouped, })} onClick={hideDragFixOverlay} aria-label={bucket.name} > {children}
); } ================================================ FILE: src/app/inventory-page/StoreBuckets.m.scss ================================================ @use '../variables.scss' as *; /* The bucket label shown on mobile only */ .bucketLabel { display: flex; flex-direction: row; align-items: center; text-transform: uppercase; letter-spacing: 2px; font-size: 14px; min-height: 34px; opacity: 0.8; margin: 0; padding: 0 var(--inventory-column-padding); gap: 0.5em; } .postmasterFull { background-color: rgb(151, 4, 1, 0.25) !important; } // If there's a "pull from postmaster" button, we need to stack the bucket // vertically with the button, not side-by-side like we do when we have equipped // and unequipped sub-buckets. .hasButton { flex-direction: column; } // A modifier on .store-cell for things like consumables that span all characters in D2 .accountWideCell { grid-column: 1 / span var(--num-characters); } // For account-wide buckets like consumables, in single-character view, split // the horizontal space evenly between vault and character. .singleCharacterAccountWideRow { grid-template-columns: 1fr 1fr; } .vaultUnder { > .vaultCell { grid-column: 1 / -1; margin-top: 4px; margin-bottom: 16px; > :global(.sub-bucket) { min-height: var(--item-size); } } } ================================================ FILE: src/app/inventory-page/StoreBuckets.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'accountWideCell': string; 'bucketLabel': string; 'hasButton': string; 'postmasterFull': string; 'singleCharacterAccountWideRow': string; 'vaultCell': string; 'vaultUnder': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/inventory-page/StoreBuckets.tsx ================================================ import { PullFromPostmaster } from 'app/inventory/PullFromPostmaster'; import { InventoryBucket } from 'app/inventory/inventory-buckets'; import { DimStore } from 'app/inventory/store-types'; import { findItemsByBucket } from 'app/inventory/stores-helpers'; import { POSTMASTER_SIZE, postmasterAlmostFull, postmasterSpaceUsed, } from 'app/loadout-drawer/postmaster'; import clsx from 'clsx'; import { BucketHashes } from 'data/d2/generated-enums'; import React from 'react'; import StoreBucket from '../inventory-page/StoreBucket'; import * as styles from './StoreBuckets.m.scss'; /** One row of store buckets, one for each character and vault. */ export function StoreBuckets({ bucket, stores, vault, currentStore, labels, singleCharacter, vaultUnder = false, }: { bucket: InventoryBucket; stores: DimStore[]; vault: DimStore; currentStore: DimStore; labels?: boolean; singleCharacter: boolean; vaultUnder?: boolean; }) { let content: React.ReactNode; // Don't show buckets with no items if ( (!bucket.accountWide || bucket.hash === BucketHashes.SpecialOrders) && !stores.some((s) => findItemsByBucket(s, bucket.hash).length > 0) ) { return null; } if (bucket.accountWide) { // If we're in mobile view, we only render one store const allStoresView = stores.length > 1; content = ( <> {(allStoresView || stores[0] !== vault) && (
)} {(allStoresView || stores[0] === vault) && (
)} ); } else { content = stores.map((store) => { if (!bucket.vaultBucket && store.isVault) { // Don't bother adding a cell for vaultless buckets in the vault - doing so will // add an empty space in vaultUnder mode. return null; } const hasPullFromPostmaster = bucket.hash === BucketHashes.LostItems && store.destinyVersion === 2; return (
{(!store.isVault || bucket.vaultBucket || bucket.inPostmaster) && ( )} {hasPullFromPostmaster && findItemsByBucket(store, bucket.hash).length > 0 && ( )}
); }); } const postMasterSpaceUsed = postmasterSpaceUsed(stores[0]); const checkPostmaster = bucket.hash === BucketHashes.LostItems; return (
{labels && (
{bucket.name} {checkPostmaster && ( ({postMasterSpaceUsed}/{POSTMASTER_SIZE}) )}
)} {content}
); } ================================================ FILE: src/app/inventory-page/StoreInventoryItem.tsx ================================================ import { useThunkDispatch } from 'app/store/thunk-dispatch'; import React, { memo, useCallback } from 'react'; import ConnectedInventoryItem from '../inventory/ConnectedInventoryItem'; import DraggableInventoryItem from '../inventory/DraggableInventoryItem'; import ItemPopupTrigger from '../inventory/ItemPopupTrigger'; import { DimItem } from '../inventory/item-types'; import { moveItemToCurrentStore } from '../inventory/move-item'; /** * The "full" inventory item, which can be dragged around and which pops up a move popup when clicked. */ export default memo(function StoreInventoryItem({ item }: { item: DimItem }) { const dispatch = useThunkDispatch(); const doubleClicked = useCallback( (e: React.MouseEvent) => dispatch(moveItemToCurrentStore(item, e)), [dispatch, item], ); return ( {(ref, onClick) => ( )} ); }); ================================================ FILE: src/app/inventory-page/Stores.scss ================================================ @use '../variables.scss' as *; @layer base { .store-row { width: 100%; display: grid; grid-template-columns: repeat( var(--num-characters), calc(#{$equipped-item-total-outset} + var(--character-column-width) + var(--column-padding)) ) /* Vault takes the rest*/ 1fr; box-sizing: border-box; padding-right: calc(var(--sidebar-size) * var(--expanded-sidebars)); @include phone-portrait { // Full-width, single column display: block; padding-right: 0; } } .equipped-item { border: $equipped-item-border solid #ddd; height: fit-content; padding: $equipped-item-padding; margin-top: -1 * ($equipped-item-border + $equipped-item-padding); } .store-cell { display: flex; flex-direction: row; padding: 0 var(--inventory-column-padding); box-sizing: border-box; .store-header & { padding: 16px var(--inventory-column-padding) 6px var(--inventory-column-padding); flex-direction: column; &:focus { outline: none; } } } .store-header { position: sticky; backface-visibility: hidden; left: 0; width: 100%; z-index: 10; grid-template-columns: repeat( var(--num-characters), calc(6px + var(--character-column-width) + var(--column-padding)) ) min-content min-content 1fr !important; background: var(--theme-header-characters-bg); background-position: center top; background-repeat: no-repeat; background-size: 100% 100vh; @include below-header; @include phone-portrait { padding-left: 0; overflow: hidden; } } } ================================================ FILE: src/app/inventory-page/Stores.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { bucketsSelector, sortedStoresSelector } from 'app/inventory/selectors'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { useSelector } from 'react-redux'; import PhoneStores from '../inventory-page/PhoneStores'; import DesktopStores from './DesktopStores'; /** * Display inventory and character headers for all characters and the vault. */ export default function Stores() { const stores = useSelector(sortedStoresSelector); const buckets = useSelector(bucketsSelector); const singleCharacter = useSelector(settingSelector('singleCharacter')); const isPhonePortrait = useIsPhonePortrait(); if (!stores.length || !buckets) { return null; } return isPhonePortrait ? ( ) : ( ); } ================================================ FILE: src/app/issue-awareness-banner/Game2Give.m.scss ================================================ .issueAwarenessBanner { width: 573px; z-index: 5; display: flex; flex-direction: column; align-content: center; align-items: center; transition: opacity 0.5s ease-in-out; margin-top: 16px; } .item { background-color: rgb(0, 0, 0, 0.3); display: flex; flex-direction: row; } .hero { display: block; width: 45%; object-fit: contain; padding: 0.25rem; } .info { display: flex; flex-direction: column; width: 100%; justify-content: flex-start; padding: 0.25rem; flex-grow: 1; } .cta { margin: 0; text-align: center; flex-grow: 1; } .buttons { display: flex; padding: 0.25rem 0; flex-direction: row; justify-content: space-evenly; } .thermo { background-color: dimgrey; display: flex; } .track { width: unset; flex-grow: 1; } .mercury { width: unset; background-color: #e8a534; padding: 0.125rem 0.25rem; overflow-wrap: normal; background-image: linear-gradient( -45deg, transparent 33%, rgb(0, 0, 0, 0.1) 33%, rgb(0, 0, 0, 0.1) 66%, transparent 66% ), linear-gradient(to top, rgb(255, 255, 255, 0.25), rgb(0, 0, 0, 0.25)), linear-gradient(to left, #ac7b28, #e8a534); border-radius: 2px; background-size: 35px 100%, 100% 100%, 100% 100%; } .goal { flex-shrink: 0; width: unset; padding: 0.125rem 0.25rem; background: darkslategrey; } ================================================ FILE: src/app/issue-awareness-banner/Game2Give.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttons': string; 'cta': string; 'goal': string; 'hero': string; 'info': string; 'issueAwarenessBanner': string; 'item': string; 'mercury': string; 'thermo': string; 'track': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/issue-awareness-banner/Game2Give.tsx ================================================ import ExternalLink from 'app/dim-ui/ExternalLink'; import { percent } from 'app/shell/formatters'; import * as styles from './Game2Give.m.scss'; // import heroimage from './bungie-day-giving-festival.jpg'; import heroimage from './g2g-banner.jpg'; import useGame2GiveData from './useGame2GiveData'; export default function Game2Give() { const game2GiveState = useGame2GiveData(); return (
{game2GiveState.loaded && (

Support the Bungie Foundation so together we can make an impact on the world. Donate $100 to enter a raffle for a custom painted Nerf Gjallarhorn!

Donate Learn More {game2GiveState.streamIsLive && game2GiveState.streamIsEnabled && ( 🔴 Live Stream )}
${game2GiveState.donations.toLocaleString()}
${game2GiveState.goal.toLocaleString()}
{/* {game2GiveState.error &&
Error loading latest
} */}
)} {/* {!game2GiveState.loaded && game2GiveState.error && (
{JSON.stringify(game2GiveState, null, 2)}
Can't load data atm
)} */}
); } ================================================ FILE: src/app/issue-awareness-banner/IssueAwarenessBanner.tsx ================================================ import { issueBannerEnabledSelector } from 'app/dim-api/selectors'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { useSelector } from 'react-redux'; import Game2Give from './Game2Give'; /** * A popup we can enable to get the word out about important issues for the DIM community. Edit the body directly. */ export default function IssueAwarenessBanner() { const isPhonePortrait = useIsPhonePortrait(); const issueBannerEnabled = useSelector(issueBannerEnabledSelector); return !isPhonePortrait && issueBannerEnabled ? : null; } ================================================ FILE: src/app/issue-awareness-banner/useGame2GiveData.tsx ================================================ import { refresh$ } from 'app/shell/refresh-events'; import { useEventBusListener } from 'app/utils/hooks'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; interface Game2GiveJSONResponse { fundraisingGoal: number; sumDonations: number; streamIsLive: boolean; streamIsEnabled: boolean; streamingChannel: string; } export default function useGame2GiveData() { const lastFetched = useRef(0); const [fundraisingState, setFundraisingState] = useState({ goal: 0, donations: 0, streamIsLive: false, streamIsEnabled: false, streamingChannel: '', }); const [syncLoaded, setSyncLoaded] = useState(false); const [syncError, setSyncError] = useState(false); const result = useMemo( () => ({ ...fundraisingState, loaded: syncLoaded, error: syncError, }), [fundraisingState, syncLoaded, syncError], ); const getData = useCallback(async () => { // Don't refresh any more frequently than 10 minutes. if (Date.now() - lastFetched.current < 10 * 60 * 1000) { return; } try { const response = await fetch('https://api.destinyitemmanager.com/donate'); // If there is an error communicating with the Game2Giver server, error gracefully. if (!response.ok) { setSyncError(true); return; } const json = (await response.json()) as Game2GiveJSONResponse; // If there is unexpected data with the Game2Give response, error gracefully. if (!json) { setSyncError(true); return; } // If the request is successful, capture data and reset the error and loading state. setFundraisingState({ goal: json.fundraisingGoal, donations: json.sumDonations, streamIsLive: json.streamIsLive, streamIsEnabled: json.streamIsEnabled, streamingChannel: json.streamingChannel, }); setSyncLoaded(true); setSyncError(false); } catch { setSyncError(true); } finally { lastFetched.current = Date.now(); } }, []); // Refresh data whenever DIM would refresh the Bungie.net profile useEventBusListener(refresh$, getData); useEffect(() => { getData(); }, [getData]); return result; } ================================================ FILE: src/app/item-actions/ActionButton.m.scss ================================================ @use '../variables.scss' as *; .entry { display: flex; flex-direction: row; align-items: center; padding: 6px 8px; > img, > :global(.app-icon) { display: flex; align-items: center; justify-content: center; height: 24px; width: 24px; font-size: 16px; text-align: center; } } .actionButton { composes: resetButton from 'app/dim-ui/common.m.scss'; composes: entry; @media (min-width: 541px) { @include interactive($hover: true, $focus: true) { outline: none; background: var(--theme-accent-primary); color: var(--theme-text-invert); :global(.app-icon) { color: var(--theme-text-invert); } } } &:disabled { filter: contrast(0.5) brightness(0.5); cursor: not-allowed; } } ================================================ FILE: src/app/item-actions/ActionButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'actionButton': string; 'entry': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-actions/ActionButton.tsx ================================================ import { symbolize } from 'app/hotkeys/hotkeys'; import React from 'react'; import * as styles from './ActionButton.m.scss'; export default function ActionButton({ disabled, title, hotkey, hotkeyDescription, children, onClick, }: { title?: string; disabled?: boolean; hotkey?: string; hotkeyDescription?: string; children: React.ReactNode; onClick: () => void; }) { return ( ); } ================================================ FILE: src/app/item-actions/ActionButtons.m.scss ================================================ .entry { composes: entry from './ActionButton.m.scss'; } .label { margin-left: 8px; } .tagSelectorLabelHidden { padding: 0 !important; :global(.dim-button) { border-radius: 0; padding: 6px 8px; > div > :global(.app-icon) { display: flex; align-items: center; justify-content: center; font-size: 16px; margin: 0; height: 24px; width: 24px; } } } ================================================ FILE: src/app/item-actions/ActionButtons.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'entry': string; 'label': string; 'tagSelectorLabelHidden': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-actions/ActionButtons.tsx ================================================ import { addCompareItem } from 'app/compare/actions'; import { settingSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { showInfuse } from 'app/infuse/infuse'; import { canSyncLockState } from 'app/inventory/SyncTagLock'; import { DimItem } from 'app/inventory/item-types'; import { consolidate, distribute } from 'app/inventory/move-item'; import { sortedStoresSelector, tagSelector } from 'app/inventory/selectors'; import { getStore } from 'app/inventory/stores-helpers'; import ActionButton from 'app/item-actions/ActionButton'; import LockButton from 'app/item-actions/LockButton'; import ItemTagSelector from 'app/item-popup/ItemTagSelector'; import { hideItemPopup } from 'app/item-popup/item-popup'; import { ItemActionsModel } from 'app/item-popup/item-popup-actions'; import { addItemToLoadout } from 'app/loadout-drawer/loadout-events'; import { AppIcon, addIcon, compareIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import clsx from 'clsx'; import { useDispatch, useSelector } from 'react-redux'; import arrowsIn from '../../images/arrows-in.png'; import arrowsOut from '../../images/arrows-out.png'; import d2Infuse from '../../images/d2infuse.png'; import * as styles from './ActionButtons.m.scss'; interface ActionButtonProps { item: DimItem; label?: boolean; } export function CompareActionButton({ item, label }: ActionButtonProps) { const dispatch = useDispatch(); const openCompare = () => { hideItemPopup(); dispatch(addCompareItem(item)); }; if (!item.comparable) { return null; } return ( {label && {t('Compare.Button')}} ); } export function LockActionButton({ item, label, noHotkey, }: ActionButtonProps & { noHotkey?: boolean }) { const autoLockTagged = useSelector(settingSelector('autoLockTagged')); const tag = useSelector(tagSelector(item)); if (!item.lockable && !item.trackable) { return null; } const disabled = autoLockTagged && tag !== undefined && canSyncLockState(item); const type = item.lockable ? 'lock' : 'track'; // Let's keep these translations around? // t('MovePopup.FavoriteUnFavorite.Favorited') // t('MovePopup.FavoriteUnFavorite.Unfavorited') const title = type === 'lock' ? item.locked ? t('MovePopup.LockUnlock.Locked') : t('MovePopup.LockUnlock.Unlocked') : item.tracked ? t('MovePopup.TrackUntrack.Tracked') : t('MovePopup.TrackUntrack.Untracked'); return ( {label && {title}} ); } export function TagActionButton({ item, label, hideKeys, }: ActionButtonProps & { hideKeys?: boolean }) { if (!item.taggable) { return null; } return (
); } export function ConsolidateActionButton({ item, label, actionModel, }: ActionButtonProps & { actionModel: ItemActionsModel }) { const stores = useSelector(sortedStoresSelector); const owner = getStore(stores, item.owner); const dispatch = useThunkDispatch(); if (!actionModel.canConsolidate) { return null; } const dispatchConsolidate = () => { if (owner) { dispatch(consolidate(item, owner)); hideItemPopup(); } }; return ( {label && {t('MovePopup.Consolidate')}} ); } export function DistributeActionButton({ item, label, actionModel, }: ActionButtonProps & { actionModel: ItemActionsModel }) { const dispatch = useThunkDispatch(); if (!actionModel.canDistribute) { return null; } const dispatchDistribute = () => { dispatch(distribute(item)); hideItemPopup(); }; return ( {label && {t('MovePopup.DistributeEvenly')}} ); } export function InfuseActionButton({ item, label, actionModel, }: ActionButtonProps & { actionModel: ItemActionsModel }) { if (!actionModel.infusable) { return null; } const infuse = () => { showInfuse(item); hideItemPopup(); }; return ( {label && {t('MovePopup.Infuse')}} ); } export function LoadoutActionButton({ item, label, actionModel, }: ActionButtonProps & { actionModel: ItemActionsModel }) { if (!actionModel.loadoutable) { return null; } const addToLoadout = () => { hideItemPopup(); addItemToLoadout(item); }; return ( {label && {t('MovePopup.AddToLoadout')}} ); } ================================================ FILE: src/app/item-actions/ItemAccessoryButtons.tsx ================================================ import { DimItem } from 'app/inventory/item-types'; import { ItemActionsModel } from 'app/item-popup/item-popup-actions'; import OpenOnStreamDeckButton from 'app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton'; import { streamDeckEnabledSelector } from 'app/stream-deck/selectors'; import { useSelector } from 'react-redux'; import { CompareActionButton, ConsolidateActionButton, DistributeActionButton, InfuseActionButton, LoadoutActionButton, LockActionButton, TagActionButton, } from './ActionButtons'; /** * "Accessory" buttons like tagging, locking, comparing, adding to loadout. Displayed separately on mobile, but together with the move actions on desktop. */ export default function ItemAccessoryButtons({ item, mobile, actionsModel, showLabel = true, }: { item: DimItem; mobile: boolean; showLabel: boolean; actionsModel: ItemActionsModel; }) { const streamDeckEnabled = $featureFlags.elgatoStreamDeck ? // eslint-disable-next-line react-hooks/rules-of-hooks useSelector(streamDeckEnabledSelector) : false; return ( <> {actionsModel.taggable && ( )} {actionsModel.lockable && } {actionsModel.comparable && } {actionsModel.canConsolidate && ( )} {actionsModel.canDistribute && ( )} {actionsModel.loadoutable && ( )} {actionsModel.infusable && ( )} {streamDeckEnabled && } ); } ================================================ FILE: src/app/item-actions/ItemActionsDropdown.m.scss ================================================ .storeIcon { margin-right: 4px; } .dropdownButton { composes: filterBarButton from '../search/SearchBar.m.scss'; margin: 0; } ================================================ FILE: src/app/item-actions/ItemActionsDropdown.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'dropdownButton': string; 'storeIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-actions/ItemActionsDropdown.tsx ================================================ import { SearchType } from '@destinyitemmanager/dim-api-types'; import { destinyVersionSelector } from 'app/accounts/selectors'; import { compareFilteredItems } from 'app/compare/actions'; import { saveSearch } from 'app/dim-api/basic-actions'; import { recentSearchesSelector } from 'app/dim-api/selectors'; import Dropdown, { Option } from 'app/dim-ui/Dropdown'; import { t } from 'app/i18next-t'; import { bulkLockItems, bulkTagItems } from 'app/inventory/bulk-actions'; import { storesSortedByImportanceSelector } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { itemMoveLoadout } from 'app/loadout-drawer/auto-loadouts'; import { applyLoadout } from 'app/loadout-drawer/loadout-apply'; import { TagCommandInfo } from 'app/organizer/ItemActions'; import { validateQuerySelector } from 'app/search/items/item-search-filter'; import { canonicalizeQuery, parseQuery } from 'app/search/query-parser'; import { toggleSearchResults } from 'app/shell/actions'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { stripSockets } from 'app/strip-sockets/strip-sockets-actions'; import { compact } from 'app/utils/collections'; import { memo } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import { TagCommand, itemTagSelectorList } from '../inventory/dim-item-info'; import { DimItem } from '../inventory/item-types'; import { AppIcon, clearIcon, compareIcon, faList, faWindowClose, lockIcon, starIcon, starOutlineIcon, stickyNoteIcon, unlockedIcon, } from '../shell/icons'; import { loadingTracker } from '../shell/loading-tracker'; import * as styles from './ItemActionsDropdown.m.scss'; /** * Various actions that can be performed on an item */ export default memo(function ItemActionsDropdown({ searchActive, filteredItems, searchQuery, fixed, bulkNote, }: { searchQuery: string; filteredItems: DimItem[]; searchActive: boolean; fixed?: boolean; bulkNote: (items: DimItem[]) => Promise; }) { const dispatch = useThunkDispatch(); const isPhonePortrait = useIsPhonePortrait(); const stores = useSelector(storesSortedByImportanceSelector); const destinyVersion = useSelector(destinyVersionSelector); let isComparable = false; if (filteredItems.length) { const type = filteredItems[0].typeName; isComparable = filteredItems.every((i) => i.typeName === type); } const canStrip = filteredItems.some((i) => i.sockets?.allSockets.some( (s) => s.emptyPlugItemHash && s.plugged?.plugDef.hash !== s.emptyPlugItemHash, ), ); const bulkTag = loadingTracker.trackPromise(async (selectedTag: TagCommand) => { // Bulk tagging const tagItems = filteredItems.filter((i) => i.taggable); dispatch(bulkTagItems(tagItems, selectedTag)); }); const bulkLock = loadingTracker.trackPromise(async (selectedTag: 'lock' | 'unlock') => { // Bulk locking/unlocking const state = selectedTag === 'lock'; const lockables = filteredItems.filter((i) => i.lockable); dispatch(bulkLockItems(lockables, state)); }); const compareMatching = () => { dispatch(compareFilteredItems(searchQuery, filteredItems, undefined)); }; // Move items matching the current search. Max 9 per type. const applySearchLoadout = async (store: DimStore) => { const loadout = itemMoveLoadout(filteredItems, store); dispatch(applyLoadout(store, loadout, { allowUndo: true })); }; const bulkItemTags: (Omit & { label: string })[] = itemTagSelectorList .filter((t) => t.type) .map((tag) => ({ ...tag, label: t('Header.TagAs', { tag: t(tag.label) }), })); bulkItemTags.push({ type: 'clear', label: t('Tags.ClearTag'), icon: clearIcon }); // Is the current search saved? const recentSearches = useSelector(recentSearchesSelector(SearchType.Item)); const validateQuery = useSelector(validateQuerySelector); const { valid, saveable } = validateQuery(searchQuery); const canonical = searchQuery ? canonicalizeQuery(parseQuery(searchQuery)) : ''; const saved = canonical ? recentSearches.find((s) => s.query === canonical)?.saved : false; const toggleSaved = () => { // TODO: keep track of the last search, if you search for something more narrow immediately after then replace? dispatch(saveSearch({ query: searchQuery, saved: !saved, type: SearchType.Item })); }; const location = useLocation(); const onInventory = location.pathname.endsWith('inventory'); const showSearchResults = onInventory; const dropdownOptions: Option[] = compact([ isPhonePortrait && { key: 'favoriteSearch', onSelected: toggleSaved, disabled: !searchQuery.length || !saveable, content: ( <> {t('Header.SaveSearch')} ), }, isPhonePortrait && showSearchResults && { key: 'showSearchResults', onSelected: () => dispatch(toggleSearchResults()), disabled: !searchQuery.length || !valid || filteredItems.length === 0, content: ( <> {t('Header.SearchResults')} ), }, isPhonePortrait && { key: 'mobile' }, ...stores.map((store) => ({ key: `move-${store.id}`, onSelected: () => applySearchLoadout(store), disabled: !searchActive, content: ( <> {' '} {store.isVault ? t('MovePopup.SendToVault') : t('MovePopup.StoreWithName', { character: store.name })} ), })), { key: 'characters' }, { key: 'compare', onSelected: compareMatching, disabled: !isComparable || !searchActive, content: ( <> {t('Header.CompareMatching')} ), }, destinyVersion === 2 && { key: 'strip-sockets', onSelected: () => stripSockets(searchQuery), disabled: !canStrip || !searchActive, content: ( <> {t('StripSockets.Action')} ), }, { key: 'note', onSelected: () => bulkNote(filteredItems), disabled: !searchActive, content: ( <> {t('Organizer.Note')} ), }, { key: 'lock-item', onSelected: () => bulkLock('lock'), disabled: !searchActive, content: ( <> {t('Tags.LockAll')} ), }, { key: 'unlock-item', onSelected: () => bulkLock('unlock'), disabled: !searchActive, content: ( <> {t('Tags.UnlockAll')} ), }, { key: 'tags' }, ...bulkItemTags.map((tag) => ({ key: tag.type || 'default', onSelected: () => tag.type && bulkTag(tag.type), disabled: !searchActive, content: ( <> {tag.icon && } {tag.label} ), })), ]); return ( ); }); ================================================ FILE: src/app/item-actions/ItemMoveLocations.m.scss ================================================ @use '../variables.scss' as *; .moveLocations { flex-direction: column; align-items: inherit; justify-self: flex-end; &:last-child { padding-bottom: 8px; } } .moveLocationPadding { padding: 6px 8px 0 8px; } .moveLocationIcons { display: flex; flex-direction: row; justify-content: flex-start; margin: 4px 0 0 0; gap: 8px; @include phone-portrait { gap: 5px; } } .move { composes: resetButton from 'app/dim-ui/common.m.scss'; cursor: pointer; position: relative; display: flex; place-content: center; align-items: center; height: 36px; width: 36px; @include phone-portrait { width: calc((100vw - 40px - 5 * 5px) / 7); height: calc((100vw - 40px - 5 * 5px) / 7); max-width: 55px; max-height: 55px; } @include interactive($hover: true, $focus: true) { outline: none; &::after { content: ''; display: block; position: absolute; inset: -2px -2px -2px -2px; border: 1px solid white; } } img { position: absolute; top: 0; left: 0; height: 100%; width: 100%; object-fit: cover; } :global(.app-icon) { position: absolute; height: 60%; width: 60%; top: 20%; left: 20%; filter: drop-shadow(1px 1px 2px black); @include phone-portrait { position: relative; top: 0; left: 0; margin: 0; } } &:disabled { filter: contrast(0.5) brightness(0.5); cursor: not-allowed; } } .vaultLabel { margin-left: 8px; } .moveWithVault { display: flex; > * { display: flex; } } .vaultButton { align-self: flex-end; margin-left: 8px; @include phone-portrait { margin-left: 5px; } } ================================================ FILE: src/app/item-actions/ItemMoveLocations.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'move': string; 'moveLocationIcons': string; 'moveLocationPadding': string; 'moveLocations': string; 'moveWithVault': string; 'vaultButton': string; 'vaultLabel': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-actions/ItemMoveLocations.tsx ================================================ import { StoreIcon } from 'app/character-tile/StoreIcon'; import { symbolize } from 'app/hotkeys/hotkeys'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { moveItemTo } from 'app/inventory/move-item'; import { sortedStoresSelector } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { getStore, getVault } from 'app/inventory/stores-helpers'; import ActionButton from 'app/item-actions/ActionButton'; import ItemMoveAmount from 'app/item-popup/ItemMoveAmount'; import { hideItemPopup } from 'app/item-popup/item-popup'; import { ItemActionsModel, StoreButtonInfo } from 'app/item-popup/item-popup-actions'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { noop } from 'app/utils/functions'; import clsx from 'clsx'; import { BucketHashes } from 'data/d2/generated-enums'; import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import * as styles from './ItemMoveLocations.m.scss'; type MoveSubmit = (store: DimStore, equip?: boolean, moveAmount?: number) => void; export default function ItemMoveLocations({ item, splitVault, actionsModel, }: { item: DimItem; actionsModel: ItemActionsModel; /** Split the vault button out into its own section (for desktop) instead of making it part of the horizontal emblem store buttons. */ splitVault?: boolean; }) { const stores = useSelector(sortedStoresSelector); const vault = getVault(stores)!; // barring a user selection, default to moving the whole stack of this item const [amount, setAmount] = useState(item.amount); const itemOwner = getStore(stores, item.owner); const dispatch = useThunkDispatch(); const submitMoveTo = (store: DimStore, equip = false, moveAmount = amount) => { dispatch(moveItemTo(item, store, equip, moveAmount)); hideItemPopup(); }; if (!itemOwner || !actionsModel.hasMoveControls) { return null; } return ( <> {actionsModel.inPostmaster ? ( actionsModel.pullFromPostmaster && ( ) ) : ( <> {splitVault && actionsModel.canVault && ( submitMoveTo(vault)} /> )} {actionsModel.equip.length > 0 && ( )} {(actionsModel.store.length > 0 || actionsModel.canVault) && (
{!splitVault && actionsModel.canVault && ( submitMoveTo(vault)} /> )}
)} )} {actionsModel.showAmounts && ( )} ); } function VaultButton({ store, handleMove }: { store: DimStore; handleMove: () => void }) { return ( ); } function VaultActionButton({ vault, onClick }: { vault: DimStore; onClick: () => void }) { return ( {t('MovePopup.Vault')} ); } function MoveLocations({ label, shortcutKey, defaultPadding, type, actionsModel, submitMoveTo, }: { label: string; shortcutKey?: string; defaultPadding?: boolean; type: 'equip' | 'store'; actionsModel: ItemActionsModel; submitMoveTo: MoveSubmit; }) { const buttonInfos = actionsModel[type]; const equip = type === 'equip'; if (!buttonInfos.length) { return null; } function moveLocation({ store, enabled }: StoreButtonInfo) { const handleMove = enabled ? () => submitMoveTo(store, equip) : noop; const title = type === 'equip' ? t('MovePopup.EquipWithName', { character: store.name }) : t('MovePopup.StoreWithName', { character: store.name }); const shortcutHelp = shortcutKey && store.current ? ` [${symbolize(shortcutKey)}]` : ''; const button = ( ); return {button}; } return (
{label}
{buttonInfos.map(moveLocation)}
); } /** * Buttons for pulling an item from the Postmaster. */ function PullButtons({ item, itemOwner, submitMoveTo, actionsModel, vault, }: { item: DimItem; itemOwner: DimStore; submitMoveTo: MoveSubmit; actionsModel: ItemActionsModel; vault?: DimStore; }) { const showAmounts = item.maxStackSize > 1 || item.bucket.hash === BucketHashes.Consumables; const moveAllLabel = showAmounts ? t('MovePopup.All') : undefined; const moveMaxLabel = item.amount === actionsModel.maximumMoveAmount ? moveAllLabel : `${actionsModel.maximumMoveAmount}`; return (
{t('MovePopup.PullPostmaster')}
{showAmounts && ( )} {showAmounts ? ( actionsModel.maximumMoveAmount !== 1 && ( ) ) : ( )} {actionsModel.canVault && ( )}
); } ================================================ FILE: src/app/item-actions/LockButton.m.scss ================================================ .inProgress { opacity: 0.5; } ================================================ FILE: src/app/item-actions/LockButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'inProgress': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-actions/LockButton.tsx ================================================ import { trackTriumph } from 'app/dim-api/basic-actions'; import { useHotkey } from 'app/hotkeys/useHotkey'; import { t } from 'app/i18next-t'; import { setItemLockState } from 'app/inventory/item-move-service'; import { hideItemPopup } from 'app/item-popup/item-popup'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import clsx from 'clsx'; import React, { useState } from 'react'; import { DimItem } from '../inventory/item-types'; import { AppIcon, lockIcon, trackedIcon, unlockedIcon, unTrackedIcon } from '../shell/icons'; import ActionButton from './ActionButton'; import * as styles from './LockButton.m.scss'; export default function LockButton({ type, item, disabled, noHotkey, children, }: { item: DimItem; type: 'lock' | 'track'; disabled?: boolean; noHotkey?: boolean; children?: React.ReactNode; }) { const [locking, setLocking] = useState(false); const dispatch = useThunkDispatch(); const lockUnlock = async () => { if (locking || disabled) { return; } let state = false; if (type === 'lock') { state = !item.locked; } else if (type === 'track') { state = !item.tracked; } if (item.pursuit?.recordHash) { dispatch(trackTriumph({ recordHash: item.pursuit.recordHash, tracked: state })); hideItemPopup(); return; } setLocking(true); try { await dispatch(setItemLockState(item, state, type)); } finally { setLocking(false); } }; useHotkey('l', t('Hotkey.LockUnlock'), lockUnlock, noHotkey); const title = lockButtonTitle(item, type); const icon = type === 'lock' ? item.locked ? lockIcon : unlockedIcon : item.tracked ? trackedIcon : unTrackedIcon; const iconElem = ; return ( {children ? ( <> {iconElem} {children} ) : ( iconElem )} ); } function lockButtonTitle(item: DimItem, type: 'lock' | 'track') { const data = { itemType: item.typeName }; // Let's keep these translations around? // t('MovePopup.FavoriteUnFavorite.Favorite', data) // t('MovePopup.FavoriteUnFavorite.Unfavorite', data) return type === 'lock' ? !item.locked ? t('MovePopup.LockUnlock.Lock', data) : t('MovePopup.LockUnlock.Unlock', data) : !item.tracked ? t('MovePopup.TrackUntrack.Track', data) : t('MovePopup.TrackUntrack.Untrack', data); } ================================================ FILE: src/app/item-feed/Highlights.m.scss ================================================ @use '../variables.scss' as *; .type { > * { display: inline; } } .perks { display: flex; flex-direction: column; margin: 4px 0; gap: 1px; > div { margin-left: -3px; padding-left: 4px; } .multiPerk { margin-left: 0; padding-left: 1px; box-shadow: -5px 0 0 -3px var(--theme-item-feed-bg), -5px 0 0 -1px #888; } } .perk { display: flex; flex-direction: row; align-items: center; gap: 2px; :global(.item-img) { height: 24px; width: 24px; margin-left: -1px; } } // copy pasted from src/app/item-popup/PlugTooltip.m.scss with spacing tweaks .enhancedArrow { display: flex; &::after { content: ''; display: inline-block; width: 9px; height: 16px; vertical-align: text-bottom; mask-image: url('images/enhancedArrow.svg'); background-color: $enhancedYellow; margin-left: 1px; margin-top: 4px; } } .stats { composes: flexColumn from '../dim-ui/common.m.scss'; margin: 4px 0 0 0; gap: 4px; } .statRow { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; justify-content: space-between; flex-wrap: wrap; } .armorStats { :global(.stat) { line-height: 8px; } } ================================================ FILE: src/app/item-feed/Highlights.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'armorStats': string; 'enhancedArrow': string; 'multiPerk': string; 'perk': string; 'perks': string; 'statRow': string; 'stats': string; 'type': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-feed/Highlights.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import { PressTip } from 'app/dim-ui/PressTip'; import { DefItemIcon } from 'app/inventory/ItemIcon'; import { DimItem, DimStat } from 'app/inventory/item-types'; import { DimPlugTooltip } from 'app/item-popup/PlugTooltip'; import { compact } from 'app/utils/collections'; import { itemTypeName } from 'app/utils/item-utils'; import { getArmorArchetypeSocket, getExtraIntrinsicPerkSockets, getIntrinsicArmorPerkSocket, getWeaponArchetype, isEnhancedPerk, socketContainsIntrinsicPlug, socketContainsPlugWithCategory, } from 'app/utils/socket-utils'; import clsx from 'clsx'; import { PlugCategoryHashes } from 'data/d2/generated-enums'; import '../store-stats/CharacterStats.m.scss'; import * as styles from './Highlights.m.scss'; /** * Some useful details about an item, meant to be shown in a summary tile on views like the Item Feed or Item Picker. */ export default function Highlights({ item }: { item: DimItem }) { if (item.bucket.inWeapons && item.sockets) { // Don't ask me why Traits are called "Frames" but it does work. const perkSockets = item.sockets.allSockets.filter( (s) => s.isPerk && (socketContainsPlugWithCategory(s, PlugCategoryHashes.Frames) || (s.hasRandomizedPlugItems && socketContainsIntrinsicPlug(s))), ); const archetype = !item.isExotic && getWeaponArchetype(item)?.displayProperties.name; return ( <> {archetype}
{itemTypeName(item)}
{perkSockets.map((s) => (
1, })} > {s.plugOptions.map((p) => ( } className={styles.perk} >
{p.plugDef.displayProperties.name}
))}
))}
); } else if (item.bucket.inArmor) { const renderStat = (stat: DimStat) => (
{stat.displayProperties.hasIcon ? ( ) : ( `${stat.displayProperties.name}: ` )} {stat.value}
); const extraIntrinsicSockets = compact([ getIntrinsicArmorPerkSocket(item), ...getExtraIntrinsicPerkSockets(item), getArmorArchetypeSocket(item), ]); return ( <>
{item.stats?.filter((s) => s.statHash > 0).map(renderStat)}
{item.stats?.filter((s) => s.statHash < 0).map(renderStat)}
{extraIntrinsicSockets.length > 0 && (
{extraIntrinsicSockets.map((s) => (
1, })} > {s.plugOptions.map((p) => ( } className={styles.perk} > {p.plugDef.displayProperties.name} ))}
))}
)} ); } return null; } ================================================ FILE: src/app/item-feed/ItemFeed.m.scss ================================================ @use '../variables.scss' as *; .list { --search-hidden-opacity: 0.4; padding: 10px; scrollbar-gutter: stable; } .item { composes: flexRow from '../dim-ui/common.m.scss'; align-items: flex-start; margin: 8px 0; gap: 8px; padding-left: 1px; // to account for the item hover outline } .info { flex: 1; } .title { text-decoration: none; font-size: 14px; @include destiny-header; } .classIcon { margin-left: 4px; } .clearButton { margin: 2px 0; width: 100%; text-align: left; } ================================================ FILE: src/app/item-feed/ItemFeed.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'classIcon': string; 'clearButton': string; 'info': string; 'item': string; 'list': string; 'title': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-feed/ItemFeed.tsx ================================================ import CheckButton from 'app/dim-ui/CheckButton'; import ClassIcon from 'app/dim-ui/ClassIcon'; import { VirtualList, WindowVirtualList } from 'app/dim-ui/VirtualList'; import { t } from 'app/i18next-t'; import ConnectedInventoryItem from 'app/inventory/ConnectedInventoryItem'; import DraggableInventoryItem from 'app/inventory/DraggableInventoryItem'; import ItemPopupTrigger from 'app/inventory/ItemPopupTrigger'; import { TagValue } from 'app/inventory/dim-item-info'; import { DimItem } from 'app/inventory/item-types'; import { allItemsSelector, getTagSelector } from 'app/inventory/selectors'; import { useSetting } from 'app/settings/hooks'; import { getItemRecencyKey, isNewerThan } from 'app/shell/item-comparators'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { compareBy, reverseComparator } from 'app/utils/comparators'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { memo } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import Highlights from './Highlights'; import * as styles from './ItemFeed.m.scss'; import TagButtons from './TagButtons'; const Item = memo(function Item({ item, tag }: { item: DimItem; tag: TagValue | undefined }) { const isPhonePortrait = useIsPhonePortrait(); const itemIcon = ( {(ref, onClick) => ( )} ); return (
{isPhonePortrait ? ( itemIcon ) : ( {itemIcon} )}
{item.name} {item.classType !== DestinyClass.Unknown && ( )}
); }); const filteredItemsSelector = createSelector(allItemsSelector, (allItems) => allItems .filter((i) => i.equipment && i.power > 0 && i.taggable) .sort(reverseComparator(compareBy(getItemRecencyKey))), ); const estimatedSize = 120; const overscan = 10; /** * An ordered list of items as they are acquired, optionally hiding items that * have been tagged. The idea is to be able to keep track of what drops you're * getting, and ideally to tag them all as they're coming in. */ export default function ItemFeed({ page }: { page?: boolean }) { const allItems = useSelector(filteredItemsSelector); const getTag = useSelector(getTagSelector); const [hideTagged, setHideTagged] = useSetting('itemFeedHideTagged'); const [itemFeedWatermark, setItemFeedWatermark] = useSetting('itemFeedWatermark'); const untaggedItems = hideTagged ? allItems.filter((i) => !hideTagged || !getTag(i)) : allItems; const items = untaggedItems.filter((i) => isNewerThan(i, itemFeedWatermark)); const header = ( <> {t('ItemFeed.HideTagged')} {items.length > 0 && ( )} {items.length === 0 && untaggedItems.length > 0 && ( <>

{t('ItemFeed.NoNewItems')}

)} ); const numItems = items.length + 1; // one more for the header const renderItem = (index: number) => { if (index === 0) { return header; } const item = items[index - 1]; return ; }; const getItemKey = (index: number) => { if (index === 0) { return 'header;'; } const item = items[index - 1]; return item.index; }; return page ? ( {renderItem} ) : ( {renderItem} ); } ================================================ FILE: src/app/item-feed/ItemFeedPage.m.scss ================================================ .page { composes: dim-page from global; padding: 10px; } ================================================ FILE: src/app/item-feed/ItemFeedPage.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'page': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-feed/ItemFeedPage.tsx ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { useLoadStores } from 'app/inventory/store/hooks'; import { usePageTitle } from 'app/utils/hooks'; import { Suspense, lazy } from 'react'; import * as styles from './ItemFeedPage.m.scss'; const ItemFeed = lazy(() => import(/* webpackChunkName: "item-feed" */ './ItemFeed')); /** * The Item Feed in a full page for mobile. */ export default function ItemFeedPage({ account }: { account: DestinyAccount }) { usePageTitle(t('ItemFeed.Description')); const storesLoaded = useLoadStores(account); if (!storesLoaded) { return ; } return (
}>
); } ================================================ FILE: src/app/item-feed/ItemFeedSidebar.m.scss ================================================ @use '../variables.scss' as *; .trayContainer { position: fixed; right: 0; height: calc(var(--viewport-height) - var(--header-height)); z-index: 10; box-sizing: border-box; width: 0; @include below-header; } .expanded { transform: translate(calc(-1 * var(--sidebar-size)), 0); } .sideTray { background-color: var(--theme-item-feed-bg); width: var(--sidebar-size); height: 100%; box-sizing: border-box; } .trayButton { composes: resetButton from '../dim-ui/common.m.scss'; transform: rotate(-90deg); background: black; position: absolute; right: 100%; color: var(--theme-text); padding: 4px 8px; top: 0; transform-origin: bottom right; white-space: nowrap; font-size: 16px; @include interactive($hover: true) { color: var(--theme-text-invert); background-color: var(--theme-accent-primary); } :global(.app-icon) { margin-left: 2px; } } ================================================ FILE: src/app/item-feed/ItemFeedSidebar.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'expanded': string; 'sideTray': string; 'trayButton': string; 'trayContainer': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-feed/ItemFeedSidebar.tsx ================================================ import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { useSetting } from 'app/settings/hooks'; import { AppIcon, collapseIcon, faCaretUp } from 'app/shell/icons'; import clsx from 'clsx'; import { Suspense, lazy, useEffect } from 'react'; import * as styles from './ItemFeedSidebar.m.scss'; const ItemFeed = lazy(() => import(/* webpackChunkName: "item-feed" */ './ItemFeed')); /** * The Item Feed in an expandable sidebar to be placed on the inventory screen. */ export default function ItemFeedSidebar() { const [expanded, setExpanded] = useSetting('itemFeedExpanded'); const handleToggle = () => setExpanded(!expanded); useEffect(() => { document.querySelector('html')!.style.setProperty('--expanded-sidebars', `${expanded ? 1 : 0}`); }, [expanded]); return (
{expanded && (
}>
)}
); } ================================================ FILE: src/app/item-feed/TagButtons.m.scss ================================================ @use '../variables.scss' as *; .tagButtons { composes: flexRow from '../dim-ui/common.m.scss'; margin-top: 4px; gap: 4px; width: 100%; > * { flex: 1; padding: 0; } } ================================================ FILE: src/app/item-feed/TagButtons.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'tagButtons': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-feed/TagButtons.tsx ================================================ import { addCompareItem } from 'app/compare/actions'; import { clearNewItem, setTag } from 'app/inventory/actions'; import { TagValue, tagConfig } from 'app/inventory/dim-item-info'; import { DimItem } from 'app/inventory/item-types'; import { hideItemPopup } from 'app/item-popup/item-popup'; import { AppIcon, compareIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { compareBy } from 'app/utils/comparators'; import * as styles from './TagButtons.m.scss'; /** * A row of compact buttons for quick-tagging items. */ export default function TagButtons({ item, tag }: { item: DimItem; tag: TagValue | undefined }) { const dispatch = useThunkDispatch(); const tagOptions = Object.values(tagConfig) .filter((t) => t.type !== 'archive') .sort(compareBy((t) => t.sortOrder)); const handleSetTag = (tag: TagValue) => { dispatch(setTag(item, tag)); dispatch(clearNewItem(item.id)); }; const openCompare = () => { hideItemPopup(); dispatch(addCompareItem(item)); }; return (
{tagOptions.map((tagOption) => ( ))}
); } ================================================ FILE: src/app/item-picker/ItemPicker.m.scss ================================================ @use '../variables.scss' as *; // These classes are used in item-picker, mod-picker, exotic-picker and search results .grid { composes: sub-bucket from global; margin: 10px; } .itemPickerItem { composes: resetButton from '../dim-ui/common.m.scss'; position: relative; } .classIcon { box-sizing: border-box; width: calc(var(--item-size) / 4) !important; height: calc(var(--item-size) / 4) !important; font-size: calc(var(--item-size) * 0.71); color: white; position: absolute; z-index: 1; left: 1px; bottom: 1px; display: block; } ================================================ FILE: src/app/item-picker/ItemPicker.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'classIcon': string; 'grid': string; 'itemPickerItem': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-picker/ItemPicker.tsx ================================================ import ClassIcon from 'app/dim-ui/ClassIcon'; import { t } from 'app/i18next-t'; import { hideItemPopup, showItemPopup, showItemPopup$ } from 'app/item-popup/item-popup'; import SearchBar from 'app/search/SearchBar'; import { filterFactorySelector } from 'app/search/items/item-search-filter'; import { uniqBy } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { BucketHashes } from 'data/d2/generated-enums'; import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'; import { mergeProps, useKeyboard, useLongPress, usePress } from 'react-aria'; import { useSelector } from 'react-redux'; import Sheet from '../dim-ui/Sheet'; import '../inventory-page/StoreBucket.scss'; import ConnectedInventoryItem from '../inventory/ConnectedInventoryItem'; import { DimItem } from '../inventory/item-types'; import { allItemsSelector } from '../inventory/selectors'; import { itemSorterSelector } from '../settings/item-sort'; import * as styles from './ItemPicker.m.scss'; import { ItemPickerState } from './item-picker'; export default function ItemPicker({ prompt, filterItems, sortBy, uniqueBy, onItemSelected, onSheetClosed, }: ItemPickerState & { onSheetClosed: () => void; }) { const [liveQuery, setQuery] = useState(''); const query = useDeferredValue(liveQuery); const allItems = useSelector(allItemsSelector); const filters = useSelector(filterFactorySelector); const sortItems = useSelector(itemSorterSelector); const onItemSelectedFn = useCallback( (item: DimItem, onClose: () => void) => { onItemSelected(item); onClose(); }, [onItemSelected], ); const onSheetClosedFn = () => { onItemSelected(undefined); onSheetClosed(); }; const header = (

{prompt || t('ItemPicker.ChooseItem')}

); // All items, filtered by the pre-filter configured on the item picker const filteredItems = useMemo( () => (filterItems ? allItems.filter(filterItems) : allItems), [allItems, filterItems], ); // Further filtered by the search bar in the item picker const items = useMemo(() => { let items = sortItems(filteredItems.filter(filters(query))); if (sortBy) { items = items.toSorted(compareBy(sortBy)); } if (uniqueBy) { items = uniqBy(items, uniqueBy); } return items; }, [sortItems, filteredItems, filters, query, sortBy, uniqueBy]); // TODO: have compact and "list" views // TODO: long press for item popup return ( {({ onClose }) => (
{items.map((item) => ( ))}
)}
); } function ItemPickerItem({ item, onClose, onItemSelectedFn, }: { item: DimItem; onClose: () => void; onItemSelectedFn: (item: DimItem, onClose: () => void) => void; }) { const ref = useRef(null); const { longPressProps } = useLongPress({ onLongPress: () => { showItemPopup(item, ref.current!); }, }); const { pressProps } = usePress({ onPress: (e) => { if (e.shiftKey) { showItemPopup(item, ref.current!); } else if (showItemPopup$.getCurrentValue()?.item) { hideItemPopup(); } else { onItemSelectedFn(item, onClose); } }, }); const { keyboardProps } = useKeyboard({ onKeyDown: (e) => { if (e.key === 'i') { showItemPopup(item, ref.current!); } }, }); // Close the popup if this component is unmounted useEffect( () => () => { if (showItemPopup$.getCurrentValue()?.item?.index === item.index) { hideItemPopup(); } }, [item.index], ); return ( ); } ================================================ FILE: src/app/item-picker/ItemPickerContainer.tsx ================================================ import { noop } from 'app/utils/functions'; import { createContext, use, useCallback, useEffect, useState } from 'react'; import { useLocation } from 'react-router'; import ItemPicker from './ItemPicker'; import { ItemPickerState } from './item-picker'; export const ItemPickerContext = createContext<(value: ItemPickerState | undefined) => void>(noop); /** * A container that can show a single item picker. This is a single element to * help prevent multiple pickers from showing at once and to make the API * easier. It uses context so you can nest item picker containers and the * closest one in the tree to your component will handle showing the picker. */ export default function ItemPickerContainer({ children }: { children: React.ReactNode }) { const parentSetOptions = use(ItemPickerContext); // The "generation" just allows us to set a key so the item picker isn't reused between different invocations const [generation, setGeneration] = useState(0); const [options, setOptionsState] = useState(); const setOptions = useCallback( (newOptions: ItemPickerState | undefined) => { // Close any open item pickers higher up the tree - we want to have only one parentSetOptions(undefined); setOptionsState((options) => { if (options) { // Cleanup any existing item picker options.onItemSelected(undefined); } return newOptions; }); setGeneration((gen) => gen + 1); }, [parentSetOptions], ); const onClose = useCallback(() => { setOptionsState((options) => { if (options) { // Cleanup any existing item picker options.onItemSelected(undefined); } return undefined; }); }, []); // Close the item picker if we change page const location = useLocation(); useEffect(() => { onClose(); }, [location.pathname, onClose]); return ( {children} {options && } ); } ================================================ FILE: src/app/item-picker/item-picker.ts ================================================ import { use, useCallback } from 'react'; import { DimItem } from '../inventory/item-types'; import { ItemPickerContext } from './ItemPickerContainer'; export interface ItemPickerOptions { /** Override the default "Choose an Item" prompt. */ prompt?: string; /** Optionally restrict items to a particular subset. */ filterItems?: (item: DimItem) => boolean; /** An extra sort function that items will be sorted by (beyond the default sort chosen by the user) */ sortBy?: (item: DimItem) => string | number | boolean | undefined; uniqueBy?: (item: DimItem) => string | number | boolean | undefined; } export type ItemPickerState = ItemPickerOptions & { onItemSelected: (result: DimItem | undefined) => void; }; /** * A function to show an item picker UI, optionally filtered to a specific set of items. When an item * is selected, the promise is resolved with that item. It is resolved with undefined if the picker * is closed without a selection. */ export type ShowItemPickerFn = (options: ItemPickerOptions) => Promise; /** * Returns a function to show an item picker UI, optionally filtered to a specific set of items. When an item * is selected, the promise is resolved with that item. It is resolved with undefined if the picker * is closed without a selection. */ export function useItemPicker(): ShowItemPickerFn { const setOptions = use(ItemPickerContext); return useCallback( (options) => new Promise((resolve) => { setOptions({ ...options, onItemSelected: resolve }); }), [setOptions], ); } /** * Returns a function that can be used to hide the item picker. */ export function useHideItemPicker() { const setOptions = use(ItemPickerContext); return useCallback(() => setOptions(undefined), [setOptions]); } ================================================ FILE: src/app/item-popup/AmmoIcon.m.scss ================================================ @use '../variables.scss' as *; .ammoIcon { margin: 0 2px 0 6px; height: 14px; width: 19px; } .primary { opacity: 0.9; } ================================================ FILE: src/app/item-popup/AmmoIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'ammoIcon': string; 'primary': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/AmmoIcon.tsx ================================================ import { LookupTable } from 'app/utils/util-types'; import { DestinyAmmunitionType } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import heavy from 'destiny-icons/general/ammo-heavy.svg'; import primary from 'destiny-icons/general/ammo-primary.svg'; import special from 'destiny-icons/general/ammo-special.svg'; import * as styles from './AmmoIcon.m.scss'; const ammoIcons: LookupTable = { [DestinyAmmunitionType.Primary]: primary, [DestinyAmmunitionType.Special]: special, [DestinyAmmunitionType.Heavy]: heavy, }; export function AmmoIcon({ type, className }: { type: DestinyAmmunitionType; className?: string }) { return ( ); } ================================================ FILE: src/app/item-popup/ApplyPerkSelection.m.scss ================================================ @use '../variables' as *; .buttons { composes: item-details from global; composes: flexRow from '../dim-ui/common.m.scss'; gap: 8px; } .insertButton { composes: dim-button from global; display: flex; flex-direction: row; align-items: center; transition: none; gap: 8px; } ================================================ FILE: src/app/item-popup/ApplyPerkSelection.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttons': string; 'insertButton': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ApplyPerkSelection.tsx ================================================ import { t } from 'app/i18next-t'; import { canInsertPlug, insertPlug } from 'app/inventory/advanced-write-actions'; import { DimItem, DimSocket } from 'app/inventory/item-types'; import { destiny2CoreSettingsSelector, useD2Definitions } from 'app/manifest/selectors'; import { showNotification } from 'app/notifications/notifications'; import { AppIcon, faCheckCircle, refreshIcon, thumbsUpIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { errorMessage } from 'app/utils/errors'; import { wishListSelector } from 'app/wishlists/selectors'; import { useState } from 'react'; import { useSelector } from 'react-redux'; import * as styles from './ApplyPerkSelection.m.scss'; export default function ApplyPerkSelection({ item, setSocketOverride, onApplied, }: { item: DimItem; setSocketOverride: (value: { item: DimItem; socket: DimSocket; plugHash: number }) => void; onApplied: () => void; }) { const dispatch = useThunkDispatch(); const defs = useD2Definitions()!; const destiny2CoreSettings = useSelector(destiny2CoreSettingsSelector)!; const [insertInProgress, setInsertInProgress] = useState(false); const wishlistRoll = useSelector(wishListSelector(item)); if (!item.sockets) { return null; } const plugOverridesToSave: { socket: DimSocket; plugHash: number }[] = []; const wishListSocketChanges: { socket: DimSocket; plugHash: number }[] = []; for (const socket of item.sockets.allSockets) { // Find wishlist perks that aren't selected if ( wishlistRoll && !wishlistRoll.isUndesirable && socket.isPerk && socket.plugOptions.length > 1 ) { const wishlistPlug = socket.plugOptions.find((p) => wishlistRoll.wishListPerks.has(p.plugDef.hash), ); if ( wishlistPlug && socket.actuallyPlugged !== wishlistPlug && socket.plugged !== wishlistPlug ) { wishListSocketChanges.push({ socket, plugHash: wishlistPlug.plugDef.hash }); } } if ( !item.vendor && socket.actuallyPlugged && socket.plugged && canInsertPlug(socket, socket.plugged.plugDef.hash, destiny2CoreSettings, defs) ) { plugOverridesToSave.push({ socket, plugHash: socket.plugged.plugDef.hash }); } } const onInsertPlugs = async () => { if (insertInProgress) { return; } setInsertInProgress(true); try { for (const { socket, plugHash } of plugOverridesToSave) { try { await dispatch(insertPlug(item, socket, plugHash)); } catch (e) { const plugName = defs.InventoryItem.get(plugHash)?.displayProperties.name ?? 'Unknown Plug'; showNotification({ type: 'error', title: t('AWA.Error'), body: t('AWA.ErrorMessage', { error: errorMessage(e), item: item.name, plug: plugName, }), }); return; // bail out without calling onApplied } } onApplied(); } finally { setInsertInProgress(false); } }; const selectWishlistPerks = () => { for (const change of wishListSocketChanges) { setSocketOverride({ ...change, item }); } }; if (wishListSocketChanges.length === 0 && plugOverridesToSave.length === 0) { return null; } // TODO: "ProgressButton" return (
{wishListSocketChanges.length > 0 && ( )} {plugOverridesToSave.length > 0 && ( )}
); } ================================================ FILE: src/app/item-popup/ArchetypeSocket.m.scss ================================================ @use '../variables' as *; .mod { // Change the perk image to be a bit larger --mod-size: var(--archetype-size, calc(#{dim-item-px(34)})); } .row { composes: flexRow from '../dim-ui/common.m.scss'; gap: 8px; padding: 6px 10px; background-color: var(--theme-item-popup-panel-bg); width: 100%; border-bottom: none; box-sizing: border-box; &.isWeapons { gap: 2px; align-items: center; } :global(.armory) & { background: rgb(0, 0, 0, 0.5); backdrop-filter: blur(6px); } } .info { flex-direction: column; align-items: flex-start; flex: 1; width: 0; // margin-right: 8px; } .name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ================================================ FILE: src/app/item-popup/ArchetypeSocket.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'info': string; 'isWeapons': string; 'mod': string; 'name': string; 'row': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ArchetypeSocket.tsx ================================================ import { DimItem, DimSocket } from 'app/inventory/item-types'; import clsx from 'clsx'; import React from 'react'; import * as styles from './ArchetypeSocket.m.scss'; import { PlugClickHandler } from './ItemSockets'; import Socket from './Socket'; /** * A special socket display for the "archetype" socket (or exotic perk) and */ export default function ArchetypeSocket({ archetypeSocket, item, noTooltip, children, onClick, }: { archetypeSocket?: DimSocket; item: DimItem; noTooltip?: boolean; children?: React.ReactNode; onClick?: PlugClickHandler; }) { if (!archetypeSocket?.plugged) { return null; } return ( <>
{archetypeSocket.plugged.plugDef.displayProperties.name}
{children}
); } export function ArchetypeRow({ className, children, isWeapons, }: { className?: string; children: React.ReactNode; isWeapons?: boolean; }) { return (
{children}
); } ================================================ FILE: src/app/item-popup/BreakerType.m.scss ================================================ .breakerIcon { margin: 0 4px; width: 15px; height: 15px; } .artifactBreaker { background-color: #3f8e90; padding: 1px 2px; border-radius: 1px; } ================================================ FILE: src/app/item-popup/BreakerType.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'artifactBreaker': string; 'breakerIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/BreakerType.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { getSeasonalBreakerTypeHash } from 'app/utils/item-utils'; import clsx from 'clsx'; import * as styles from './BreakerType.m.scss'; export default function BreakerType({ item }: { item: DimItem }) { const defs = useD2Definitions()!; let breakerType = item.breakerType; let breakerClass: string | undefined; if (!breakerType) { const breakerTypeHash = getSeasonalBreakerTypeHash(item); if (breakerTypeHash) { breakerType = defs.BreakerType.get(breakerTypeHash); breakerClass = styles.artifactBreaker; } } return ( breakerType && ( ) ); } ================================================ FILE: src/app/item-popup/DeepSightHarmonizerIcon.m.scss ================================================ @use '../variables.scss' as *; .deepsightHarmonizerIcon { flex-grow: 1; max-width: fit-content; img { display: block; width: 15px; height: 15px; border: 1px solid #999; margin-right: 4px; } } ================================================ FILE: src/app/item-popup/DeepSightHarmonizerIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'deepsightHarmonizerIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/DeepsightHarmonizerIcon.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import { PressTip } from 'app/dim-ui/PressTip'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { DEEPSIGHT_HARMONIZER } from 'app/search/d2-known-values'; import * as styles from './DeepSightHarmonizerIcon.m.scss'; export function DeepsightHarmonizerIcon({ item }: { item: DimItem }) { return ( } className={styles.deepsightHarmonizerIcon} > ); } export function HarmonizerIcon() { const defs = useD2Definitions()!; const harmonizerIcon = defs.InventoryItem.get(DEEPSIGHT_HARMONIZER)?.displayProperties.icon; return ; } function HarmonizableTooltipContent({ item }: { item: DimItem }) { const harmonizableTooltipText = item.tooltipNotifications?.map((t) => t.displayString); const harmonizableTooltip = ( <>

{harmonizableTooltipText}

{t('Filter.FilterWith')} deepsight:harmonizable

); return harmonizableTooltip; } ================================================ FILE: src/app/item-popup/DesktopItemActions.m.scss ================================================ @use '../variables.scss' as *; .interaction { display: flex; flex-direction: column; justify-content: space-between; background-color: var(--theme-item-popup-actions-bg); user-select: none; box-shadow: 0 0 0 1px var(--theme-item-popup-border), var(--theme-drop-shadow); } .collapseButton { composes: resetButton from 'app/dim-ui/common.m.scss'; display: flex; justify-content: flex-end; cursor: pointer; @include interactive($hover: true) { :global(.app-icon) { color: var(--theme-accent-primary); } } :global(.app-icon) { margin: 7px 10px 0; display: block; width: 16px; font-size: 16px; } .collapsed & { padding: 5px 0 15px; justify-content: center; border-bottom: 1px solid var(--theme-item-popup-border); } [data-popper-placement^='left'] & { transform: scaleX(-1); } } ================================================ FILE: src/app/item-popup/DesktopItemActions.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'collapseButton': string; 'collapsed': string; 'interaction': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/DesktopItemActions.tsx ================================================ import { addCompareItem } from 'app/compare/actions'; import { useHotkey } from 'app/hotkeys/useHotkey'; import { t } from 'app/i18next-t'; import { showInfuse } from 'app/infuse/infuse'; import { DimItem } from 'app/inventory/item-types'; import { moveItemTo } from 'app/inventory/move-item'; import { sortedStoresSelector } from 'app/inventory/selectors'; import { getCurrentStore, getVault } from 'app/inventory/stores-helpers'; import ItemAccessoryButtons from 'app/item-actions/ItemAccessoryButtons'; import ItemMoveLocations from 'app/item-actions/ItemMoveLocations'; import { hideItemPopup } from 'app/item-popup/item-popup'; import { useSetting } from 'app/settings/hooks'; import { AppIcon, maximizeIcon, minimizeIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; import * as styles from './DesktopItemActions.m.scss'; import { ItemActionsModel } from './item-popup-actions'; export const menuClassName = styles.interaction; export default function DesktopItemActions({ item, actionsModel, }: { item: DimItem; actionsModel: ItemActionsModel; }) { const stores = useSelector(sortedStoresSelector); const dispatch = useThunkDispatch(); const [sidecarCollapsed, setSidecarCollapsed] = useSetting('sidecarCollapsed'); const toggleSidecar = () => setSidecarCollapsed(!sidecarCollapsed); useHotkey('esc', t('Hotkey.ClearDialog'), () => hideItemPopup()); useHotkey('k', t('MovePopup.ToggleSidecar'), toggleSidecar); useHotkey('p', t('Hotkey.Pull'), () => { // TODO: if movable const currentChar = getCurrentStore(stores)!; dispatch(moveItemTo(item, currentChar, false, item.amount)); hideItemPopup(); }); useHotkey('v', t('Hotkey.Vault'), () => { // TODO: if vaultable const vault = getVault(stores)!; dispatch(moveItemTo(item, vault, false, item.amount)); hideItemPopup(); }); useHotkey('c', t('Compare.ButtonHelp'), () => { if (item.comparable) { hideItemPopup(); dispatch(addCompareItem(item)); } }); useHotkey('i', t('MovePopup.InfuseTitle'), (e: KeyboardEvent) => { if (item.infusable) { e.preventDefault(); showInfuse(item); hideItemPopup(); } }); return (
{actionsModel.hasControls && ( )} {!sidecarCollapsed && ( )}
); } ================================================ FILE: src/app/item-popup/EmblemPreview.m.scss ================================================ .container { position: relative; width: min-content; } .banner { position: absolute; top: -1px; right: 4px; left: auto; } .value { position: absolute; right: 25px; top: 27px; width: 50%; text-align: right; } ================================================ FILE: src/app/item-popup/EmblemPreview.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'banner': string; 'container': string; 'value': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/EmblemPreview.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import { DimItem } from 'app/inventory/item-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { ObjectiveValue } from 'app/progress/Objective'; import MetricBanner from 'app/records/MetricBanner'; import * as styles from './EmblemPreview.m.scss'; export default function EmblemPreview({ item }: { item: DimItem }) { const defs = useD2Definitions()!; const metricDef = item.metricObjective && item.metricHash !== undefined && defs.Metric.get(item.metricHash); const parentPresentationNode = metricDef && defs.PresentationNode.get(metricDef.parentNodeHashes[0]); const trait = metricDef && defs.Trait.get(metricDef.traitHashes.at(-1)!); const objectiveHash = item.metricObjective?.objectiveHash; const objectiveDef = objectiveHash !== undefined && defs.Objective.get(objectiveHash); return (
{item.metricObjective && item.metricHash !== undefined && ( )} {item.metricObjective?.progress !== undefined && objectiveDef && (
)} {item.secondaryIcon && } {parentPresentationNode && metricDef && trait && (
{trait.displayProperties.name} {' // '} {parentPresentationNode.displayProperties.name} {' // '} {metricDef.displayProperties.name}
)}
); } ================================================ FILE: src/app/item-popup/EmoteSockets.m.scss ================================================ .emoteWheel { margin-left: 13px; display: grid; gap: 4px; place-items: center; grid-template-columns: min-content min-content min-content; grid-template-rows: min-content min-content min-content; grid-template-areas: ' . slot0 . ' 'slot2 collection slot3' ' . slot1 . '; } .collectionIcon { border: 0; } ================================================ FILE: src/app/item-popup/EmoteSockets.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'collectionIcon': string; 'emoteWheel': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/EmoteSockets.tsx ================================================ import { DefItemIcon } from 'app/inventory/ItemIcon'; import { DimItem, DimSocket } from 'app/inventory/item-types'; import { DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2'; import * as styles from './EmoteSockets.m.scss'; import { ItemSocketsList, PlugClickHandler } from './ItemSockets'; import Socket from './Socket'; /** * A special socket display for the emote sockets so that we * show the up/down/left/right emotes in their correct positions * instead of a flat list. */ export default function EmoteSockets({ item, itemDef, sockets, onClick, }: { item: DimItem; itemDef: DestinyInventoryItemDefinition; sockets: DimSocket[]; onClick?: PlugClickHandler; }) { const selectorIcon = ; return ( {sockets.map((s, i) => (
))}
{selectorIcon}
); } ================================================ FILE: src/app/item-popup/EnergyMeter.m.scss ================================================ @use '../variables.scss' as *; .energyMeter { margin: 10px; } .upgradePreview { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; overflow: hidden; box-sizing: border-box; gap: 8px; > :first-child { flex: 1; } } .upgradeButton { composes: resetButton from '../dim-ui/common.m.scss'; color: var(--theme-text); font-size: 16px; margin: 4px 0 0 0; } .cost { font-size: 12px !important; } ================================================ FILE: src/app/item-popup/EnergyMeter.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'cost': string; 'energyMeter': string; 'upgradeButton': string; 'upgradePreview': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/EnergyMeter.tsx ================================================ import { EnergyMeterIncrements } from 'app/dim-ui/EnergyIncrements'; import { t } from 'app/i18next-t'; import { insertPlug } from 'app/inventory/advanced-write-actions'; import { DimItem } from 'app/inventory/item-types'; import { getEnergyUpgradeHashes, sumModCosts } from 'app/inventory/store/energy'; import { useD2Definitions } from 'app/manifest/selectors'; import { showNotification } from 'app/notifications/notifications'; import { AppIcon, disabledIcon, enabledIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { compareBy } from 'app/utils/comparators'; import { errorMessage } from 'app/utils/errors'; import { getFirstSocketByCategoryHash } from 'app/utils/socket-utils'; import Cost from 'app/vendors/Cost'; import { SocketCategoryHashes } from 'data/d2/generated-enums'; import { AnimatePresence, Transition, Variants, motion } from 'motion/react'; import { useState } from 'react'; import * as styles from './EnergyMeter.m.scss'; import { SocketCategoryHeader } from './ItemSocketsGeneral'; const upgradeAnimateVariants: Variants = { shown: { height: 'auto', opacity: 1 }, hidden: { height: 0, opacity: 0 }, }; const upgradeAnimateTransition: Transition = { duration: 0.3 }; export default function EnergyMeter({ item }: { item: DimItem }) { const defs = useD2Definitions()!; const energyCapacity = item.energy?.energyCapacity || 0; const [previewCapacity, setPreviewCapacity] = useState(energyCapacity); const dispatch = useThunkDispatch(); if (!item.energy) { return null; } const minCapacity = item.energy.energyCapacity; const previewUpgrade = (i: number) => setPreviewCapacity(Math.max(minCapacity, i)); const resetPreview = () => setPreviewCapacity(energyCapacity); const applyChanges = async () => { if (!$featureFlags.awa) { return; } if (!item.energy || !item.sockets) { return; } // TODO: i18n, maybe check to see if we have enough currency // eslint-disable-next-line no-alert if (!confirm('Pay the costs to upgrade?')) { return; } const upgradeMods = getEnergyUpgradeHashes(item, previewCapacity); const socket = getFirstSocketByCategoryHash(item.sockets, SocketCategoryHashes.ArmorTier)!; try { for (const modHash of upgradeMods) { await dispatch(insertPlug(item, socket, modHash)); } // TODO: show confirmation, hide preview, update item } catch (e) { showNotification({ type: 'error', title: 'Error', body: errorMessage(e) }); } }; return ( defs && (
{Math.max(minCapacity, previewCapacity)} {t('EnergyMeter.Energy')} {previewCapacity > minCapacity && ( {$featureFlags.awa && ( )} )}
) ); } function EnergyUpgradePreview({ item, previewCapacity, }: { item: DimItem; previewCapacity: number; }) { const defs = useD2Definitions()!; if (!item.energy) { return null; } const energyModHashes = getEnergyUpgradeHashes(item, previewCapacity); const costs = sumModCosts( defs, energyModHashes.map((h) => defs.InventoryItem.get(h)), ).sort(compareBy((c) => c.quantity)); return ( <> {item.energy.energyCapacity} → {previewCapacity} {costs.map((cost) => ( ))} ); } ================================================ FILE: src/app/item-popup/ItemDescription.m.scss ================================================ @use '../variables.scss' as *; .description { margin: 5px 10px 5px; white-space: pre-wrap; } .label { font-weight: bold; } .secondaryText { font-style: italic; } ================================================ FILE: src/app/item-popup/ItemDescription.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'description': string; 'label': string; 'secondaryText': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemDescription.tsx ================================================ import { ExpandableTextBlock } from 'app/dim-ui/ExpandableTextBlock'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { wishListSelector } from 'app/wishlists/selectors'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; import * as styles from './ItemDescription.m.scss'; import NotesArea from './NotesArea'; export default function ItemDescription({ item }: { item: DimItem }) { const wishlistItem = useSelector(wishListSelector(item)); // suppressing some unnecessary information for weapons and armor, // to make room for all that other delicious info const showFlavor = !item.bucket.inWeapons && !item.bucket.inArmor; return ( <> {showFlavor && ( <> {Boolean(item.description?.length) && (
)} {Boolean(item.displaySource?.length) && (
)} )} {!$featureFlags.triage && wishlistItem && Boolean(wishlistItem?.notes?.length) && ( {t('WishListRoll.WishListNotes')} {wishlistItem.notes} )} ); } ================================================ FILE: src/app/item-popup/ItemDetails.m.scss ================================================ @use '../variables.scss' as *; .itemDetailsBody { overflow: hidden; } .itemDescription { margin: 5px 10px; white-space: pre-wrap; font-style: italic; } .itemSource { font-style: italic; opacity: 0.7; } .itemShader { display: block; margin: 15px auto; } .acquiredIcon { background: #3c94ff; } .ownedIcon { background: $acquiredGreen; } .fullImage { max-width: 100%; aspect-ratio: auto 16/9; &.milestoneImage { aspect-ratio: auto 7/2; } } .ownedIcon, .acquiredIcon { width: calc(var(--item-size) / 4) !important; height: calc(var(--item-size) / 4) !important; font-size: calc(var(--item-size) / 6) !important; border-radius: 50%; padding: 3px; box-sizing: border-box; } ================================================ FILE: src/app/item-popup/ItemDetails.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'acquiredIcon': string; 'fullImage': string; 'itemDescription': string; 'itemDetailsBody': string; 'itemShader': string; 'itemSource': string; 'milestoneImage': string; 'ownedIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemDetails.tsx ================================================ import { DestinyTooltipText } from 'app/dim-ui/DestinyTooltipText'; import { t, tl } from 'app/i18next-t'; import { createItemContextSelector, storesSelector } from 'app/inventory/selectors'; import { isTrialsPassage } from 'app/inventory/store/objectives'; import { applySocketOverrides, useSocketOverrides } from 'app/inventory/store/override-sockets'; import { getStore } from 'app/inventory/stores-helpers'; import { KillTrackerInfo } from 'app/item-popup/KillTracker'; import { useDefinitions } from 'app/manifest/selectors'; import { ActivityModifier } from 'app/progress/ActivityModifier'; import Objective from 'app/progress/Objective'; import { Reward } from 'app/progress/Reward'; import { RootState } from 'app/store/types'; import { getItemKillTrackerInfo, isD1Item } from 'app/utils/item-utils'; import { SingleVendorSheetContext } from 'app/vendors/single-vendor/SingleVendorSheetContainer'; import clsx from 'clsx'; import { BucketHashes, ItemCategoryHashes } from 'data/d2/generated-enums'; import { use } from 'react'; import { useSelector } from 'react-redux'; import BungieImage from '../dim-ui/BungieImage'; import { DimItem } from '../inventory/item-types'; import { AppIcon, faCheck } from '../shell/icons'; import ApplyPerkSelection from './ApplyPerkSelection'; import EmblemPreview from './EmblemPreview'; import EnergyMeter from './EnergyMeter'; import { ItemPopupExtraInfo } from './item-popup'; import ItemDescription from './ItemDescription'; import * as styles from './ItemDetails.m.scss'; import ItemExpiration from './ItemExpiration'; import ItemPerks from './ItemPerks'; import './ItemPopupBody.scss'; import ItemSockets from './ItemSockets'; import ItemStats from './ItemStats'; import ItemTalentGrid from './ItemTalentGrid'; import MetricCategories from './MetricCategories'; import { WeaponCatalystInfo } from './WeaponCatalystInfo'; import { WeaponCraftedInfo } from './WeaponCraftedInfo'; import { WeaponDeepsightInfo } from './WeaponDeepsightInfo'; // TODO: probably need to load manifest. We can take a lot of properties off the item if we just load the definition here. export default function ItemDetails({ item: originalItem, id, extraInfo = {}, }: { item: DimItem; id: string; extraInfo?: ItemPopupExtraInfo; }) { const defs = useDefinitions()!; const itemCreationContext = useSelector(createItemContextSelector); const [socketOverrides, onPlugClicked, resetSocketOverrides] = useSocketOverrides(); const item = defs.isDestiny2 ? applySocketOverrides(itemCreationContext, originalItem, socketOverrides) : originalItem; const ownerStore = useSelector((state: RootState) => getStore(storesSelector(state), item.owner)); const killTrackerInfo = getItemKillTrackerInfo(item); const showVendor = use(SingleVendorSheetContext); const missingSocketsMessage = item.missingSockets === 'missing' ? tl('MovePopup.MissingSockets') : tl('MovePopup.LoadingSockets'); return (
{item.itemCategoryHashes.includes(ItemCategoryHashes.Shaders) && ( )} {(item.bucket.hash === BucketHashes.Quests || item.itemCategoryHashes.includes(ItemCategoryHashes.Mods_Ornament)) && item.secondaryIcon && ( )} {!item.stats && Boolean(item.collectibleHash) && defs.isDestiny2 && (
{defs.Collectible.get(item.collectibleHash!).sourceString}
)} {defs.isDestiny2 && item.itemCategoryHashes.includes(ItemCategoryHashes.Emblems) && (
)} {defs.isDestiny2 && item.availableMetricCategoryNodeHashes && (
)} {defs.isDestiny2 && } {defs.isDestiny2 && } {defs.isDestiny2 && } {killTrackerInfo && defs.isDestiny2 && ( )} {item.classified &&
{t('ItemService.Classified2')}
} {item.stats && (
)} {isD1Item(item) && item.talentGrid && (
)} {item.missingSockets && (
{t(missingSocketsMessage)}
)} {defs.isDestiny2 && item.energy && defs && } {item.sockets && } {item.perks && } {defs && item.objectives && (
{item.objectives.map((objective) => ( ))}
)} {item.previewVendor !== undefined && item.previewVendor !== 0 && (extraInfo.characterId ?? (ownerStore && !ownerStore.isVault)) && ( )} {defs.isDestiny2 && item.pursuit && item.pursuit.rewards.length !== 0 && (
{t('MovePopup.Rewards')}
{item.pursuit.rewards.map((reward) => ( ))}
)} {defs.isDestiny2 && item.pursuit && item.pursuit.modifierHashes.length !== 0 && (
{item.pursuit.modifierHashes.map((modifierHash) => ( ))}
)} {(extraInfo.owned || extraInfo.acquired) && (
{extraInfo.owned && (
{' '} {extraInfo.mod ? t('MovePopup.OwnedMod') : t('MovePopup.Owned')}
)} {extraInfo.acquired && (
{' '} {extraInfo.mod ? t('MovePopup.AcquiredMod') : t('MovePopup.Acquired')}
)}
)}
); } ================================================ FILE: src/app/item-popup/ItemExpiration.tsx ================================================ import Countdown from 'app/dim-ui/Countdown'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { AppIcon, faClock } from 'app/shell/icons'; import clsx from 'clsx'; export default function ItemExpiration({ item, compact }: { item: DimItem; compact?: boolean }) { const expiration = item.pursuit?.expiration; if (!expiration) { return null; } const expired = expiration.expirationDate.getTime() < Date.now(); const suppressExpiration = expiration.suppressExpirationWhenObjectivesComplete && item.complete; if (suppressExpiration) { return null; } const expiresSoon = expiration.expirationDate.getTime() - Date.now() < 1 * 60 * 60 * 1000; return (
{expired ? ( compact ? ( t('Progress.QuestExpired') ) : ( expiration.expiredInActivityMessage ) ) : ( <> {!compact && t('Progress.QuestExpires')} )}
); } ================================================ FILE: src/app/item-popup/ItemMoveAmount.m.scss ================================================ .moveAmount { margin: 6px 8px; display: flex; flex-direction: column; label { margin-bottom: 2px; } input[type='text'] { width: 3em; } } ================================================ FILE: src/app/item-popup/ItemMoveAmount.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'moveAmount': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemMoveAmount.tsx ================================================ import { t } from 'app/i18next-t'; import { clamp } from 'es-toolkit'; import React from 'react'; import * as styles from './ItemMoveAmount.m.scss'; /** An editor for selecting how much of a stackable item you want. */ export default function ItemMoveAmount({ maximum, amount, onAmountChanged, }: { amount: number; maximum: number; onAmountChanged: (amount: number) => void; }) { const constrain = () => { const constrained = clamp(amount, 1, maximum); if (constrained !== amount) { onAmountChanged(constrained); } }; const onChange = (e: React.ChangeEvent) => { onAmountChanged(parseInt(e.currentTarget.value, 10)); }; return (
); } ================================================ FILE: src/app/item-popup/ItemPerks.m.scss ================================================ .itemPerk { composes: flexRow from '../dim-ui/common.m.scss'; gap: 8px; img { height: 48px; width: 48px; } > *:nth-child(2) { flex: 1; } } .itemPerkName { font-weight: bold; margin-bottom: 2px; } ================================================ FILE: src/app/item-popup/ItemPerks.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'itemPerk': string; 'itemPerkName': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemPerks.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { DimItem } from 'app/inventory/item-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { DestinyItemPerkEntryDefinition } from 'bungie-api-ts/destiny2'; import * as styles from './ItemPerks.m.scss'; export default function ItemPerks({ item }: { item: DimItem }) { if (!item.perks) { return null; } return (
{item.perks.map((perk) => ( ))}
); } function ItemPerk({ perk }: { perk: DestinyItemPerkEntryDefinition }) { const defs = useD2Definitions()!; const perkDef = defs.SandboxPerk.get(perk.perkHash); const { hasIcon, icon, name, description } = perkDef.displayProperties; return (
{hasIcon && }
{name}
); } ================================================ FILE: src/app/item-popup/ItemPerksList.m.scss ================================================ @use '../variables.scss' as *; .sockets { display: flex; flex-direction: column; } .socket { composes: flexRow from '../dim-ui/common.m.scss'; align-items: flex-start; border-bottom: 1px solid #333; padding: 4px 8px; flex-wrap: wrap; @include interactive($hover: true) { background-color: rgb(255, 255, 255, 0.05); } &:first-child { padding-right: 50px; } &:last-child { border-bottom: none; } } .plug { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; margin-right: 0.7em; cursor: pointer; &:last-child { margin-right: 0; } h2 { font-size: 12px; line-height: 12px; margin: 0; } } .perkIconWrapper { // Change the size of the perk icons --item-size-adjusted: calc(#{dim-item-px(30)}); grid-area: icon; align-self: flex-start; position: relative; // to position the thumbs-up correctly display: block; padding: 1px; flex-shrink: 0; > * { --item-size: var(--item-size-adjusted); } } .perkInfo { composes: flexColumn from '../dim-ui/common.m.scss'; align-items: flex-start; margin-left: 6px; white-space: pre-wrap; > div { color: var(--theme-text-secondary); } h3 { color: var(--theme-text); } } .selected { flex: 1; } .disabled { opacity: 0.5; } .plugLabel { margin-left: 1em; } ================================================ FILE: src/app/item-popup/ItemPerksList.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'disabled': string; 'perkIconWrapper': string; 'perkInfo': string; 'plug': string; 'plugLabel': string; 'selected': string; 'socket': string; 'sockets': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemPerksList.tsx ================================================ import { useD2Definitions } from 'app/manifest/selectors'; import { isKillTrackerSocket } from 'app/utils/item-utils'; import { getSocketsByIndexes } from 'app/utils/socket-utils'; import { wishListSelector } from 'app/wishlists/selectors'; import clsx from 'clsx'; import { useState } from 'react'; import { useSelector } from 'react-redux'; import { DimItem, DimPlug, DimSocket, DimSocketCategory } from '../inventory/item-types'; import { InventoryWishListRoll } from '../wishlists/wishlists'; import * as styles from './ItemPerksList.m.scss'; import { PlugClickHandler } from './ItemSockets'; import { PerkCircleWithTooltip } from './Plug'; import { DimPlugTooltip } from './PlugTooltip'; /** * The list-style, vertical display of perks for a weapon. */ export default function ItemPerksList({ item, perks, onClick, }: { item: DimItem; perks: DimSocketCategory; onClick?: PlugClickHandler; }) { const defs = useD2Definitions(); const wishlistRoll = useSelector(wishListSelector(item)); const [selectedPerk, setSelectedPerk] = useState<{ socketIndex: number; perkHash: number }>(); const onPerkSelected = (socket: DimSocket, perk: DimPlug) => { if (selectedPerk?.perkHash === perk.plugDef.hash) { setSelectedPerk(undefined); } else { setSelectedPerk({ socketIndex: socket.socketIndex, perkHash: perk.plugDef.hash }); } onClick?.(item, socket, perk, false); }; if (!perks.socketIndexes || !defs || !item.sockets) { return null; } const socketIndices = perks.socketIndexes.toReversed(); const sockets = getSocketsByIndexes(item.sockets, socketIndices); return (
{sockets.map( (socketInfo) => !isKillTrackerSocket(socketInfo) && ( ), )}
); } function PerkSocket({ item, socket, wishlistRoll, selectedPerk, onPerkSelected, }: { item: DimItem; socket: DimSocket; wishlistRoll?: InventoryWishListRoll; selectedPerk?: { socketIndex: number; perkHash: number }; onPerkSelected: (socketInfo: DimSocket, plug: DimPlug) => void; }) { if (!socket.plugOptions.length) { return null; } return (
{socket.plugOptions.map((plug) => ( ))}
); } function PerkPlug({ item, plug, socketInfo, wishlistRoll, selectedSocket, selectedPerk, onPerkSelected, }: { item: DimItem; plug: DimPlug; socketInfo: DimSocket; wishlistRoll?: InventoryWishListRoll; /* True, false, or undefined for "no selection" */ // TODO: maybe use an enum selectedSocket: boolean; selectedPerk: boolean; onPerkSelected: (socketInfo: DimSocket, plug: DimPlug) => void; }) { if (!plug.plugDef.plug) { return null; } const perkSelected = () => onPerkSelected(socketInfo, plug); const selected = plug === socketInfo.plugged; return (
{selectedPerk ? (
) : ( selected && !selectedSocket && (

{plug.plugDef.displayProperties.name}

) )}
); } ================================================ FILE: src/app/item-popup/ItemPopup.m.scss ================================================ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Move Popup // // The popup displaying info and actions for an single item. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @use '../variables.scss' as *; .arrow { width: $theme-tooltip-arrow-size; height: $theme-tooltip-arrow-size; border-style: solid; position: absolute; border-width: $theme-tooltip-arrow-size; border-color: transparent; } .desktopPopupRoot { pointer-events: none; } .movePopupDialog { --background-color: rgb(0, 0, 0, 1); // Fallback background &.exotic { --background-color: rgb(22, 18, 4, 1); } &.legendary { --background-color: rgb(14, 8, 17, 1); } &.rare { --background-color: rgb(10, 15, 21, 1); } &.uncommon { --background-color: rgb(8, 17, 9, 1); } &.common { --background-color: rgb(18, 18, 18, 1); } &[data-popper-placement^='top'] .arrow { width: 0; height: 0; border-bottom-width: 0; border-top-color: var(--theme-item-popup-arrow); bottom: calc(-1 * $theme-tooltip-arrow-size); } &[data-popper-placement^='bottom'] .arrow { width: 0; height: 0; border-top-width: 0; border-bottom-color: var(--theme-item-popup-arrow); top: calc(-1 * $theme-tooltip-arrow-size); &.exotic { border-bottom-color: $exotic; } &.legendary { border-bottom-color: $legendary; } &.rare { border-bottom-color: $rare; } &.uncommon { border-bottom-color: $uncommon; } &.common { border-bottom-color: $common; } } &[data-popper-placement^='right'] .arrow { width: 0; height: 0; border-left-width: 0; border-right-color: var(--theme-item-popup-arrow); left: calc(-1 * $theme-tooltip-arrow-size); } &[data-popper-placement^='left'] .arrow { width: 0; height: 0; border-right-width: 0; border-left-color: var(--theme-item-popup-arrow); right: calc(-1 * $theme-tooltip-arrow-size); } textarea { resize: vertical; } } .popupBackground { background-color: var(--background-color); contain: content; @include desktop { box-shadow: 0 0 0 1px var(--theme-item-popup-border), var(--theme-drop-shadow); } } .desktopPopup { display: flex; flex-direction: row; .movePopupDialog[data-popper-placement^='right'] & { flex-direction: row-reverse; } } .desktopPopupBody { width: 320px; display: block; pointer-events: auto; } .desktopActions { display: block; > div { pointer-events: auto; } } .mobileItemActions { padding: 5px 0; display: flex; justify-content: space-between; background: #111; border-bottom: 1px solid #333; border-top: 1px solid #333; &:empty { display: none; } } .mobileMoveLocations { display: flex; justify-content: space-between; flex-wrap: wrap; } .sheetHeader { padding: 0; border: none; } .sheetClose { padding: 10px; color: black; opacity: 0.7; .uncommon &, .rare &, .legendary & { color: var(--theme-text); } } .failureReason { color: var(--theme-text); background-color: #923c3c; margin: 0; padding: 2px 8px; } .header { display: flex; flex-flow: column nowrap; } ================================================ FILE: src/app/item-popup/ItemPopup.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'arrow': string; 'common': string; 'desktopActions': string; 'desktopPopup': string; 'desktopPopupBody': string; 'desktopPopupRoot': string; 'exotic': string; 'failureReason': string; 'header': string; 'legendary': string; 'mobileItemActions': string; 'mobileMoveLocations': string; 'movePopupDialog': string; 'popupBackground': string; 'rare': string; 'sheetClose': string; 'sheetHeader': string; 'uncommon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemPopup.tsx ================================================ import { AlertIcon } from 'app/dim-ui/AlertIcon'; import ClickOutside from 'app/dim-ui/ClickOutside'; import { PressTipRoot } from 'app/dim-ui/PressTip'; import Sheet from 'app/dim-ui/Sheet'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { usePopper } from 'app/dim-ui/usePopper'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { sortedStoresSelector } from 'app/inventory/selectors'; import ItemAccessoryButtons from 'app/item-actions/ItemAccessoryButtons'; import ItemMoveLocations from 'app/item-actions/ItemMoveLocations'; import type { ItemRarityName } from 'app/search/d2-known-values'; import { useIsPhonePortrait } from 'app/shell/selectors'; import OpenOnStreamDeckButton from 'app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton'; import { streamDeckEnabledSelector } from 'app/stream-deck/selectors'; import { nonPullablePostmasterItem } from 'app/utils/item-utils'; import { Portal } from 'app/utils/temp-container'; import clsx from 'clsx'; import { useMemo, useRef } from 'react'; import { useSelector } from 'react-redux'; import DesktopItemActions, { menuClassName } from './DesktopItemActions'; import * as styles from './ItemPopup.m.scss'; import ItemPopupHeader from './ItemPopupHeader'; import { useItemPopupTabs } from './ItemPopupTabs'; import ItemTagHotkeys from './ItemTagHotkeys'; import { ItemPopupExtraInfo } from './item-popup'; import { buildItemActionsModel } from './item-popup-actions'; const rarityClasses: Record = { Exotic: styles.exotic, Legendary: styles.legendary, Rare: styles.rare, Uncommon: styles.uncommon, Common: styles.common, Unknown: '', Currency: '', } as const; /** * The item inspection popup, which is either a popup on desktop or a sheet on mobile. */ export default function ItemPopup({ item, element, extraInfo, boundarySelector, zIndex, noLink, onClose, }: { item: DimItem; element?: HTMLElement; extraInfo?: ItemPopupExtraInfo; boundarySelector?: string; zIndex?: number; /** Don't allow opening Armory from the header link */ noLink?: boolean; onClose: () => void; }) { const { content, tabButtons } = useItemPopupTabs(item, extraInfo); const stores = useSelector(sortedStoresSelector); const isPhonePortrait = useIsPhonePortrait(); const popupRef = useRef(null); usePopper({ placement: 'right', contents: popupRef, reference: { current: element || null }, boundarySelector, arrowClassName: styles.arrow, menuClassName: menuClassName, }); // TODO: we need this to fire after popper repositions the popup. Maybe try again when we switch to floatingui. // useFocusFirstFocusableElement(popupRef); const itemActionsModel = useMemo( () => item && buildItemActionsModel(item, stores), [item, stores], ); const streamDeckEnabled = $featureFlags.elgatoStreamDeck ? // eslint-disable-next-line react-hooks/rules-of-hooks useSelector(streamDeckEnabledSelector) : false; const failureStrings = Array.from(new Set(extraInfo?.failureStrings ?? [])); const header = (
{failureStrings?.map( (failureString) => failureString.length > 0 && (
), )} {nonPullablePostmasterItem(item) && (
{t('MovePopup.CantPullFromPostmaster')}
)} {isPhonePortrait && itemActionsModel.hasAccessoryControls && (
)} {tabButtons}
); return isPhonePortrait ? (
) } >
{content}
) : (
{header} {content} {streamDeckEnabled && item.bucket.inInventory && ( )}
{itemActionsModel.hasControls && (
)}
); } ================================================ FILE: src/app/item-popup/ItemPopupBody.scss ================================================ @use 'sass:color'; @use '../variables.scss' as *; /** * Move Popup - Details */ @layer base { .item-details { // tie icon size to item tile size, default value still the same --set-bonus-icon-size: calc(var(--item-size) * 0.4); margin: 10px; box-sizing: border-box; &.warning { background: color.scale($red, $lightness: -90%); border-top: 4px solid $red; padding: 8px; white-space: normal; max-width: fit-content; a { color: var(--theme-text); font-size: 12px; } } } .masterwork-progress { margin: 4px 0; padding: 4px 10px 4px; background: var(--theme-item-popup-panel-bg); span { color: var(--theme-accent-primary); font-weight: bold; } img { width: 16px; height: 16px; margin-bottom: -4px; } } .crafted-progress { --objective-progress-color: #{$deepsight-border-color}; --objective-background-color: var(--theme-item-popup-progress-bar-bg); padding: 4px 10px 4px; background: var(--theme-item-popup-panel-bg); gap: 4px; display: flex; align-items: center; } } ================================================ FILE: src/app/item-popup/ItemPopupContainer.tsx ================================================ import { createItemContextSelector, sortedStoresSelector } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { applySocketOverrides } from 'app/inventory/store/override-sockets'; import { useD2Definitions } from 'app/manifest/selectors'; import { lazy, Suspense, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import { useSubscription } from 'use-subscription'; import { DimItem } from '../inventory/item-types'; import { hideItemPopup, showItemPopup$ } from './item-popup'; const ItemPopup = lazy(() => import(/* webpackChunkName: "item-popup-armory" */ './ItemPopup')); interface Props { boundarySelector?: string; } /** * A container that can show a single item popup/tooltip. This is a * single element to help prevent multiple popups from showing at once. */ export default function ItemPopupContainer({ boundarySelector }: Props) { const stores = useSelector(sortedStoresSelector); const defs = useD2Definitions(); const itemCreationContext = useSelector(createItemContextSelector); const currentItem = useSubscription(showItemPopup$); const onClose = () => hideItemPopup(); const { pathname } = useLocation(); useEffect(() => { onClose(); }, [pathname]); // Try to find an updated version of the item! let item = currentItem?.item && maybeFindItem(currentItem.item, stores); // Apply socket overrides to customize the item (e.g. from a loadout) if (item && defs && currentItem?.extraInfo?.socketOverrides) { item = applySocketOverrides(itemCreationContext, item, currentItem.extraInfo.socketOverrides); } if (!currentItem || !item) { return null; } return ( ); } /** * The passed in item may be old - look through stores to try and find a newer version! * This helps with items that have objectives, like Pursuits. * * TODO: This doesn't work for the synthetic items created for Milestones. */ function maybeFindItem(item: DimItem, stores: DimStore[]) { // Don't worry about non-instanced items if (!item.instanced) { return item; } for (const store of stores) { for (const storeItem of store.items) { if (storeItem.id === item.id) { return storeItem; } } } // Didn't find it, use what we've got. return item; } ================================================ FILE: src/app/item-popup/ItemPopupHeader.m.scss ================================================ @use '../variables.scss' as *; @use 'sass:math'; $iconOverlayScale: math.div(65, 96); .header { composes: resetButton from 'app/dim-ui/common.m.scss'; display: block; text-align: left; width: 100%; font-size: 15px; line-height: 17px; letter-spacing: -0.02em; padding: 10px; background-color: #555; color: #eee; &:has(.iconOverlay) { padding-left: 5px + 28px * $iconOverlayScale; } } .armory { @include interactive($hover: true, $focus: true) { outline: none; cursor: pointer; h1 { text-decoration: underline; } } } .title { font-size: 21px !important; line-height: 24px; text-decoration: none; margin: 0 !important; padding: 0; @include destiny-header; // Prevent the title running into the sheet close button @include phone-portrait { margin: 0 32px 0 0 !important; } } .subtitle { display: flex; justify-content: space-between; margin-top: 4px; } .type, .details { display: flex; align-self: flex-end; align-items: center; } .type { flex: 1; } .elementIcon { height: 16px; width: 16px; } .rare, .common, .exotic { .elementIcon { filter: drop-shadow(0 0 1px #222) drop-shadow(0 0 0 #222); } } .itemType { opacity: 0.7; } .power { margin: 0 4px 0 2px; } .masterwork { background-image: url('../../images/masterworkHeader.png'); background-repeat: repeat-x; background-size: cover; background-position: top center; &.exotic { background-image: url('../../images/exoticMasterworkHeader.png'); } } .common { background-color: $common; } .common, .exotic { color: #222; /* stylelint-disable-next-line no-descending-specificity */ a { color: #222; } } .uncommon { background-color: $uncommon; } .rare { background-color: $rare; } .pursuit { background-color: #333; color: #eee; /* stylelint-disable-next-line no-descending-specificity */ a { color: #eee; } } .legendary { background-color: $legendary; } .exotic { background-color: $exotic; .itemType { opacity: 0.9; } } .iconOverlay { composes: flexColumn from '../dim-ui/common.m.scss'; position: absolute; box-sizing: border-box; align-items: center; top: 0; left: 0; width: math.round(28px * $iconOverlayScale); height: math.round(96px * $iconOverlayScale); background-size: cover; background-repeat: no-repeat; pointer-events: none; padding-top: math.round(3px * $iconOverlayScale); > img { width: math.round(22px * $iconOverlayScale); } } ================================================ FILE: src/app/item-popup/ItemPopupHeader.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'armory': string; 'common': string; 'details': string; 'elementIcon': string; 'exotic': string; 'header': string; 'iconOverlay': string; 'itemType': string; 'legendary': string; 'masterwork': string; 'power': string; 'pursuit': string; 'rare': string; 'subtitle': string; 'title': string; 'type': string; 'uncommon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemPopupHeader.tsx ================================================ import ArmorySheet from 'app/armory/ArmorySheet'; import { itemConstants } from 'app/destiny2/d2-definitions'; import BungieImage, { bungieBackgroundStyles } from 'app/dim-ui/BungieImage'; import ElementIcon from 'app/dim-ui/ElementIcon'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { useHotkey } from 'app/hotkeys/useHotkey'; import { t } from 'app/i18next-t'; import type { ItemRarityName } from 'app/search/d2-known-values'; import { compact } from 'app/utils/collections'; import { itemTypeName } from 'app/utils/item-utils'; import { LookupTable } from 'app/utils/util-types'; import clsx from 'clsx'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import { useState } from 'react'; import { DimItem } from '../inventory/item-types'; import { AmmoIcon } from './AmmoIcon'; import BreakerType from './BreakerType'; import * as styles from './ItemPopupHeader.m.scss'; const rarityClassName: LookupTable = { Common: styles.common, Uncommon: styles.uncommon, Rare: styles.rare, Legendary: styles.legendary, Exotic: styles.exotic, }; export default function ItemPopupHeader({ item, noLink, }: { item: DimItem; /** Don't allow opening Armory from the header link */ noLink?: boolean; }) { const [showArmory, setShowArmory] = useState(false); useHotkey('a', t('Hotkey.Armory'), () => setShowArmory(true)); const showElementIcon = Boolean(item.element); const linkToArmory = !noLink && item.destinyVersion === 2; return ( ); } function SeasonTierBanner({ item }: { item: DimItem }) { if (!item.iconDef || !itemConstants) { return null; } const seasonIcon = item.iconDef.secondaryBackground; const backgrounds = compact([ // Featured flags item.featured ? itemConstants.featuredItemFlagPath : undefined, // Tier pips item.tier > 0 && itemConstants.gearTierOverlayImagePaths[Math.min(item.tier - 1, 4)], // Black stripe item.iconDef.secondaryBackground && itemConstants.watermarkDropShadowPath, ]); if (!seasonIcon && backgrounds.length === 0) { return null; } return (
{seasonIcon && }
); } ================================================ FILE: src/app/item-popup/ItemPopupTabs.m.scss ================================================ @use '../variables.scss' as *; .movePopupTabs { display: flex; justify-content: space-around; } .movePopupTab { composes: resetButton from '../dim-ui/common.m.scss'; color: hsl(0, 0%, 80%); padding: 5px 0 3px; cursor: pointer; width: 100%; text-align: center; border-bottom: 2px solid #222; background-color: black; @include interactive($hover: true) { color: var(--theme-text); background-color: #3f3f3f; } @include phone-portrait { font-size: 16px; } &.selected { border-bottom-color: var(--theme-accent-primary); } } ================================================ FILE: src/app/item-popup/ItemPopupTabs.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'movePopupTab': string; 'movePopupTabs': string; 'selected': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemPopupTabs.tsx ================================================ import { ItemPopupTab } from '@destinyitemmanager/dim-api-types'; import { useHotkey } from 'app/hotkeys/useHotkey'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { ItemTriage, TriageTabToggle, doShowTriage } from 'app/item-triage/ItemTriage'; import { useSetting } from 'app/settings/hooks'; import clsx from 'clsx'; import { JSX, useCallback, useId, useRef } from 'react'; import ItemDetails from './ItemDetails'; import * as styles from './ItemPopupTabs.m.scss'; import { ItemPopupExtraInfo } from './item-popup'; export function useItemPopupTabs(item: DimItem, extraInfo: ItemPopupExtraInfo | undefined) { const [tab, setTab] = useSetting('itemPopupTab'); const id = useId(); const focusedTab = useRef(undefined); const detailsId = `${id}-details`; const triageId = `${id}-triage`; const tabs: { tab: ItemPopupTab; id: string; title: JSX.Element | string; component: JSX.Element; }[] = [ { tab: ItemPopupTab.Overview, title: t('MovePopup.OverviewTab'), id: detailsId, component: , }, ]; if ($featureFlags.triage && doShowTriage(item)) { tabs.push({ tab: ItemPopupTab.Triage, title: , id: triageId, component: , }); } // https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ // The keyboard handling code would need to be modified if we ever have more than two tabs const toggleTab = useCallback( (_e: Event | React.UIEvent, fromKeyboard = false) => { const newTab = tab === ItemPopupTab.Overview ? ItemPopupTab.Triage : ItemPopupTab.Overview; if (fromKeyboard) { focusedTab.current = newTab; } setTab(newTab); }, [setTab, tab], ); // When toggling via arrow keys, move the focus to the new tab // TODO: try this again when we switch to floating UI - otherwise this causes the page to jump up as the popup gets repositioned // useEffect(() => { // if (focusedTab.current !== undefined) { // const tabId = focusedTab.current === ItemPopupTab.Overview ? detailsId : triageId; // if (tabId) { // const element = document.getElementById(`${tabId}-tab`); // element?.focus(); // focusedTab.current = undefined; // } // } // // no dependency array - we want to run this every render // }); const handleKeyDown = (event: React.KeyboardEvent) => { if (event.repeat) { return; } switch (event.key) { case 'ArrowLeft': case 'ArrowRight': case 'Home': case 'End': toggleTab(event, true); event.stopPropagation(); event.preventDefault(); break; default: break; } }; useHotkey('t', t('Hotkey.ItemPopupTab'), toggleTab); const content = (tabs.length > 1 ? tabs.find((t) => t.tab === tab)! : tabs[0]).component; const tabButtons = tabs.length > 1 ? (
{tabs.map((ta) => ( ))}
) : undefined; return { content, tabButtons, } as const; } ================================================ FILE: src/app/item-popup/ItemSockets.m.scss ================================================ @use '../variables.scss' as *; // Horizontal list of sockets .itemSockets { composes: flexRow from '../dim-ui/common.m.scss'; gap: 4px; } ================================================ FILE: src/app/item-popup/ItemSockets.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'itemSockets': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemSockets.tsx ================================================ import clsx from 'clsx'; import { memo, useState } from 'react'; import { DimItem, DimPlug, DimSocket } from '../inventory/item-types'; import * as styles from './ItemSockets.m.scss'; import ItemSocketsGeneral from './ItemSocketsGeneral'; import ItemSocketsWeapons from './ItemSocketsWeapons'; import SocketDetails from './SocketDetails'; export type PlugClickHandler = ( item: DimItem, socket: DimSocket, plug: DimPlug, hasMenu: boolean, ) => void; export default memo(function ItemSockets({ item, minimal, grid, onPlugClicked, }: { item: DimItem; /** minimal style used for compare */ minimal?: boolean; /** Force grid style */ grid?: boolean; onPlugClicked?: (value: { item: DimItem; socket: DimSocket; plugHash: number }) => void; }) { const [socketInMenu, setSocketInMenu] = useState(null); const handlePlugClick: PlugClickHandler = (item, socket, plug, hasMenu) => { if (hasMenu) { setSocketInMenu(socket); } else { onPlugClicked?.({ item, socket, plugHash: plug.plugDef.hash, }); } }; const content = item.destinyVersion === 2 && item.bucket.inWeapons ? ( ) : ( ); return ( <> {content} {socketInMenu && ( setSocketInMenu(null)} onPlugSelected={onPlugClicked} /> )} ); }); export function ItemSocketsList({ children, className, }: { children: React.ReactNode; className?: string; }) { return
{children}
; } ================================================ FILE: src/app/item-popup/ItemSocketsGeneral.m.scss ================================================ @use '../variables' as *; .armorIntrinsicDescription { color: var(--theme-text-secondary); white-space: pre-line; h3 { color: var(--theme-text); } } .clarityDescription { margin-top: 2px; border-left: 2px solid $communityBlue; padding-left: 6px; } .minimalSockets { flex-flow: column nowrap; } .generalSockets { margin: 10px; display: flex; flex-flow: row wrap; gap: 4px 16px; &.minimalSockets { margin: 0; } } // Uppercased title for socket categories .socketCategoryHeader { text-transform: uppercase; margin-bottom: 2px; } ================================================ FILE: src/app/item-popup/ItemSocketsGeneral.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'armorIntrinsicDescription': string; 'clarityDescription': string; 'generalSockets': string; 'minimalSockets': string; 'socketCategoryHeader': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemSocketsGeneral.tsx ================================================ import ClarityDescriptions from 'app/clarity/descriptions/ClarityDescriptions'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { singleStoreSelector } from 'app/inventory/selectors'; import { useD2Definitions } from 'app/manifest/selectors'; import { filterMap, uniqBy } from 'app/utils/collections'; import { usePlugDescriptions } from 'app/utils/plug-descriptions'; import { getArmorArchetypeSocket, getExtraIntrinsicPerkSockets, getGeneralSockets, } from 'app/utils/socket-utils'; import clsx from 'clsx'; import { SocketCategoryHashes } from 'data/d2/generated-enums'; import { useSelector } from 'react-redux'; import { DimItem, DimSocket } from '../inventory/item-types'; import { wishListSelector } from '../wishlists/selectors'; import ArchetypeSocket, { ArchetypeRow } from './ArchetypeSocket'; import EmoteSockets from './EmoteSockets'; import { ItemSocketsList, PlugClickHandler } from './ItemSockets'; import * as styles from './ItemSocketsGeneral.m.scss'; import { SetBonus } from './SetBonus'; import Socket from './Socket'; export default function ItemSocketsGeneral({ item, minimal, onPlugClicked, }: { item: DimItem; /** minimal style used for compare */ minimal?: boolean; onPlugClicked: PlugClickHandler; }) { const defs = useD2Definitions(); const wishlistRoll = useSelector(wishListSelector(item)); const store = useSelector(singleStoreSelector(item.owner)); if (!item.sockets || !defs) { return null; } const { intrinsicSocket, modSocketsByCategory } = getGeneralSockets(item)!; const emoteWheelCategory = item.sockets.categories.find( (c) => c.category.hash === SocketCategoryHashes.Emotes, ); // exotic class armor intrinsics const extraIntrinsicSockets = getExtraIntrinsicPerkSockets(item); const archetypeSocket = getArmorArchetypeSocket(item); if (archetypeSocket) { extraIntrinsicSockets.push(archetypeSocket); } const extraIntrinsicSocketIndices = extraIntrinsicSockets.map((s) => s.socketIndex); // Only show the first of each style of category when minimal const modSocketCategories = ( minimal && item.bucket.inArmor ? uniqBy(modSocketsByCategory.entries(), ([category]) => category.category.categoryStyle) : [...modSocketsByCategory.entries()] ) .map( ([category, sockets]) => [ category, sockets.filter((s) => !extraIntrinsicSocketIndices.includes(s.socketIndex)), ] as const, ) .filter(([, sockets]) => sockets.length > 0); const intrinsicRows = !minimal && filterMap( [intrinsicSocket, ...extraIntrinsicSockets], (s) => s && ( ), ); return ( <> {intrinsicRows} {!minimal && item.setBonus && (
)}
{emoteWheelCategory && ( item.sockets!.allSockets[s])} onClick={onPlugClicked} /> )} {modSocketCategories.map(([category, sockets]) => (
{!minimal && ( {category.category.displayProperties.name} )} {sockets.map((socketInfo) => ( ))}
))}
); } function IntrinsicArmorPerk({ item, socket, onPlugClicked, }: { item: DimItem; socket: DimSocket; onPlugClicked: PlugClickHandler; }) { const plugDescriptions = usePlugDescriptions(socket.plugged?.plugDef); return (
{plugDescriptions.perks.map( (perkDesc) => perkDesc.description && ( ), )} {plugDescriptions.communityInsight && ( )}
); } export function SocketCategoryHeader({ children }: { children: React.ReactNode }) { return
{children}
; } ================================================ FILE: src/app/item-popup/ItemSocketsWeapons.m.scss ================================================ @use '../variables.scss' as *; .archetype { margin-bottom: 4px; } .perks { position: relative; // to help contain the displayStyleButton } .stats { color: var(--theme-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .displayStyleButton { composes: resetButton from '../dim-ui/common.m.scss'; position: absolute; right: 10px; top: 8px; border-radius: 50%; padding: 4px; width: 24px; height: 24px; z-index: 1; background-color: rgb(255, 255, 255, 0.2); color: var(--theme-text); text-shadow: 1px 1px 3px rgb(0, 0, 0, 0.25); @include interactive($hover: true, $active: true) { background-color: var(--theme-accent-primary) !important; color: var(--theme-text-invert) !important; } } // For when perks are displayed in a grid .grid { composes: flexRow from '../dim-ui/common.m.scss'; // in the item popup, when the archetype row is shown, inset the perks &:nth-child(n + 2) { padding: 8px 10px; } > div { padding-right: 2px; padding-left: 2px; &:first-child { padding-left: 0; } } &.gridLines > div { border-right: 0.5px solid #444; padding-right: 5px; padding-left: 5px; &:last-child { border-right: none; } &:first-child { padding-left: 0; } } } ================================================ FILE: src/app/item-popup/ItemSocketsWeapons.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'archetype': string; 'displayStyleButton': string; 'grid': string; 'gridLines': string; 'perks': string; 'stats': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemSocketsWeapons.tsx ================================================ import { t } from 'app/i18next-t'; import { statsMs } from 'app/inventory/store/stats'; import { useD2Definitions } from 'app/manifest/selectors'; import { useSetting } from 'app/settings/hooks'; import { AppIcon, faGrid, faList } from 'app/shell/icons'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { isKillTrackerSocket } from 'app/utils/item-utils'; import { getSocketsByIndexes, getWeaponSockets } from 'app/utils/socket-utils'; import { LookupTable } from 'app/utils/util-types'; import clsx from 'clsx'; import { ItemCategoryHashes, StatHashes } from 'data/d2/generated-enums'; import { useState } from 'react'; import { useSelector } from 'react-redux'; import { DimItem, DimSocket } from '../inventory/item-types'; import { wishListSelector } from '../wishlists/selectors'; import ArchetypeSocket, { ArchetypeRow } from './ArchetypeSocket'; import ItemPerksList from './ItemPerksList'; import { ItemSocketsList, PlugClickHandler } from './ItemSockets'; import * as styles from './ItemSocketsWeapons.m.scss'; import Socket from './Socket'; import SocketDetails from './SocketDetails'; export default function ItemSocketsWeapons({ item, minimal, grid: forceGrid, onPlugClicked, }: { item: DimItem; /** minimal style used for compare. suppresses the archetype/mods row */ minimal?: boolean; /** Force grid style */ grid?: boolean; onPlugClicked: PlugClickHandler; }) { const defs = useD2Definitions(); const wishlistRoll = useSelector(wishListSelector(item)); const isPhonePortrait = useIsPhonePortrait(); const [listPerksSetting, setListPerks] = useSetting( isPhonePortrait ? 'perkList' : 'perkListDesktop', ); const listPerks = forceGrid === undefined ? listPerksSetting : !forceGrid; if (!item.sockets || !defs) { return null; } // Separate out perks from sockets. const { intrinsicSocket, perks, modSocketsByCategory } = getWeaponSockets(item, { includeFakeMasterwork: Boolean(item.crafted), })!; // Improve this when we use iterator-helpers const mods = [...modSocketsByCategory.values()].flat(); const keyStats = item.stats && !item.itemCategoryHashes.includes(ItemCategoryHashes.Sword) && !item.itemCategoryHashes.includes(ItemCategoryHashes.LinearFusionRifles) && item.stats .slice(0, 2) .filter((s) => !statsMs.includes(s.statHash) && s.statHash !== StatHashes.BlastRadius); // Some stat labels are long. This lets us replace them with i18n const statLabels: LookupTable = { [StatHashes.RoundsPerMinute]: t('Organizer.Stats.RPM'), }; const renderSocket = (socketInfo: DimSocket) => ( ); const renderSocketWithoutEmpties = (socketInfo: DimSocket) => { // Prevent empty mods from showing on craftable weapons const socketWithoutEmpties = { ...socketInfo, plugOptions: socketInfo.plugOptions.filter( (option) => option.plugDef.plug.plugCategoryIdentifier !== 'crafting.recipes.empty_socket', ), }; return !isKillTrackerSocket(socketWithoutEmpties) && renderSocket(socketWithoutEmpties); }; return ( <> {!minimal && (intrinsicSocket?.plugged || mods.length > 0) && ( {intrinsicSocket?.plugged && ( {keyStats && keyStats.length > 0 && (
{keyStats ?.map( (s) => `${s.value} ${( statLabels[s.statHash as StatHashes] || s.displayProperties.name ).toLowerCase()}`, ) ?.join(' / ')}
)}
)} {mods.length > 0 && {mods.map(renderSocket)}}
)} {perks && (listPerks ? (
{!forceGrid && ( )}
) : (
{!forceGrid && ( )} {getSocketsByIndexes(item.sockets, perks.socketIndexes).map((socketInfo) => renderSocketWithoutEmpties(socketInfo), )}
))} ); } // TODO: just pass in sockets? export function ItemModSockets({ item, onPlugClicked, }: { item: DimItem; onPlugClicked?: (value: { item: DimItem; socket: DimSocket; plugHash: number }) => void; }) { const defs = useD2Definitions(); const wishlistRoll = useSelector(wishListSelector(item)); const [socketInMenu, setSocketInMenu] = useState(null); if (!item.sockets || !defs) { return null; } // Separate out perks from sockets. const { modSocketsByCategory } = getWeaponSockets(item, { includeFakeMasterwork: Boolean(item.crafted), })!; // Improve this when we use iterator-helpers const mods = [...modSocketsByCategory.values()].flat(); const handlePlugClick: PlugClickHandler = (item, socket, plug, hasMenu) => { if (hasMenu) { setSocketInMenu(socket); } else { onPlugClicked?.({ item, socket, plugHash: plug.plugDef.hash, }); } }; const renderSocket = (socketInfo: DimSocket) => ( ); return ( <> {mods.length > 0 && {mods.map(renderSocket)}}{' '} {socketInMenu && ( setSocketInMenu(null)} onPlugSelected={onPlugClicked} /> )} ); } ================================================ FILE: src/app/item-popup/ItemStat.m.scss ================================================ @use 'sass:color'; @use '../variables.scss' as *; .statName { grid-column: 1; text-align: right; margin-right: 8px; max-width: 130px; text-overflow: ellipsis; overflow-x: clip; @include phone-portrait { max-width: 40vw; } } .tunableSymbol { position: relative; } // The numeric value of the stat .value { grid-column: 2; text-align: right !important; font-variant-numeric: tabular-nums; white-space: nowrap; > svg { vertical-align: middle; margin-left: 4px; } } .icon { margin-left: 4px; img { height: 12px; width: 12px; vertical-align: bottom; filter: drop-shadow(0 0 0 white); } } // Stat bars .statBar { margin-left: 8px; grid-column: -2 / -1; } .barContainer { background-color: #333; display: flex; width: 100%; height: 100%; :global(.armory) & { background: rgb(0, 0, 0, 0.5); backdrop-filter: blur(6px); } } .statBarSegment { order: 1; display: block; height: 100%; float: left; line-height: 20px; background-color: #555; background-color: white; color: black; transition: width 150ms ease-in-out; &.negative { background-color: #7a2727; order: 5; // Push negative segments to the right side of the bar } // An assumption: never more than 4 part effects in a single stat? // 3 have been observed on weapons with a barrel + mag + grip. &.parts { order: 2; opacity: 0.88; & + &.parts { opacity: 0.76; & + &.parts { opacity: 0.64; & + &.parts { opacity: 0.52; } } } } &.mod { order: 3; background-color: $stat-modded; & + &.mod { opacity: 0.8; & + &.mod { opacity: 0.6; } } } &.trait { order: 3; background-color: #4887ba; & + &.trait { opacity: 0.8; & + &.trait { opacity: 0.6; } } } // Colors for the stat bars &.masterwork { order: 4; background-color: $stat-masterworked; } } .statBarTooltip { display: grid; grid-template-columns: auto auto; column-gap: 4px; & > :nth-child(2n-1) { text-align: right; } .mod { color: $stat-modded; } .trait { color: #4887ba; } .masterwork { color: $stat-masterworked; } .negative { color: $red; order: 1; // Push negative rows to the end of the addition list } .base { opacity: 1; } .parts { opacity: 0.8; } .tooltipNetStat { order: 2; // Keep this after the negative rows display: grid; grid-template-columns: subgrid; grid-column: span 2; span:nth-child(2) { text-align: left; } } .tooltipTotalRow { // Addition-bar styling margin-top: 2px; padding-top: 1px; border-top: 1px solid #fff; font-weight: bold; } } .qualitySummary { grid-column: 2 / -1; a { margin-left: 4px; } } .quality { display: inline; margin-left: 4px; } .masterworked { color: $stat-masterworked; } .negativeModded { color: $red; } .archetypeStat { font-weight: bold; } .totalRow { padding-top: 4px; } .totalRow.value { border-top: 1px solid white; padding-top: 3px; } // Total stat breakdown .totalStatDetailed { margin-left: 8px; grid-column: -2 / -1; } .totalStatModded { color: $stat-modded; } .totalStatNegativeModded { color: $red; } .totalStatMasterwork { color: $stat-masterworked; } .customTotal { color: #bbb; } .nonDimmedStatIcons { color: #bbb; :global(.stat-icon) { filter: none; } } .smallStatToggle { grid-column: -3 / -1; margin-left: 4px; img { height: 12px !important; width: 12px !important; vertical-align: bottom; } } ================================================ FILE: src/app/item-popup/ItemStat.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'archetypeStat': string; 'barContainer': string; 'base': string; 'customTotal': string; 'icon': string; 'masterwork': string; 'masterworked': string; 'mod': string; 'negative': string; 'negativeModded': string; 'nonDimmedStatIcons': string; 'parts': string; 'quality': string; 'qualitySummary': string; 'smallStatToggle': string; 'statBar': string; 'statBarSegment': string; 'statBarTooltip': string; 'statName': string; 'tooltipNetStat': string; 'tooltipTotalRow': string; 'totalRow': string; 'totalStatDetailed': string; 'totalStatMasterwork': string; 'totalStatModded': string; 'totalStatNegativeModded': string; 'trait': string; 'tunableSymbol': string; 'value': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/item-popup/ItemStat.tsx ================================================ import { customStatsSelector, settingSelector } from 'app/dim-api/selectors'; import AnimatedNumber from 'app/dim-ui/AnimatedNumber'; import BungieImage from 'app/dim-ui/BungieImage'; import { CustomStatWeightsFromHash } from 'app/dim-ui/CustomStatWeights'; import ExternalLink from 'app/dim-ui/ExternalLink'; import { PressTip } from 'app/dim-ui/PressTip'; import { I18nKey, t, tl } from 'app/i18next-t'; import { D1Item, D1Stat, DimItem, DimSocket, DimStat } from 'app/inventory/item-types'; import { statsMs } from 'app/inventory/store/stats'; import { TOTAL_STAT_HASH, armorStats, statfulOrnaments } from 'app/search/d2-known-values'; import { getD1QualityColor, percent } from 'app/shell/formatters'; import { AppIcon, helpIcon, tunedStatIcon } from 'app/shell/icons'; import { userGuideUrl } from 'app/shell/links'; import { sumBy } from 'app/utils/collections'; import { compareBy, reverseComparator } from 'app/utils/comparators'; import { LookupTable } from 'app/utils/util-types'; import clsx from 'clsx'; import { ItemCategoryHashes, StatHashes } from 'data/d2/generated-enums'; import { clamp } from 'es-toolkit'; import React from 'react'; import { useSelector } from 'react-redux'; import { getSocketsByType, getWeaponComponentSockets, socketContainsIntrinsicPlug, } from '../utils/socket-utils'; import * as styles from './ItemStat.m.scss'; import RecoilStat from './RecoilStat'; // used in displaying the modded segments on item stats const modItemCategoryHashes = new Set([ ItemCategoryHashes.WeaponModsDamage, ItemCategoryHashes.ArmorModsGameplay, // armor mods (pre-2.0) ItemCategoryHashes.ArmorMods, // armor 2.0 mods ]); // Some stat labels are long. This lets us replace them with i18n const statLabels: LookupTable = { [StatHashes.RoundsPerMinute]: tl('Organizer.Stats.RPM'), [StatHashes.AirborneEffectiveness]: tl('Organizer.Stats.Airborne'), [StatHashes.AmmoGeneration]: tl('Organizer.Stats.AmmoGeneration'), }; type StatSegmentType = 'base' | 'parts' | 'traits' | 'mod' | 'masterwork'; const statStyles: Record = { base: [styles.base, tl('Organizer.Columns.BaseStats')], parts: [styles.parts, tl('Stats.WeaponPart')], traits: [styles.trait, tl('Organizer.Columns.Traits')], mod: [styles.mod, tl('Loadouts.Mods')], masterwork: [styles.masterwork, tl('Organizer.Columns.MasterworkStat')], }; type StatSegments = [value: number, statSegmentType: StatSegmentType, modName?: string][]; /** * A single stat line. */ export default function ItemStat({ stat, item, itemStatInfo, }: { stat: DimStat; item?: DimItem; itemStatInfo?: { /** Stat hash the item's tuning slot affects */ tunedStatHash?: number; /** Results from getArmor3StatFocus */ statFocus?: StatHashes[]; }; }) { const showQuality = useSelector(settingSelector('itemQuality')); const customStatsList = useSelector(customStatsSelector); const customStatHashes = customStatsList.map((c) => c.statHash); const modEffects = item && getModEffects(item, stat.statHash).sort(reverseComparator(compareBy(([value]) => value))); const modEffectsTotal = modEffects ? sumBy(modEffects, ([value]) => value) : 0; const partEffects = item && getWeaponComponentEffects(item, stat.statHash).sort( reverseComparator(compareBy(([value]) => value)), ); const partEffectsTotal = partEffects ? sumBy(partEffects, ([value]) => value) : 0; const traitEffects = item && getTraitEffects(item, stat.statHash).sort(reverseComparator(compareBy(([value]) => value))); const perkEffectsTotal = traitEffects ? sumBy(traitEffects, ([value]) => value) : 0; const armorMasterworkSockets = item?.sockets?.allSockets.filter((s) => s.plugged?.plugDef.plug.plugCategoryIdentifier.startsWith('v460.plugs.armor.masterworks'), ); const armorMasterworkValue = armorMasterworkSockets && getTotalPlugEffects(armorMasterworkSockets, [stat.statHash]); const masterworkValue = item?.masterworkInfo?.stats?.find((s) => s.hash === stat.statHash)?.value ?? 0; // This bool controls the stat name being gold const isMasterworkedStat = !item?.bucket.inArmor && masterworkValue !== 0; const masterworkDisplayValue = masterworkValue || armorMasterworkValue; let masterworkDisplayWidth = masterworkDisplayValue || 0; // baseBar here is the leftmost segment of the stat bar. // For armor, this is the "roll," the sum of its invisible stat plugs. // For weapons, this is the default base stat in its item definition, before barrels/mags/etc. const baseBar = item?.bucket.inArmor ? // if it's armor, the base bar length should be // the shortest of base or resulting value, but not below 0 Math.max(Math.min(stat.base, stat.value), 0) : // otherwise, for weapons, we just subtract masterwork and // consider the "base" to include selected perks but not mods stat.value - masterworkValue - modEffectsTotal - partEffectsTotal - perkEffectsTotal; const segments: StatSegments = [[baseBar, 'base']]; for (const [effectAmount, modName] of partEffects ?? []) { segments.push([effectAmount, 'parts', modName]); } for (const [effectAmount, modName] of traitEffects ?? []) { segments.push([effectAmount, 'traits', modName]); } for (const [effectAmount, modName] of modEffects ?? []) { segments.push([effectAmount, 'mod', modName]); } if (masterworkDisplayWidth) { // Account for a masterwork being completely counteracted by a mod penalty. // A MW segment cannot be longer than the bar's total. // ie: a +6 base, a +2mw, and a -10 mod, results in 0. MW segment width is 0. if (modEffectsTotal < 0) { masterworkDisplayWidth = clamp(masterworkDisplayWidth, 0, stat.value); } segments.push([masterworkDisplayWidth, 'masterwork']); } // Get the values that contribute to the total stat value const totalDetails = item && stat.statHash === TOTAL_STAT_HASH && breakDownTotalValue(stat.base, item, armorMasterworkSockets || []); const modSign = (stat.value !== stat.base ? modEffectsTotal : 0) * (stat.smallerIsBetter ? -1 : 1); const optionalClasses = { [styles.masterworked]: isMasterworkedStat, [styles.mod]: modSign > 0, [styles.negativeModded]: modSign < 0, [styles.totalRow]: Boolean(totalDetails), [styles.customTotal]: customStatHashes.includes(stat.statHash), [styles.archetypeStat]: itemStatInfo?.statFocus?.[0] === stat.statHash || itemStatInfo?.statFocus?.[1] === stat.statHash, }; return ( <>
{stat.statHash === itemStatInfo?.tunedStatHash && ( )}{' '} {stat.statHash in statLabels ? t(statLabels[stat.statHash as StatHashes]!) : stat.displayProperties.name}
{stat.additive && stat.value >= 0 && '+'}
{item?.destinyVersion === 2 && statsMs.includes(stat.statHash) && (
{t('Stats.Milliseconds')}
)} {stat.displayProperties.hasIcon && (
)} {showQuality && item && isD1Stat(item, stat) && stat.qualityPercentage && stat.qualityPercentage.min !== 0 && (
({stat.qualityPercentage.range})
)} {stat.statHash === StatHashes.RecoilDirection && (
)} {stat.bar && } {totalDetails && Boolean(totalDetails.totalModsValue || totalDetails.totalMasterworkValue) && ( )} {item && customStatHashes.includes(stat.statHash) && ( )} ); } function StatBar({ segments, stat }: { segments: StatSegments; stat: DimStat }) { // Make sure the combined "filled"-colored segments never exceed this. let remainingFilled = stat.value; // Make sure the red bar section never exceeds the blank space, // which would increase the total stat bar width. let remainingEmpty = Math.max(stat.maximumValue - stat.value, 0); return (
} footer={footer} onClose={onClose} freezeInitialHeight={true} >
{sets.map((set) => ( {set.setPerks.map((perk) => { const perkDef = defs.SandboxPerk.get(perk.sandboxPerkHash); return ( {perkDef.displayProperties.name}
{t('Item.SetBonus.NPiece', { count: perk.requiredSetCount })}
} icon={} onClick={handlePerkClick(set.hash, perk.requiredSetCount)} > {perkDef.displayProperties.description}
); })}
))}
); } function Footer({ setBonuses, onSubmit, }: { setBonuses: SetBonusCounts; onSubmit: (event: React.FormEvent | KeyboardEvent) => void; }) { const acceptButtonText = t('LB.SelectSetBonus'); useHotkey('enter', acceptButtonText, onSubmit); const isPhonePortrait = useIsPhonePortrait(); return (
); } function SetBonusDisplay({ setBonuses }: { setBonuses: SetBonusCounts }) { const defs = useD2Definitions()!; return Object.keys(setBonuses).map((setHash) => { const setDef = defs.EquipableItemSet.get(Number(setHash)); return ( setDef && !setDef.redacted && ( ) ); }); } ================================================ FILE: src/app/loadout-builder/filter/LockedItem.tsx ================================================ import ClosableContainer from 'app/dim-ui/ClosableContainer'; import ConnectedInventoryItem from 'app/inventory/ConnectedInventoryItem'; import DraggableInventoryItem from 'app/inventory/DraggableInventoryItem'; import { DimItem } from 'app/inventory/item-types'; import ItemPopupTrigger from 'app/inventory/ItemPopupTrigger'; /** * Render a pinned or excluded item. */ export default function LockedItem({ lockedItem, onRemove, }: { lockedItem: DimItem; onRemove?: (item: DimItem) => void; }) { return ( onRemove(lockedItem) : undefined} key={lockedItem.id} > {(ref, onClick) => ( )} ); } ================================================ FILE: src/app/loadout-builder/filter/NewFeaturedGearFilter.m.scss ================================================ .container { composes: flexRow from '../../dim-ui/common.m.scss'; align-items: center; justify-content: space-between; label { font-size: 1.17em; cursor: pointer; } :global(.app-icon) { color: #00a5a5; } } ================================================ FILE: src/app/loadout-builder/filter/NewFeaturedGearFilter.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'container': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-builder/filter/NewFeaturedGearFilter.tsx ================================================ import Switch from 'app/dim-ui/Switch'; import { t } from 'app/i18next-t'; import { toggleSearchQueryComponent } from 'app/shell/actions'; import { featuredBannerIcon } from 'app/shell/icons'; import AppIcon from 'app/shell/icons/AppIcon'; import { querySelector } from 'app/shell/selectors'; import { useDispatch, useSelector } from 'react-redux'; import * as styles from './NewFeaturedGearFilter.m.scss'; const newFeaturedGearTerms = ['is:featured', 'is:newgear']; /** * It's important since Edge of Fate to make builds that use "new gear" because * they have a bonus. Folks could enter this search themselves, but this filter * makes it easier and teaches how the search works. */ export default function NewFeaturedGearFilter({ className }: { className?: string }) { const searchQuery = useSelector(querySelector); const dispatch = useDispatch(); let currentQueryTerm: string | undefined; for (const term of newFeaturedGearTerms) { if (searchQuery.includes(term)) { currentQueryTerm = term; break; } } const isEnabled = Boolean(currentQueryTerm); const handleToggle = () => { dispatch(toggleSearchQueryComponent(currentQueryTerm ?? newFeaturedGearTerms[0])); }; return (
); } ================================================ FILE: src/app/loadout-builder/filter/TierlessStatConstraintEditor.m.scss ================================================ @use '../../variables' as *; .editor { composes: flexColumn from '../../dim-ui/common.m.scss'; margin: 0 -4px -4px -4px; gap: 2px; } .iconStat { display: inline-block; height: 17px; width: 17px; } .grabHandle { cursor: grab; touch-action: none; &:active { cursor: grabbing; } } .grip { composes: flexRow from '../../dim-ui/common.m.scss'; composes: grabHandle; grid-area: grip; opacity: 0.5; font-size: 10px; padding-left: 10px; padding-top: 4px; align-self: stretch; align-items: flex-start; } .row { display: grid; grid-template-columns: min-content 1fr min-content; grid-template-areas: 'grip name buttons' 'grip bar bar'; gap: 0 4px; background-color: rgb(0, 0, 0, 0.6); align-items: center; padding: 4px 4px 4px 0; font-size: 14px; > * { white-space: nowrap; } &.ignored { grid-template-areas: 'grip name buttons'; background-color: rgb(0, 0, 0, 0.3); } } .name { grid-area: name; composes: flexRow from '../../dim-ui/common.m.scss'; align-items: center; gap: 2px; } .label { composes: flexRow from '../../dim-ui/common.m.scss'; composes: grabHandle; align-items: center; white-space: nowrap; gap: 2px; text-overflow: ellipsis; overflow: hidden; align-self: stretch; width: 100%; margin-top: -4px; padding-top: 4px; .ignored & { opacity: 0.4; } } .statBar { grid-area: bar; composes: flexRow from '../../dim-ui/common.m.scss'; margin: 4px 0 2px 4px; gap: 6px; align-items: center; justify-content: space-between; } .buttons { composes: flexRow from '../../dim-ui/common.m.scss'; } .rowControl { composes: resetButton from '../../dim-ui/common.m.scss'; padding: 0 6px; opacity: 0.9; @include interactive($hover: true, $focus: true) { &:not(:disabled) { color: var(--theme-accent-primary); } } .ignored &, &:disabled { opacity: 0.9; color: #555; } // We must remove pointer events from the icon or else the wacky event // delegation in useButtonSensor won't work because the event target will be // the icon, not the button. > :global(.app-icon) { pointer-events: none; } } .statRange { width: 100%; // TODO: Colors background-color: #333; height: 12px; position: relative; cursor: pointer; } .statBarFill { position: absolute; background-color: white; height: 100%; transition: all 0.2s ease-in-out, opacity 0.5s linear; } .statBarMin, .statBarMax { position: absolute; top: 0; box-sizing: border-box; height: 20px; width: 4px; transform: translateY(-4px); border: 2px solid var(--theme-accent-primary); } .statBarMax { transform: translateY(-4px) translateX(-2px); border-left: none; } .statBarMin { border-right: none; } .processing { // Apply a gentle pulsing opacity animation animation: processing-pulse 1s alternate infinite; animation-delay: 0.5s; opacity: 0.5; } @keyframes processing-pulse { 100% { opacity: 0.2; } } ================================================ FILE: src/app/loadout-builder/filter/TierlessStatConstraintEditor.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttons': string; 'editor': string; 'grabHandle': string; 'grip': string; 'iconStat': string; 'ignored': string; 'label': string; 'name': string; 'processing': string; 'processingPulse': string; 'row': string; 'rowControl': string; 'statBar': string; 'statBarFill': string; 'statBarMax': string; 'statBarMin': string; 'statRange': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-builder/filter/TierlessStatConstraintEditor.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import { t } from 'app/i18next-t'; import { DimStore } from 'app/inventory/store-types'; import { MAX_STAT } from 'app/loadout/known-values'; import LoadoutEditSection from 'app/loadout/loadout-edit/LoadoutEditSection'; import { useD2Definitions } from 'app/manifest/selectors'; import { percent } from 'app/shell/formatters'; import { AppIcon, dragHandleIcon, faCheckSquare, faSquare, moveDownIcon, moveUpIcon, } from 'app/shell/icons'; import clsx from 'clsx'; import { Reorder, useDragControls } from 'motion/react'; import { Dispatch, useEffect, useId, useRef, useState } from 'react'; import { LoadoutBuilderAction } from '../loadout-builder-reducer'; import { ArmorStatHashes, MinMaxStat, ResolvedStatConstraint, StatRanges } from '../types'; import * as styles from './TierlessStatConstraintEditor.m.scss'; /** * A selector that allows for choosing minimum and maximum stat ranges, plus * reordering the stat priority. This does not use tiers, it allows selecting * exact stat values and is mean to be used after Edge of Fate releases and * makes all stats have an incremental effect. */ export default function TierlessStatConstraintEditor({ store, resolvedStatConstraints, statRangesFiltered, equippedHashes, className, lbDispatch, processing, }: { store: DimStore; resolvedStatConstraints: ResolvedStatConstraint[]; /** The ranges the stats could have gotten to INCLUDING stat filters and mod compatibility */ statRangesFiltered?: Readonly; equippedHashes: Set; className?: string; lbDispatch: Dispatch; processing: boolean; }) { // Local state for dragging - use undefined when not dragging const [draggingOrder, setDraggingOrder] = useState(); // Actually change the stat constraints in the LO state, which triggers recalculation of sets. const handleStatChange = (constraint: ResolvedStatConstraint) => lbDispatch({ type: 'statConstraintChanged', constraint }); const handleClear = () => lbDispatch({ type: 'statConstraintReset' }); const handleRandomize = () => lbDispatch({ type: 'statConstraintRandomize' }); const handleSyncFromEquipped = () => { const constraints = Object.values(store.stats).map( (s): ResolvedStatConstraint => ({ statHash: s.hash, ignored: false, maxStat: MAX_STAT, minStat: s.value, }), ); lbDispatch({ type: 'setStatConstraints', constraints }); }; // Handle reordering the stat constraints const handleReorder = (newOrder: ResolvedStatConstraint[]) => { // During dragging, just update local state without applying business logic setDraggingOrder(newOrder); }; const handleDragEnd = (draggedConstraint: ResolvedStatConstraint) => { if (!draggingOrder) { return; // No dragging in progress } const oldIndex = resolvedStatConstraints.findIndex( (constraint) => constraint.statHash === draggedConstraint.statHash, ); const newIndex = draggingOrder.findIndex( (constraint) => constraint.statHash === draggedConstraint.statHash, ); if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) { lbDispatch({ type: 'statOrderChanged', sourceIndex: oldIndex, destinationIndex: newIndex, }); } setDraggingOrder(undefined); // Reset local state after drag ends }; // Handle button-based reordering (up/down buttons) const handleButtonMove = (currentIndex: number, direction: 'up' | 'down') => { const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; if (targetIndex >= 0 && targetIndex < resolvedStatConstraints.length) { lbDispatch({ type: 'statOrderChanged', sourceIndex: currentIndex, destinationIndex: targetIndex, }); } }; return ( {(draggingOrder ?? resolvedStatConstraints).map((c, index) => { const statHash = c.statHash as ArmorStatHashes; return ( ); })} ); } function StatRow({ statConstraint, statRange, index, onStatChange, onButtonMove, onDragEnd, equippedHashes, processing, }: { statConstraint: ResolvedStatConstraint; statRange?: MinMaxStat; index: number; onStatChange: (constraint: ResolvedStatConstraint) => void; onButtonMove: (currentIndex: number, direction: 'up' | 'down') => void; onDragEnd: (constraint: ResolvedStatConstraint) => void; equippedHashes: Set; processing: boolean; }) { const defs = useD2Definitions()!; const statHash = statConstraint.statHash as ArmorStatHashes; const statDef = defs.Stat.get(statHash); const handleIgnore = () => onStatChange({ ...statConstraint, ignored: !statConstraint.ignored }); // We use our own controls to avoid having the entire element be draggable. // Requires dragListener={false} on Reorder.Item. const controls = useDragControls(); // Assign this to onPointerDown to start dragging from this item const startDrag = (e: React.PointerEvent) => controls.start(e); const setMin = (value: number) => { if (value !== statConstraint.minStat) { onStatChange({ ...statConstraint, minStat: value, maxStat: Math.max(value, statConstraint.maxStat), }); } }; const setMax = (value: number) => { if (value !== statConstraint.maxStat) { onStatChange({ ...statConstraint, minStat: Math.min(value, statConstraint.minStat), maxStat: value, }); } }; const min = statConstraint.minStat; const max = statConstraint.maxStat; return ( onDragEnd(statConstraint)} data-index={index} as="div" dragListener={false} dragControls={controls} >
{statDef.displayProperties.name}
{!statConstraint.ignored && ( )}
); } function StatEditBar({ min, max, setMin, setMax, children, }: { min: number; max: number; setMin: (value: number) => void; setMax: (value: number) => void; children: React.ReactNode; }) { const id = useId(); const [minText, setMinText] = useState(min.toString()); const [maxText, setMaxText] = useState(max.toString()); useEffect(() => { setMinText(min.toString()); }, [min]); useEffect(() => { setMaxText(max.toString()); }, [max]); const setMinMaxFromText = (text: string, setter: (s: number) => void) => { const value = parseInt(text, 10); if (isNaN(value) || value < 0 || value > MAX_STAT) { return; } setter(value); }; const setMinFromText = setMinMaxFromText.bind(null, minText, setMin); const setMaxFromText = setMinMaxFromText.bind(null, maxText, setMax); const handleEnter = (setter: () => void) => (e: React.KeyboardEvent) => { if (e.key === 'Enter') { setter(); } }; return (
{ setMinText(e.target.value); }} onBlur={setMinFromText} onKeyDown={handleEnter(setMinFromText)} /> {children} { setMaxText(e.target.value); }} onBlur={setMaxFromText} onKeyDown={handleEnter(setMaxFromText)} />
); } function StatBar({ min, max, range, setMin, setMax, processing, }: { range?: MinMaxStat; equippedHashes: Set; min: number; max: number; setMin: (value: number) => void; setMax: (value: number) => void; processing: boolean; }) { const [dragging, setDragging] = useState(false); const [dragValue, setDragValue] = useState(0); // Whether we're dragging the max or min value const draggingMax = useRef(false); const lastClickTime = useRef(0); // Set the live value of min or max based on where the pointer is const setValueToPointer = (e: React.PointerEvent) => { const bar = e.currentTarget; const rect = bar.getBoundingClientRect(); const clickX = e.clientX - rect.left; const ratio = Math.max(0, Math.min(1, clickX / rect.width)); const value = Math.round(ratio * MAX_STAT); setDragValue(value); }; const handlePointerMove = (e: React.PointerEvent) => { setValueToPointer(e); }; // Commit the value on pointer up const handlePointerUp = (e: React.PointerEvent) => { const bar = e.currentTarget; bar.releasePointerCapture(e.pointerId); // Detect double-click if (performance.now() - lastClickTime.current < 200 && !draggingMax.current && range) { setMin(range.maxStat); } else { draggingMax.current ? setMax(dragValue) : setMin(dragValue); } setDragging(false); lastClickTime.current = performance.now(); }; const handlePointerDown = (e: React.PointerEvent) => { // If you shift-click you set max, or if you click on the max bar first. draggingMax.current = e.shiftKey || (e.target as Element).classList.contains(styles.statBarMax); const bar = e.currentTarget; bar.setPointerCapture(e.pointerId); setValueToPointer(e); setDragging(true); }; const effectiveMin = dragging && !draggingMax.current ? dragValue : min; const effectiveMax = dragging && draggingMax.current ? dragValue : max; return (
{range && range.minStat < range.maxStat && (
)}
); } ================================================ FILE: src/app/loadout-builder/generated-sets/CompareLoadoutsDrawer.m.scss ================================================ @use '../../variables.scss' as *; .header { font-size: 20px; margin: 4px; } .content { margin: 16px; @include phone-portrait { display: flex; flex-flow: row nowrap; } } .fillRow { display: flex; flex-direction: row; justify-content: space-between; } .setHeader { margin: 0 4px 4px 0; @include phone-portrait { flex-direction: column; } } .setTitle { font-size: 16px; font-weight: 600; margin-bottom: 4px; } .noLoadouts { margin: 16px; font-size: 14px; } .loadoutName { font-weight: 600; } ================================================ FILE: src/app/loadout-builder/generated-sets/CompareLoadoutsDrawer.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'content': string; 'fillRow': string; 'header': string; 'loadoutName': string; 'noLoadouts': string; 'setHeader': string; 'setTitle': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-builder/generated-sets/CompareLoadoutsDrawer.tsx ================================================ import Select from 'app/dim-ui/Select'; import Sheet from 'app/dim-ui/Sheet'; import useConfirm from 'app/dim-ui/useConfirm'; import { t } from 'app/i18next-t'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import LoadoutView from 'app/loadout/LoadoutView'; import { updateLoadout } from 'app/loadout/actions'; import { Loadout } from 'app/loadout/loadout-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import clsx from 'clsx'; import React, { useMemo, useState } from 'react'; import { ArmorSet } from '../types'; import { mergeLoadout } from '../updated-loadout'; import * as styles from './CompareLoadoutsDrawer.m.scss'; // TODO: just get rid of compare loadout feature altogether! function chooseInitialLoadout( setItems: DimItem[], useableLoadouts: Loadout[], initialLoadoutId?: string, ) { // Most of all, try to find the loadout we started with const loadoutFromInitialId = useableLoadouts.find((lo) => lo.id === initialLoadoutId); if (loadoutFromInitialId) { return loadoutFromInitialId; } const exotic = setItems.find((i) => i.isExotic); const initialLoadout = // Prefer finding a loadout that shares an exotic (exotic && useableLoadouts.find((l) => l.items.some((i) => i.hash === exotic.hash))) || // Or else just get whatever the first (in an arbitrary order?) is (useableLoadouts.length ? useableLoadouts[0] : undefined); if (!initialLoadout) { throw new Error("bug: Shouldn't show compare loadouts drawer without any loadouts"); } return initialLoadout; } export default function CompareLoadoutsDrawer({ loadouts, loadout, selectedStore, compareSet, lockedMods, onClose, }: { compareSet: { set: ArmorSet; items: DimItem[]; }; selectedStore: DimStore; loadouts: Loadout[]; loadout: Loadout; lockedMods: PluggableInventoryItemDefinition[]; onClose: () => void; }) { const defs = useD2Definitions()!; const dispatch = useThunkDispatch(); const { set, items } = compareSet; const [selectedLoadout, setSelectedLoadout] = useState(() => chooseInitialLoadout(items, loadouts, loadout.id), ); // This probably isn't needed but I am being cautious as it iterates over the stores. const generatedLoadout = useMemo( () => mergeLoadout(defs, selectedLoadout, loadout, set, items, lockedMods), [defs, selectedLoadout, loadout, set, items, lockedMods], ); const [confirmDialog, confirm] = useConfirm(); const onSaveLoadout = async (e: React.MouseEvent) => { e.preventDefault(); if ( selectedLoadout && !(await confirm(t('LoadoutBuilder.ConfirmOverwrite', { name: selectedLoadout.name }))) ) { return; } if (!generatedLoadout) { return; } dispatch(updateLoadout(generatedLoadout)); onClose(); }; const header =
{t('LoadoutBuilder.CompareLoadout')}
; const loadoutOptions = useMemo( () => loadouts.map((l) => ({ key: l.id, value: l, content: l.name, })), [loadouts], ); // This is likely never to happen but since it is disconnected to the button its here for safety. if (!selectedLoadout || !generatedLoadout) { return (
{t('LoadoutBuilder.NoLoadoutsToCompare')}
); } return ( {confirmDialog}
{t('LoadoutBuilder.OptimizerSet')}
{t('LoadoutBuilder.SaveAs')}{' '} {selectedLoadout.name} , ]} />
{t('LoadoutBuilder.ExistingLoadout')}
key="select-loadout" value={selectedLoadout} options={loadoutOptions} onChange={(l) => setSelectedLoadout(l!)} />, ]} />
); } ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSet.m.scss ================================================ @use '../../variables.scss' as *; .container { --set-bonus-icon-size: 25px; display: flex; flex-direction: column; border-bottom: 1px solid rgb(255, 255, 255, 0.3); gap: 8px; box-sizing: border-box; padding-top: 10px; padding-bottom: 14px; width: fit-content !important; @include phone-portrait { padding-left: 12px; padding-right: 12px; } &:last-child { border-bottom: 0; } } .build { composes: flexRow from '../../dim-ui/common.m.scss'; gap: 16px; @include phone-portrait { flex-direction: column; } } .items { display: flex; flex-flow: row wrap; padding: 0; gap: 14px; } ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSet.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'build': string; 'container': string; 'items': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSet.tsx ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { DimStore, statSourceOrder } from 'app/inventory/store-types'; import { getSetBonusStatus } from 'app/item-popup/SetBonus'; import { calculateAssumedMasterworkStats } from 'app/loadout-drawer/loadout-utils'; import { fotlWildcardHashes } from 'app/loadout/known-values'; import { Loadout } from 'app/loadout/loadout-types'; import { fitMostMods } from 'app/loadout/mod-assignment-utils'; import { getTotalModStatChanges } from 'app/loadout/stats'; import { useD2Definitions } from 'app/manifest/selectors'; import { armorStats } from 'app/search/d2-known-values'; import { mapValues } from 'app/utils/collections'; import { compareByIndex } from 'app/utils/comparators'; import { errorLog } from 'app/utils/log'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { StatHashes } from 'data/d2/generated-enums'; import { intersectionBy, once } from 'es-toolkit'; import { Dispatch, memo, useMemo } from 'react'; import { LoadoutBuilderAction } from '../loadout-builder-reducer'; import { ArmorEnergyRules, ArmorSet, ArmorStatHashes, DesiredStatRange, ModStatChanges, PinnedItems, } from '../types'; import { getPower } from '../utils'; import * as styles from './GeneratedSet.m.scss'; import GeneratedSetButtons from './GeneratedSetButtons'; import GeneratedSetItem from './GeneratedSetItem'; import { TierlessSetStats } from './SetStats'; /** * A single "stat mix" of builds. Each armor slot contains multiple possibilities, * but only the highest light set is displayed. */ export default memo(function GeneratedSet({ originalLoadout, set, selectedStore, lockedMods, pinnedItems, desiredStatRanges, modStatChanges, loadouts, lbDispatch, armorEnergyRules, equippedHashes, autoStatMods, }: { originalLoadout: Loadout; set: ArmorSet; selectedStore: DimStore; lockedMods: PluggableInventoryItemDefinition[]; pinnedItems: PinnedItems; desiredStatRanges: DesiredStatRange[]; modStatChanges: ModStatChanges; loadouts: Loadout[]; lbDispatch: Dispatch; armorEnergyRules: ArmorEnergyRules; equippedHashes: Set; autoStatMods: boolean; }) { const defs = useD2Definitions()!; let overlappingLoadout: Loadout | undefined; // Items are sorted by their energy capacity when grouping let displayedItems = set.armor; const allSetItems = set.armor.flat(); // This has got to be expensive when the user has a lot of loadouts? for (const loadout of loadouts) { // Compare all possible items that could make up this set (not just the first item in each bucket) against all the equipped items of the given loadout const equippedLoadoutItems = loadout.items.filter((item) => item.equip); const intersection = intersectionBy( allSetItems, equippedLoadoutItems as unknown as DimItem[], // intersectionBy doesn't actually need the types to match (item) => item.id, ); if (intersection.length === set.armor.length) { overlappingLoadout = loadout; // Replace the list of items to show with the ones that were from the matching loadout displayedItems = intersection; break; } } // Automatically added stat/artifice mods const autoMods = useMemo( () => set.statMods.map((d) => defs.InventoryItem.get(d) as PluggableInventoryItemDefinition), [defs.InventoryItem, set.statMods], ); // Assign the chosen mods to items so we can display them as if they were slotted const [itemModAssignments, unassignedMods, resultingItemEnergies] = useMemo(() => { const allMods = [...lockedMods, ...autoMods]; // TODO: this isn't assigning the tuning mods correctly, and we aren't calculating balanced tuning stats correctly either. const { itemModAssignments, unassignedMods, invalidMods, resultingItemEnergies } = fitMostMods({ defs, items: displayedItems, plannedMods: allMods, armorEnergyRules, }); // Set rendering is a great place to verify that the worker process // and DIM's regular mod assignment algorithm agree with each other, // so do that here. if (unassignedMods.length || invalidMods.length) { errorLog( 'loadout optimizer', 'internal error: set rendering was unable to fit some mods that the worker thought were possible', { unassignedMods, invalidMods }, ); } return [itemModAssignments, unassignedMods, resultingItemEnergies]; }, [lockedMods, autoMods, defs, displayedItems, armorEnergyRules]); // Compute a presentable stat breakdown, lazily. This is a bit expensive, so we calculate it only // when it's actually needed (in the tooltip), and memoize this via once (no need to memoize // the memoized function since this component itself is memoized and the dependency array would // include most props). const getStatsBreakdownOnce = once(() => getStatsBreakdown( defs, selectedStore.classType, set, modStatChanges, armorEnergyRules, itemModAssignments, unassignedMods, ), ); const boostedStats = useMemo( () => new Set( armorStats.filter((hash) => modStatChanges[hash].breakdown?.some((change) => change.source === 'runtimeEffect'), ), ), [modStatChanges], ); // Distribute our automatically picked mods across the items so that item components // can highlight them const assignAutoMods = set.statMods.slice(); const autoModsPerItem = mapValues(itemModAssignments, (mods) => { const autoModHashes = []; for (const mod of mods) { const modIdx = assignAutoMods.findIndex((m) => m === mod.hash); if (modIdx !== -1) { autoModHashes.push(mod.hash); assignAutoMods.splice(modIdx, 1); } } return autoModHashes; }); const canCompareLoadouts = loadouts.length > 0; const setBonusStatus = getSetBonusStatus(defs, set.armor); return ( <> fotlWildcardHashes.has(i.hash))} />
{displayedItems.map((item) => ( ))}
); }); export const containerClass = styles.container; /** * Compute a presentable stat breakdown. This info isn't quite easy to get since * the process worker responds with full build stats and auto mod hashes and * simply says "trust me, these are correct". When the user wants to actually * see the breakdown, we have to add the summed up ProcessItem armor stats * (which the worker does respond with), the mod/subclass stats from the loadout * (with LoadoutBuilder calculates higher up and passes to the worker too), and * the auto mods the worker picked (which we calculate via * `getTotalModStatChanges` here). */ function getStatsBreakdown( defs: D2ManifestDefinitions, classType: DestinyClass, set: ArmorSet, modStatChanges: ModStatChanges, armorEnergyRules: ArmorEnergyRules, itemModAssignments: { [itemInstanceId: string]: PluggableInventoryItemDefinition[]; }, unassignedMods: PluggableInventoryItemDefinition[], ) { const totals: ModStatChanges = { [StatHashes.Weapons]: { value: 0, breakdown: [] }, [StatHashes.Health]: { value: 0, breakdown: [] }, [StatHashes.Class]: { value: 0, breakdown: [] }, [StatHashes.Grenade]: { value: 0, breakdown: [] }, [StatHashes.Super]: { value: 0, breakdown: [] }, [StatHashes.Melee]: { value: 0, breakdown: [] }, }; const autoModStats = getTotalModStatChanges( defs, unassignedMods, /* subclass */ undefined, // doesn't matter, modStatChanges already includes subclass stats classType, /* includeRuntimeStatBenefits */ false, // doesn't matter, auto mods have no runtime stats itemModAssignments, set.armor, ); // We have a bit of a problem where armor mods can come from both // the global loadout parameters (modStatChanges) and the auto stat mods // (autoModStats), so we have to merge them together here by matching // hashes and adding counts/values const mergeContributions = ( contributions: ModStatChanges[ArmorStatHashes], hash: ArmorStatHashes, ) => { totals[hash].value += contributions.value; if (contributions.breakdown) { const existingBreakdown = totals[hash].breakdown!; for (const part of contributions.breakdown) { const existingIndex = existingBreakdown.findIndex( (change) => change.source === part.source && change.hash === part.hash, ); if (existingIndex === -1) { existingBreakdown.push(part); } else { const existingEntry = existingBreakdown[existingIndex]; existingBreakdown[existingIndex] = { ...existingEntry, count: existingEntry.count || part.count ? (existingEntry.count ?? 0) + (part.count ?? 0) : undefined, value: existingEntry.value + part.value, }; } } } }; // Recompute the assumed armor stats, since the set stats might already have // the effect of a tuning mod baked in. const stats = set.armor.reduce<{ [statHash: number]: number; }>((memo, dimItem) => { const itemStats = calculateAssumedMasterworkStats(dimItem, armorEnergyRules); for (const [statHash, value] of Object.entries(itemStats)) { const statHashNum = parseInt(statHash, 10) as ArmorStatHashes; memo[statHashNum] = (memo[statHashNum] || 0) + value; } return memo; }, {}); for (const hash of armorStats) { totals[hash].value += stats[hash]; totals[hash].breakdown!.push({ hash: -1, count: undefined, name: t('Loadouts.ArmorStats'), icon: undefined, source: 'armorStats', value: stats[hash], }); mergeContributions(modStatChanges[hash], hash); mergeContributions(autoModStats[hash], hash); // Similarly to the above, a good place to check for errors -- if the worker thinks a set has // certain stats, and we calculate different stats, then that's a bug somewhere. if (totals[hash].value !== set.stats[hash]) { errorLog( 'loadout optimizer', 'internal error: set rendering came up with different build stats from what the worker said', totals, set.stats, ); } } for (const val of Object.values(totals)) { val.breakdown!.sort(compareByIndex(statSourceOrder, (val) => val.source)); } return totals; } ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSetButtons.m.scss ================================================ @use '../../variables.scss' as *; .buttons { grid-area: buttons; text-align: center; display: flex; flex-direction: column; @include phone-portrait { flex-flow: row wrap; width: auto; } @media (min-width: 1080px) { margin-right: 0; } > * { // Matches three action buttons to the same height as two socket slots min-height: calc((((0.59 * var(--item-size)) * 2) - 4px) / 3); width: 100%; // Matches GeneratedSetItem width min-width: calc( var(--item-size) + ((((var(--item-size) + ((var(--item-size) / 5) + 4px)) / 2) - 1px) * 2) + 8px ); margin-bottom: 4px; padding: 5px; @include phone-portrait { margin-right: 4px; width: auto; } } } ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSetButtons.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttons': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSetButtons.tsx ================================================ import { t } from 'app/i18next-t'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { applyLoadout } from 'app/loadout-drawer/loadout-apply'; import { editLoadout } from 'app/loadout-drawer/loadout-events'; import { Loadout } from 'app/loadout/loadout-types'; import { loadoutSavedSelector } from 'app/loadout/selectors'; import { useD2Definitions } from 'app/manifest/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { Dispatch } from 'react'; import { useSelector } from 'react-redux'; import { LoadoutBuilderAction } from '../loadout-builder-reducer'; import { ArmorSet } from '../types'; import { updateLoadoutWithArmorSet } from '../updated-loadout'; import * as styles from './GeneratedSetButtons.m.scss'; /** * Renders the Create Loadout and Equip Items buttons for each generated set */ export default function GeneratedSetButtons({ originalLoadout, store, set, items, lockedMods, canCompareLoadouts, lbDispatch, }: { originalLoadout: Loadout; store: DimStore; set: ArmorSet; /** The list of items to use - these are chosen from the set's options and match what's displayed. */ items: DimItem[]; lockedMods: PluggableInventoryItemDefinition[]; canCompareLoadouts: boolean; lbDispatch: Dispatch; }) { const defs = useD2Definitions()!; const dispatch = useThunkDispatch(); const loadout = () => updateLoadoutWithArmorSet(defs, originalLoadout, set, items, lockedMods); const isSaved = useSelector(loadoutSavedSelector(originalLoadout.id)); // Opens the loadout menu for the generated set const openLoadout = () => editLoadout(loadout(), store.id, { showClass: false, }); // Automatically equip items for this generated set to the active store const equipItems = () => dispatch(applyLoadout(store, loadout(), { allowUndo: true })); return (
{canCompareLoadouts && ( )}
); } ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSetItem.m.scss ================================================ @use '../../variables.scss' as *; .item { composes: flexRow from '../../dim-ui/common.m.scss'; gap: 6px; width: min-content; align-items: start; justify-content: start; .energyMeter { margin-top: 3px; padding: 3px 0; width: 100%; @include phone-portrait { padding: 3px 0 20px; } } } .energySwapContainer { composes: flexRow from '../../dim-ui/common.m.scss'; justify-content: flex-start; align-items: center; font-size: 10px; } .arrow { margin: 0 4px; } .energyValue { composes: flexRow from '../../dim-ui/common.m.scss'; align-items: center; } .swapButton { composes: dim-button from global; justify-self: center; margin-top: 4px; @include phone-portrait { padding: 8px 20px; } } .swapButtonContainer { composes: flexColumn from '../../dim-ui/common.m.scss'; min-height: 24px; align-items: center; @include phone-portrait { min-height: 32px; } } .energyHidden { visibility: hidden; } .archetype { --item-icon-size: calc(var(--item-size) * 0.4); height: var(--item-icon-size); width: var(--item-icon-size); :global(.item-img) { --item-size: var(--item-icon-size); } } ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSetItem.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'archetype': string; 'arrow': string; 'energyHidden': string; 'energyMeter': string; 'energySwapContainer': string; 'energyValue': string; 'item': string; 'swapButton': string; 'swapButtonContainer': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSetItem.tsx ================================================ import { EnergyIncrementsWithPresstip } from 'app/dim-ui/EnergyIncrements'; import { t } from 'app/i18next-t'; import PlugDef from 'app/loadout/loadout-ui/PlugDef'; import Sockets from 'app/loadout/loadout-ui/Sockets'; import { toggleSearchQueryComponent } from 'app/shell/actions'; import { AppIcon, pinIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { getArmorArchetypeSocket } from 'app/utils/socket-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { BucketHashes, PlugCategoryHashes } from 'data/d2/generated-enums'; import { Dispatch } from 'react'; import { DimItem, PluggableInventoryItemDefinition } from '../../inventory/item-types'; import LoadoutBuilderItem from '../LoadoutBuilderItem'; import { LoadoutBuilderAction } from '../loadout-builder-reducer'; import { autoAssignmentPCHs } from '../types'; import * as styles from './GeneratedSetItem.m.scss'; /** * Shows how we recommend the energy of this armor be changed in order to fit its mods. */ export function EnergySwap({ energy }: { energy: { energyCapacity: number; energyUsed: number } }) { const armorEnergyCapacity = energy.energyCapacity; const resultingEnergyCapacity = Math.max(energy.energyUsed, armorEnergyCapacity); const noEnergyChange = resultingEnergyCapacity === armorEnergyCapacity; return (
{armorEnergyCapacity}
{resultingEnergyCapacity}
); } /** * An individual item in a generated set. Includes a perk display and a button for selecting * alternative items with the same stat mix. */ export default function GeneratedSetItem({ item, pinned, assignedMods, autoStatMods, automaticallyPickedMods, energy, lbDispatch, }: { item: DimItem; pinned: boolean; assignedMods?: PluggableInventoryItemDefinition[]; autoStatMods: boolean; automaticallyPickedMods?: number[]; energy: { energyCapacity: number; energyUsed: number }; lbDispatch: Dispatch; }) { const pinItem = (item: DimItem) => lbDispatch({ type: 'pinItem', item }); const unpinItem = () => lbDispatch({ type: 'unpinItem', item }); const dispatch = useThunkDispatch(); const onSocketClick = ( plugDef: PluggableInventoryItemDefinition, plugCategoryHashWhitelist?: number[], ) => { const { plugCategoryHash } = plugDef.plug; if (plugCategoryHash === PlugCategoryHashes.Intrinsics) { // Legendary armor can have intrinsic perks and it might be // nice to provide a convenient user interface for those, // but the exotic picker is not the way to do it. if (item.isExotic && item.bucket.hash !== BucketHashes.ClassArmor) { lbDispatch({ type: 'lockExotic', lockedExoticHash: item.hash }); } else { dispatch(toggleSearchQueryComponent(`exactperk:"${plugDef.displayProperties.name}"`)); } } else if ( !autoAssignmentPCHs.includes(plugCategoryHash) && (!autoStatMods || plugCategoryHash !== PlugCategoryHashes.EnhancementsV2General) ) { lbDispatch({ type: 'openModPicker', plugCategoryHashWhitelist, }); } }; const archetype = getArmorArchetypeSocket(item)?.plugged!.plugDef; return (
pinItem(item)} /> {pinned ? ( ) : ( archetype && ( ) )}
); } ================================================ FILE: src/app/loadout-builder/generated-sets/GeneratedSets.tsx ================================================ import ErrorBoundary from 'app/dim-ui/ErrorBoundary'; import { WindowVirtualList } from 'app/dim-ui/VirtualList'; import { PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { Loadout } from 'app/loadout/loadout-types'; import { identity } from 'app/utils/functions'; import { Dispatch } from 'react'; import { LoadoutBuilderAction } from '../loadout-builder-reducer'; import { ArmorEnergyRules, ArmorSet, DesiredStatRange, ModStatChanges, PinnedItems, } from '../types'; import GeneratedSet, { containerClass } from './GeneratedSet'; /** * Renders the entire list of generated stat mixes, one per mix. */ export default function GeneratedSets({ lockedMods, pinnedItems, selectedStore, sets, equippedHashes, desiredStatRanges, modStatChanges, loadouts, lbDispatch, armorEnergyRules, loadout, autoStatMods, }: { selectedStore: DimStore; sets: readonly ArmorSet[]; equippedHashes: Set; lockedMods: PluggableInventoryItemDefinition[]; pinnedItems: PinnedItems; desiredStatRanges: DesiredStatRange[]; modStatChanges: ModStatChanges; loadouts: Loadout[]; lbDispatch: Dispatch; armorEnergyRules: ArmorEnergyRules; loadout: Loadout; autoStatMods: boolean; }) { return ( {(index) => ( )} ); } ================================================ FILE: src/app/loadout-builder/generated-sets/SetStats.m.scss ================================================ @use '../../variables' as *; .container { color: #ddd; font-size: 14px; display: flex; flex-flow: row wrap; gap: 4px 8px; align-items: center; @include phone-portrait { display: grid; grid-template-columns: repeat(7, min-content); > *:nth-child(n + 9) { grid-column: span 6; } } } .statIcon { vertical-align: middle; height: 16px; width: 16px; font-size: 16px; margin-right: 1px; } .light { color: $gold; white-space: nowrap; margin-right: 0.5em; :global(.app-icon) { vertical-align: 3px; font-size: 8px; margin-right: 1px; } } .nonActiveStat { opacity: 0.4; } .tierLightContainer { align-items: center; display: flex; margin-right: 0.5em; @include phone-portrait { margin-right: 0; } .nonActiveStat { margin-left: 0.25em; } } .tier { font-weight: bold; } .existingLoadout { margin: 0; color: #e8a534; font-size: 0.9em; } .loadoutName { font-weight: bold; } .statSegment { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; white-space: nowrap; min-width: 48px; @include phone-portrait { min-width: 0; } } .boostedValue { color: $stat-modded; } .statBarWarningIcon { font-size: 20px; } ================================================ FILE: src/app/loadout-builder/generated-sets/SetStats.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'boostedValue': string; 'container': string; 'existingLoadout': string; 'light': string; 'loadoutName': string; 'nonActiveStat': string; 'statBarWarningIcon': string; 'statIcon': string; 'statSegment': string; 'tier': string; 'tierLightContainer': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-builder/generated-sets/SetStats.tsx ================================================ import { AlertIcon } from 'app/dim-ui/AlertIcon'; import BungieImage from 'app/dim-ui/BungieImage'; import { PressTip } from 'app/dim-ui/PressTip'; import { t } from 'app/i18next-t'; import { ActiveSetBonusInfo, SetBonusesStatus } from 'app/item-popup/SetBonus'; import { MAX_STAT } from 'app/loadout/known-values'; import { useD2Definitions } from 'app/manifest/selectors'; import { AppIcon, powerIndicatorIcon } from 'app/shell/icons'; import StatTooltip from 'app/store-stats/StatTooltip'; import { sumBy } from 'app/utils/collections'; import { DestinyStatDefinition } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { sum } from 'es-toolkit'; import { ArmorStatHashes, ArmorStats, DesiredStatRange, ModStatChanges, ResolvedStatConstraint, } from '../types'; import * as styles from './SetStats.m.scss'; import { sumEnabledStats } from './utils'; /** * Displays the overall stats and per-stat stat of a generated loadout set. */ // TODO: would be a lot easier if this was just passed a Loadout or FullyResolvedLoadout... export function TierlessSetStats({ stats, getStatsBreakdown, maxPower, desiredStatRanges, boostedStats, className, existingLoadoutName, equippedHashes, setBonusStatus, fotlWarning, }: { stats: ArmorStats; getStatsBreakdown: () => ModStatChanges; maxPower: number; desiredStatRanges: DesiredStatRange[]; boostedStats: Set; className?: string; existingLoadoutName?: string; equippedHashes: Set; setBonusStatus: ActiveSetBonusInfo; fotlWarning?: boolean; }) { const defs = useD2Definitions()!; const totalStats = sum(Object.values(stats)); const countedStatsTotal = sumEnabledStats(stats, desiredStatRanges); // Total of the stats that were within the desired ranges // TODO: Lots of changes needed here once we drop tiers. Maybe we just show a // total stat sum? Doesn't seem that useful... // TODO: Highlight enhanced stats? return (
{desiredStatRanges.map((c) => { const statHash = c.statHash as ArmorStatHashes; const statDef = defs.Stat.get(statHash); const value = stats[statHash]; return ( ( )} > 0} isBoosted={boostedStats.has(statHash)} stat={statDef} value={value} effectiveValue={Math.min(value, c.maxStat)} /> ); })} {maxPower} {fotlWarning && ( )} {existingLoadoutName ? ( {t('LoadoutBuilder.ExistingLoadout')}:{' '} {existingLoadoutName} ) : null}
); } function TierlessStat({ stat, isActive, isBoosted, value, effectiveValue, }: { stat: DestinyStatDefinition; isActive: boolean; isBoosted: boolean; value: number; effectiveValue: number; }) { let shownValue: number; let ignoredExcess: number | undefined; if (effectiveValue !== value) { if (effectiveValue === 0 || effectiveValue >= MAX_STAT) { shownValue = value; } else { shownValue = effectiveValue; ignoredExcess = value - effectiveValue; } } else { shownValue = value; } const showIgnoredExcess = ignoredExcess !== undefined; return ( {shownValue} {showIgnoredExcess && +{ignoredExcess}} ); } function TotalStats({ totalStats, enabledStats }: { totalStats: number; enabledStats: number }) { return ( <> {t('LoadoutBuilder.StatTotal', { total: enabledStats })} {enabledStats !== totalStats && ( ({totalStats}) )} ); } export function ReferenceConstraints({ resolvedStatConstraints, }: { resolvedStatConstraints: ResolvedStatConstraint[]; }) { const defs = useD2Definitions()!; const totalStats = sumBy(resolvedStatConstraints, (c) => c.minStat); const enabledStats = sumBy(resolvedStatConstraints, (c) => (c.ignored ? 0 : c.minStat)); return (
{resolvedStatConstraints.map((c) => { const statHash = c.statHash as ArmorStatHashes; const statDef = defs.Stat.get(statHash); const value = c.minStat; return ( {value} ); })}
); } ================================================ FILE: src/app/loadout-builder/generated-sets/utils.ts ================================================ import { sumBy } from 'app/utils/collections'; import { chainComparator, Comparator, compareBy } from 'app/utils/comparators'; import { sum } from 'es-toolkit'; import { ArmorSet, ArmorStatHashes, ArmorStats, DesiredStatRange } from '../types'; import { getPower } from '../utils'; function getComparatorsForMatchedSetSorting(desiredStatRanges: DesiredStatRange[]) { const comparators: Comparator[] = [ compareBy((s) => -sumEnabledStats(s.stats, desiredStatRanges)), ]; for (const constraint of desiredStatRanges) { comparators.push( compareBy( (s) => -Math.min(s.stats[constraint.statHash as ArmorStatHashes], constraint.maxStat), ), ); } comparators.push( // Finally sort by total stats, then by power compareBy((s) => -sum(Object.values(s.stats))), compareBy((s) => -getPower(s.armor)), ); return comparators; } /** * A final sorting pass over all sets. This should mostly agree with the sorting in the worker, * but it may do a final pass over the returned sets to add more stat mods and that requires us * to sort again. So just do that here. */ export function sortGeneratedSets( sets: ArmorSet[], desiredStatRanges: DesiredStatRange[], ): ArmorSet[] { return sets.sort(chainComparator(...getComparatorsForMatchedSetSorting(desiredStatRanges))); } export function sumEnabledStats(stats: ArmorStats, desiredStatRanges: DesiredStatRange[]) { return sumBy(desiredStatRanges, (constraint) => Math.min(stats[constraint.statHash as ArmorStatHashes], constraint.maxStat), ); } ================================================ FILE: src/app/loadout-builder/item-filter.test.ts ================================================ import { AssumeArmorMasterwork } from '@destinyitemmanager/dim-api-types'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { isPluggableItem } from 'app/inventory/store/sockets'; import { isLoadoutBuilderItem } from 'app/loadout/loadout-item-utils'; import { ModMap, categorizeArmorMods } from 'app/loadout/mod-assignment-utils'; import { count, sumBy } from 'app/utils/collections'; import { stubFalse, stubTrue } from 'app/utils/functions'; import { itemCanBeEquippedBy } from 'app/utils/item-utils'; import { BucketHashes, PlugCategoryHashes } from 'data/d2/generated-enums'; import { maxBy } from 'es-toolkit'; import { elementalChargeModHash, stacksOnStacksModHash } from 'testing/test-item-utils'; import { getTestDefinitions, getTestStores } from 'testing/test-utils'; import { FilterInfo, filterItems } from './item-filter'; import { ArmorBucketHash, ArmorBucketHashes, ArmorEnergyRules, ItemsByBucket, LOCKED_EXOTIC_ANY_EXOTIC, LOCKED_EXOTIC_NO_EXOTIC, loDefaultArmorEnergyRules, } from './types'; function expectItemCount(filterInfo: FilterInfo, expectedCount: number) { expect( sumBy(Object.values(filterInfo.perBucketStats), (s) => s.finalValid + s.removedStrictlyWorse), ).toBe(expectedCount); } describe('loadout-builder item-filter', () => { let defs: D2ManifestDefinitions; let store: DimStore; let items: DimItem[]; let stacksOnStacksMod: PluggableInventoryItemDefinition; let elementalChargeMod: PluggableInventoryItemDefinition; const defaultArgs = { lockedExoticHash: undefined, excludedItems: {}, pinnedItems: {}, searchFilter: stubTrue, armorEnergyRules: loDefaultArmorEnergyRules, } satisfies Partial[0]>; let noMods: { modMap: ModMap; unassignedMods: PluggableInventoryItemDefinition[] }; beforeAll(async () => { let stores: DimStore[]; [defs, stores] = await Promise.all([getTestDefinitions(), getTestStores()]); const allItems = stores.flatMap((store) => store.items); const isValidItem = (store: DimStore, item: DimItem) => itemCanBeEquippedBy(item, store) && isLoadoutBuilderItem(item) && item.rarity !== 'Rare'; store = maxBy(stores, (store) => count(allItems, (item) => isValidItem(store, item)))!; items = allItems.filter((item) => isValidItem(store, item)); noMods = categorizeArmorMods([], items); stacksOnStacksMod = defs.InventoryItem.get( stacksOnStacksModHash, ) as PluggableInventoryItemDefinition; expect(isPluggableItem(stacksOnStacksMod)).toBe(true); expect(stacksOnStacksMod.plug.energyCost!.energyCost).toBe(4); expect(stacksOnStacksMod.plug.plugCategoryHash).toBe(PlugCategoryHashes.EnhancementsV2Legs); elementalChargeMod = defs.InventoryItem.get( elementalChargeModHash, ) as PluggableInventoryItemDefinition; expect(isPluggableItem(elementalChargeMod)).toBe(true); expect(elementalChargeMod.plug.energyCost!.energyCost).toBe(3); expect(elementalChargeMod.plug.plugCategoryHash).toBe(PlugCategoryHashes.EnhancementsV2Legs); }); function noPinInvariants(filteredItems: ItemsByBucket, filterInfo: FilterInfo) { let numItems = 0; for (const bucketHash of ArmorBucketHashes) { const originalItems = items.filter((i) => i.bucket.hash === bucketHash); const originalNum = originalItems.length; const filteredNum = filteredItems[bucketHash].length; const removedNum = filterInfo.perBucketStats[bucketHash].cantFitMods + filterInfo.perBucketStats[bucketHash].removedBySearchFilter + filterInfo.perBucketStats[bucketHash].removedStrictlyWorse; const numConsidered = filterInfo.perBucketStats[bucketHash].totalConsidered; expect(numConsidered).toBe(originalNum); expect(originalNum - removedNum).toBe(filteredNum); numItems += numConsidered; } expect(numItems).toBe(items.length); } function pinInvariants(filteredItems: ItemsByBucket, filterInfo: FilterInfo) { for (const bucketHash of ArmorBucketHashes) { const filteredNum = filteredItems[bucketHash].length; const removedNum = filterInfo.perBucketStats[bucketHash].cantFitMods + filterInfo.perBucketStats[bucketHash].removedBySearchFilter + filterInfo.perBucketStats[bucketHash].removedStrictlyWorse; const numConsidered = filterInfo.perBucketStats[bucketHash].totalConsidered; expect(numConsidered - removedNum).toBe(filteredNum); } } it('filters nothing out when no filters specified', () => { const [_filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: noMods.modMap, unassignedMods: noMods.unassignedMods, }); expectItemCount(filterInfo, items.length); }); it('filters out items with insufficient energy capacity', () => { const { modMap, unassignedMods } = categorizeArmorMods( // 10 cost [stacksOnStacksMod, elementalChargeMod, elementalChargeMod], items, ); expect(unassignedMods.length).toBe(0); const cases: [rules: ArmorEnergyRules, expectAllItemsFit: boolean][] = [ [loDefaultArmorEnergyRules, false], [ { ...loDefaultArmorEnergyRules, assumeArmorMasterwork: AssumeArmorMasterwork.All, }, true, ], ]; for (const [rules, expectAllItemsFit] of cases) { const [filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: modMap, unassignedMods, armorEnergyRules: rules, }); noPinInvariants(filteredItems, filterInfo); for (const bucketHash of ArmorBucketHashes) { const removedNum = filterInfo.perBucketStats[bucketHash].cantFitMods; if (bucketHash === BucketHashes.LegArmor) { if (expectAllItemsFit) { expect(removedNum).toBe(0); } else { for (const item of filteredItems[bucketHash]) { expect(item.energy?.energyCapacity).toBeGreaterThanOrEqual(9); } } } else { expect(removedNum).toBe(0); } } } }); it('filters nothing out when any exotic', () => { const [_filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: noMods.modMap, unassignedMods: noMods.unassignedMods, lockedExoticHash: LOCKED_EXOTIC_ANY_EXOTIC, }); expectItemCount(filterInfo, items.length); }); it('removes exotics when no exotic', () => { const [filteredItems] = filterItems({ ...defaultArgs, defs, items, lockedModMap: noMods.modMap, unassignedMods: noMods.unassignedMods, lockedExoticHash: LOCKED_EXOTIC_NO_EXOTIC, }); for (const item of Object.values(filteredItems).flat()) { expect(item.isExotic).toBe(false); } }); it('filters nothing out when infeasible filter', () => { const [_filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: noMods.modMap, unassignedMods: noMods.unassignedMods, searchFilter: stubFalse, }); expectItemCount(filterInfo, items.length); expect(filterInfo.searchQueryEffective).toBe(false); }); it('ignores filter for certain slots', () => { const [_filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: noMods.modMap, unassignedMods: noMods.unassignedMods, searchFilter: (item) => item.bucket.hash === BucketHashes.Helmet, }); expectItemCount(filterInfo, items.length); expect(filterInfo.searchQueryEffective).toBe(false); }); it('filter applies to some slots', () => { const [filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: noMods.modMap, unassignedMods: noMods.unassignedMods, searchFilter: (item) => (item.bucket.hash !== BucketHashes.Helmet && item.bucket.hash !== BucketHashes.Gauntlets) || // How to certainly filter some items? item.hash % 2 === 0, }); noPinInvariants(filteredItems, filterInfo); expect(filterInfo.searchQueryEffective).toBe(true); expect( count(Object.values(filterInfo.perBucketStats), (stat) => Boolean(stat.removedBySearchFilter), ), ).toBe(2); }); it('specific exotic filtered', () => { const targetExotic = items.find((item) => item.isExotic)!; const [filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: noMods.modMap, unassignedMods: noMods.unassignedMods, lockedExoticHash: targetExotic.hash, }); pinInvariants(filteredItems, filterInfo); for (const bucketHash of ArmorBucketHashes) { if (bucketHash === targetExotic.bucket.hash) { for (const item of filteredItems[bucketHash]) { expect(item.hash).toBe(targetExotic.hash); } } else { for (const item of filteredItems[bucketHash]) { expect(item.isExotic).toBe(false); } } } }); it('any exotic does not ignore filters if they can be satisfied', () => { const [filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: noMods.modMap, unassignedMods: noMods.unassignedMods, lockedExoticHash: LOCKED_EXOTIC_ANY_EXOTIC, searchFilter: (item) => item.rarity === 'Legendary' || item.bucket.hash === BucketHashes.Helmet, }); noPinInvariants(filteredItems, filterInfo); for (const item of Object.values(filteredItems).flat()) { expect(!item.isExotic || item.bucket.hash === BucketHashes.Helmet).toBe(true); } }); it('any exotic ignores filters if they cannot be satisfied', () => { const [filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: noMods.modMap, unassignedMods: noMods.unassignedMods, lockedExoticHash: LOCKED_EXOTIC_ANY_EXOTIC, searchFilter: (item) => item.rarity === 'Legendary', }); noPinInvariants(filteredItems, filterInfo); const originalExotics = items.filter((i) => i.isExotic); const filteredExotics = Object.values(filteredItems) .flat() .filter((i) => i.isExotic); expect(originalExotics.length).toBe(filteredExotics.length); }); it('mod assignment may cause exotic slot to not have options', () => { // Find a leg armor exotic where every copy does not have 10 energy const exotic = items.find( (i) => i.isExotic && i.bucket.hash === BucketHashes.LegArmor && items.every( (otherItem) => otherItem.hash !== i.hash || otherItem.energy!.energyCapacity < 10, ), )!; const { modMap, unassignedMods } = categorizeArmorMods( // 10 cost [stacksOnStacksMod, elementalChargeMod, elementalChargeMod], items, ); expect(unassignedMods.length).toBe(0); const [filteredItems, filterInfo] = filterItems({ ...defaultArgs, defs, items, lockedModMap: modMap, unassignedMods: unassignedMods, lockedExoticHash: exotic.hash, }); pinInvariants(filteredItems, filterInfo); expect(filteredItems[exotic.bucket.hash as ArmorBucketHash].length).toBe(0); }); }); ================================================ FILE: src/app/loadout-builder/item-filter.ts ================================================ import { SetBonusCounts } from '@destinyitemmanager/dim-api-types'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { calculateAssumedMasterworkStats } from 'app/loadout-drawer/loadout-utils'; import { calculateAssumedItemEnergy } from 'app/loadout/armor-upgrade-utils'; import { fotlWildcardHashes } from 'app/loadout/known-values'; import { ModMap, assignBucketSpecificMods } from 'app/loadout/mod-assignment-utils'; import { armorStats } from 'app/search/d2-known-values'; import { ItemFilter } from 'app/search/filter-types'; import { sumBy } from 'app/utils/collections'; import { getModTypeTagByPlugCategoryHash, getSpecialtySocketMetadata } from 'app/utils/item-utils'; import { warnLog } from 'app/utils/log'; import { computeStatDupeLower } from 'app/utils/stats'; import { BucketHashes } from 'data/d2/generated-enums'; import { sum } from 'es-toolkit'; import { Draft } from 'immer'; import { ArmorBucketHash, ArmorBucketHashes, ArmorEnergyRules, ExcludedItems, ItemsByBucket, LOCKED_EXOTIC_ANY_EXOTIC, LOCKED_EXOTIC_NO_EXOTIC, PinnedItems, } from './types'; /** * Contains information about whether items got filtered out for various reasons. */ export interface FilterInfo { /** Did the search query limit items for any bucket? */ searchQueryEffective: boolean; exoticDoesNotExist: boolean; perBucketStats: { [key in ArmorBucketHash]: { totalConsidered: number; cantFitMods: number; finalValid: number; removedStrictlyWorse: number; removedBySearchFilter: number; }; }; } /** * Filter the `items` map down given the locking and filtering configs and some * basic logic about what could go together in a loadout. The goal here is to * remove as many items as possible from consideration without expensive logic. */ export function filterItems({ defs, items, pinnedItems, excludedItems, lockedModMap, unassignedMods, lockedExoticHash, armorEnergyRules, searchFilter, setBonuses, }: { defs: D2ManifestDefinitions | undefined; items: DimItem[]; pinnedItems: PinnedItems; excludedItems: ExcludedItems; lockedModMap: ModMap; unassignedMods: PluggableInventoryItemDefinition[]; lockedExoticHash: number | undefined; armorEnergyRules: ArmorEnergyRules; searchFilter: ItemFilter; setBonuses?: SetBonusCounts; }): [ItemsByBucket, FilterInfo] { const filteredItems: Draft = { [BucketHashes.Helmet]: [], [BucketHashes.Gauntlets]: [], [BucketHashes.ChestArmor]: [], [BucketHashes.LegArmor]: [], [BucketHashes.ClassArmor]: [], }; const emptyFilterInfo = { totalConsidered: 0, cantFitMods: 0, finalValid: 0, removedBySearchFilter: 0, removedStrictlyWorse: 0, }; const filterInfo: FilterInfo = { searchQueryEffective: false, exoticDoesNotExist: false, perBucketStats: { [BucketHashes.Helmet]: { ...emptyFilterInfo }, [BucketHashes.Gauntlets]: { ...emptyFilterInfo }, [BucketHashes.ChestArmor]: { ...emptyFilterInfo }, [BucketHashes.LegArmor]: { ...emptyFilterInfo }, [BucketHashes.ClassArmor]: { ...emptyFilterInfo }, }, }; if (!items.length || !defs || unassignedMods.length) { return [filteredItems, filterInfo]; } // Usability hack: If the user requests any exotic but none of the exotics match the search filter, ignore the search filter for exotics. // This allows things like `modslot:xyz` to apply to legendary armor without removing all exotics. const excludeExoticsFromFilter = lockedExoticHash === LOCKED_EXOTIC_ANY_EXOTIC && !Object.values(items) .flat() .some( (item) => item.isExotic && lockedExoticHash === LOCKED_EXOTIC_ANY_EXOTIC && searchFilter(item), ); // Group by bucket const itemsByBucket = Map.groupBy(items, (item) => item.bucket.hash as ArmorBucketHash); const lockedExoticDef = lockedExoticHash && lockedExoticHash > 0 ? defs.InventoryItem.get(lockedExoticHash) : undefined; const requiredModTags = new Set(); for (const mod of lockedModMap.activityMods) { const modTag = getModTypeTagByPlugCategoryHash(mod.plug.plugCategoryHash); if (modTag) { requiredModTags.add(modTag); } } const requiredModTagsArray = Array.from(requiredModTags).sort(); // Currently set bonuses take 2 or 4 pieces. Exotics are 1 item. Armor is 5 pieces total. // 2 + 2 + 1 = 4 + 1 = 5 // If the user has locked an exotic, AND they have asked for set bonus(es) that require 4 items, // either 4 of the same set, or 2 each of 2 sets, then filter legendaries items to those sets. /** If set, only use items with these set bonuses. */ let includeOnlySetBonusHashes: undefined | number[]; if (setBonuses && sum(Object.values(setBonuses)) >= 4 && lockedExoticDef) { includeOnlySetBonusHashes = Object.keys(setBonuses).map(Number); } for (const bucket of ArmorBucketHashes) { const lockedModsForPlugCategoryHash = lockedModMap.bucketSpecificMods[bucket] || []; // There can only be one pinned item as we hide items from the item picker once // a single item is pinned const pinnedItem = pinnedItems[bucket]; const lockedExoticApplicable = lockedExoticHash !== undefined && lockedExoticHash > 0 && lockedExoticDef?.inventory?.bucketTypeHash === bucket; const exotics = lockedExoticApplicable ? (itemsByBucket.get(bucket) ?? []).filter( (item) => item.hash === lockedExoticHash || item.name === lockedExoticDef.displayProperties.name, ) : undefined; if (lockedExoticApplicable && !exotics?.length) { filterInfo.exoticDoesNotExist = true; } // We prefer most specific filtering since there can be competing conditions. // This means locked item and then exotic let firstPassFilteredItems = itemsByBucket.get(bucket) ?? []; if (pinnedItem) { // If the user pinned an item, that's what they get firstPassFilteredItems = [pinnedItem]; } else if (exotics) { // If the user chose an exotic, only include items matching that exotic firstPassFilteredItems = exotics; } else if ( // The user chose to exclude all exotics lockedExoticHash === LOCKED_EXOTIC_NO_EXOTIC || // The locked exotic is in a different bucket, so we can remove all // exotics from this bucket (lockedExoticDef && !lockedExoticApplicable) ) { firstPassFilteredItems = firstPassFilteredItems.filter((i) => !i.isExotic); } if (!firstPassFilteredItems.length) { warnLog( 'loadout optimizer', 'no items for bucket', bucket, 'does this happen enough to be worth reporting in some way?', ); } // If every non-exotic requires set bonuses... if (includeOnlySetBonusHashes && !lockedExoticApplicable) { firstPassFilteredItems = firstPassFilteredItems.filter( (item) => (item.setBonus && includeOnlySetBonusHashes.includes(item.setBonus.hash)) || fotlWildcardHashes.has(item.hash), ); } // Remove manually excluded items const withoutExcluded = firstPassFilteredItems.filter( (item) => !excludedItems[bucket]?.some((excluded) => item.id === excluded.id), ); // Remove armor that can't fit all the chosen bucket-specifics mods. // Filtering on energy cost is necessary because set generation only checks // mod energy for combinations of bucket independent mods. const itemsThatFitMods = withoutExcluded.filter( (item) => assignBucketSpecificMods(armorEnergyRules, item, lockedModsForPlugCategoryHash).unassigned .length === 0, ); const searchFilteredItems = itemsThatFitMods.filter( (item) => (item.isExotic && excludeExoticsFromFilter) || searchFilter(item), ); // If a search filters out all the possible items for a bucket, we ignore // the search. This allows users to filter some buckets without getting // stuck making no sets. let finalFilteredItems = searchFilteredItems.length ? searchFilteredItems : itemsThatFitMods; const removedBySearchFilter = searchFilteredItems.length ? itemsThatFitMods.length - searchFilteredItems.length : 0; filterInfo.searchQueryEffective ||= removedBySearchFilter > 0; let removedStrictlyWorse = 0; if (finalFilteredItems.length > 1) { // One last pass - remove items that are strictly worse than others. This // uses the same general logic as the `is:statlower` search filter, but // also considers the energy capacity of the items and the set of activity // mod slots they have. So if two items have the same stats, but one has // more energy capacity or more relevant slots, it will be kept. Since we // reuse the logic from `is:statlower`, this also takes into account // artifice/tuning mods. // This duplicates some logic from mapDimItemToProcessItems, but it's // easier to filter items out here than to do it later. const getStats = (item: DimItem) => { // Masterwork them up to the assumed masterwork level const masterworkedStatValues = calculateAssumedMasterworkStats(item, armorEnergyRules); const compatibleModSeason = getSpecialtySocketMetadata(item)?.slotTag; const capacity = calculateAssumedItemEnergy(item, armorEnergyRules); const modsCost = lockedModsForPlugCategoryHash ? sumBy(lockedModsForPlugCategoryHash, (mod) => mod.plug.energyCost?.energyCost ?? 0) : 0; const remainingEnergyCapacity = capacity - modsCost; return [ ...armorStats.map((statHash) => ({ statHash, value: masterworkedStatValues[statHash] ?? 0, })), { statHash: -2, value: remainingEnergyCapacity }, ...requiredModTagsArray.map((tag) => ({ statHash: -3, // ←↑ Dummy/temp stat hashes. Just need to not match real armor stat hashes. value: compatibleModSeason === tag ? 1 : 0, })), // Add a comparison stat for each required set bonus. An item that has that bonus scores 1, others score zero. // Statlower will make sure any matching set bonus item won't lose to an item without it. ...Object.keys(setBonuses || {}).map((h) => { const setBonusHash = parseInt(h, 10); return { statHash: -10 - setBonusHash, value: item.setBonus?.hash === setBonusHash ? 1 : 0, }; }), ]; }; const strictlyWorseItemIds = computeStatDupeLower( finalFilteredItems, // Consider all stats, even if they're not enabled - we still want the // highest total stats. armorStats, // Use our own getStats function to consider energy capacity and activity mod slots getStats, ); finalFilteredItems = finalFilteredItems.filter((item) => !strictlyWorseItemIds.has(item.id)); removedStrictlyWorse = strictlyWorseItemIds.size; } filteredItems[bucket] = finalFilteredItems; filterInfo.perBucketStats[bucket] = { totalConsidered: firstPassFilteredItems.length, cantFitMods: withoutExcluded.length - itemsThatFitMods.length, removedBySearchFilter, removedStrictlyWorse, finalValid: finalFilteredItems.length, }; } return [filteredItems, filterInfo]; } ================================================ FILE: src/app/loadout-builder/loadout-builder-reducer.ts ================================================ import { AssumeArmorMasterwork, LoadoutParameters, SetBonusCounts, StatConstraint, defaultLoadoutParameters, } from '@destinyitemmanager/dim-api-types'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { savedLoStatConstraintsByClassSelector, savedLoadoutParametersSelector, } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { allItemsSelector } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { isPluggableItem } from 'app/inventory/store/sockets'; import { getCurrentStore } from 'app/inventory/stores-helpers'; import { LoadoutUpdateFunction, clearSubclass, removeMod, setLoadoutParameters, updateMods, } from 'app/loadout-drawer/loadout-drawer-reducer'; import { findItemForLoadout, newLoadout, pickBackingStore } from 'app/loadout-drawer/loadout-utils'; import { EFFECTIVE_MAX_STAT, MAX_STAT } from 'app/loadout/known-values'; import { isLoadoutBuilderItem } from 'app/loadout/loadout-item-utils'; import { Loadout, ResolvedLoadoutMod } from 'app/loadout/loadout-types'; import { showNotification } from 'app/notifications/notifications'; import { armor2PlugCategoryHashesByName, armorStats } from 'app/search/d2-known-values'; import { count, isEmpty, reorder } from 'app/utils/collections'; import { emptyObject } from 'app/utils/empty'; import { useHistory } from 'app/utils/undo-redo-history'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { keyBy, shuffle } from 'es-toolkit'; import { useCallback, useMemo, useReducer } from 'react'; import { useSelector } from 'react-redux'; import { resolveStatConstraints, unresolveStatConstraints } from './loadout-params'; import { ArmorBucketHashes, ArmorSet, ExcludedItems, PinnedItems, ResolvedStatConstraint, autoAssignmentPCHs, } from './types'; interface LoadoutBuilderUI { modPicker: { open: boolean; plugCategoryHashWhitelist?: number[]; }; compareSet?: { set: ArmorSet; /** * The items selected from the armor set's options to use. This isn't * always just the first option for each bucket. */ items: DimItem[]; }; } interface LoadoutBuilderConfiguration { /** * The store we're operating on. We do this instead of just a class because * different characters of the same class may have different mods or vendor * items unlocked. */ selectedStoreId: string; /** * The loadout we're optimizing. Either a brand new loadout (starting from * saved preferences) or an existing loadout where we're trying to improve its * stats. */ loadout: Loadout; /** * If we are editing an existing loadout via the "better stats available" * feature, this contains the stats we actually need to exceed. */ strictUpgradesStatConstraints: ResolvedStatConstraint[] | undefined; /** * A copy of `loadout.parameters.statConstraints`, but with ignored stats * included. This is more convenient to use than the raw `statConstraints` but * is kept in sync. Like `statConstraints` this is always in stat preference * order. */ resolvedStatConstraints: ResolvedStatConstraint[]; // TODO: While I can think of reasons to have them, I don't love the complex // interaction of selecting individual pinned/excluded items. Maybe instead // rely on search (e.g. -is:inloadout) and otherwise let LO choose via mods. pinnedItems: PinnedItems; excludedItems: ExcludedItems; // TODO: When we are starting with an existing loadout, maybe have a new sort // of "locked" state where we only show sets that improve upon the loadout's // existing stat tiers (still obeying the stat filters)? // e.g. setTierFilter: 'all' | 'betterInAllStats' | 'betterInTotalTier' (as an enum...) } export type LoadoutBuilderState = LoadoutBuilderUI & LoadoutBuilderConfiguration; export function warnMissingClass(classType: DestinyClass, defs: D2ManifestDefinitions) { const missingClassName = Object.values(defs.Class.getAll()).find( (c) => c.classType === classType, )!.displayProperties.name; showNotification({ type: 'error', title: t('LoadoutBuilder.MissingClass', { className: missingClassName }), body: t('LoadoutBuilder.MissingClassDescription'), }); } /** * Create the initial state object for the loadout optimizer. */ const lbConfigInit = ({ stores, allItems, defs, preloadedLoadout, storeId, savedLoadoutBuilderParameters, savedStatConstraintsPerClass, strictUpgradesStatConstraints, }: { stores: DimStore[]; allItems: DimItem[]; defs: D2ManifestDefinitions; /** * A loadout that we are starting with, from the Loadouts page or editor. * This can be null to start with a brand new loadout. */ // TODO: Maybe always provide an initial loadout. preloadedLoadout: Loadout | undefined; storeId: string | undefined; savedLoadoutBuilderParameters: LoadoutParameters; savedStatConstraintsPerClass: { [classType: number]: StatConstraint[] }; strictUpgradesStatConstraints: ResolvedStatConstraint[] | undefined; }): LoadoutBuilderConfiguration => { // Preloaded loadouts from the "Optimize Armor" button take priority const classTypeFromPreloadedLoadout = preloadedLoadout?.classType ?? DestinyClass.Unknown; // Pick a store that matches the given store ID, or fall back to the loadout's classType const storeMatchingClass = pickBackingStore(stores, storeId, classTypeFromPreloadedLoadout); const initialLoadoutParameters = preloadedLoadout?.parameters; // If we requested a specific class type but the user doesn't have it, we // need to pick some different store, but ensure that class-specific stuff // doesn't end up in LO parameters. if (!storeMatchingClass && classTypeFromPreloadedLoadout !== DestinyClass.Unknown) { // This can't actually happen anymore since we won't open a loadout share at // all if we don't have that class warnMissingClass(classTypeFromPreloadedLoadout, defs); preloadedLoadout = undefined; } // Fall back to the current store if we didn't find a store matching our class const selectedStore = storeMatchingClass ?? getCurrentStore(stores)!; const selectedStoreId = selectedStore.id; const classType = selectedStore.classType; let loadout = preloadedLoadout ?? newLoadout('', [], classType); // In order of increasing priority: // default parameters, global saved parameters, stat order for this class, // things that came from the loadout share or preloaded loadout let loadoutParameters = { ...defaultLoadoutParameters, ...savedLoadoutBuilderParameters }; const thisClassStatConstraints = savedStatConstraintsPerClass[classType]; if (thisClassStatConstraints) { loadoutParameters.statConstraints = thisClassStatConstraints; } loadoutParameters = { ...loadoutParameters, ...initialLoadoutParameters }; const pinnedItems: PinnedItems = {}; // Loadouts only support items that are supported by the Loadout's class if (preloadedLoadout) { // Pin all the items in the preloaded loadout // TODO: instead of pinning items, show the loadout fixed at the top to compare against and leave all items free for (const loadoutItem of preloadedLoadout.items) { if (loadoutItem.equip) { const item = findItemForLoadout(defs, allItems, selectedStoreId, loadoutItem); if (item && isLoadoutBuilderItem(item)) { pinnedItems[item.bucket.hash] = item; } } // TODO: maybe swap in the updated item ID for items here, to make future manipulation easier } // If we load a loadout with an exotic, pre-fill the exotic armor selection if (!loadoutParameters.exoticArmorHash) { const equippedExotic = preloadedLoadout.items .filter((li) => li.equip) .map((li) => defs.InventoryItem.get(li.hash)) .find( (i) => Boolean(i?.equippingBlock?.uniqueLabel) && ArmorBucketHashes.includes(i.inventory?.bucketTypeHash ?? 0), ); if (equippedExotic) { loadoutParameters = { ...loadoutParameters, exoticArmorHash: equippedExotic.hash }; } } } // Also delete always-auto-assigned mods since they are picked automatically // per set. In contrast we remove stat mods dynamically depending on the auto // stat mods setting. if (loadoutParameters.mods) { loadoutParameters.mods = stripAlwaysAutoAssignedMods(defs, loadoutParameters.mods); } loadout = { ...loadout, parameters: loadoutParameters }; return { loadout, resolvedStatConstraints: resolveStatConstraints(loadoutParameters.statConstraints!), strictUpgradesStatConstraints, pinnedItems, excludedItems: emptyObject(), selectedStoreId, }; }; /** * We never want to include always-auto-assigned mods in the list of mods for a * loadout being edited by LO - they should be chosen by LO itself, and only * re-added when the loadout is saved. */ function stripAlwaysAutoAssignedMods(defs: D2ManifestDefinitions, mods: number[]) { return mods.filter((modHash) => { const def = defs.InventoryItem.get(modHash); return ( def && isPluggableItem(def) && !autoAssignmentPCHs.some((h) => def.plug.plugCategoryHash === h) ); }); } type LoadoutBuilderConfigAction = | { type: 'setLoadout'; updateFn: LoadoutUpdateFunction } | { type: 'changeCharacter'; store: DimStore; savedStatConstraintsByClass: { [classType: number]: StatConstraint[] }; } | { type: 'statConstraintChanged'; constraint: ResolvedStatConstraint } | { type: 'statConstraintReset' } | { type: 'statConstraintRandomize' } | { type: 'setStatConstraints'; constraints: ResolvedStatConstraint[] } | { type: 'statOrderChanged'; sourceIndex: number; destinationIndex: number } | { type: 'assumeArmorMasterworkChanged'; assumeArmorMasterwork: AssumeArmorMasterwork | undefined; } | { type: 'pinItem'; item: DimItem } | { type: 'setPinnedItems'; items: DimItem[] } | { type: 'unpinItem'; item: DimItem } | { type: 'excludeItem'; item: DimItem } | { type: 'unexcludeItem'; item: DimItem } | { type: 'clearExcludedItems' } | { type: 'setSetBonuses'; setBonuses: SetBonusCounts } | { type: 'removeSetBonuses' } | { type: 'autoStatModsChanged'; autoStatMods: boolean } | { type: 'lockedModsChanged'; lockedMods: number[] } | { type: 'removeLockedMod'; mod: ResolvedLoadoutMod } /** For adding "half tier mods" */ | { type: 'addGeneralMods'; mods: PluggableInventoryItemDefinition[] } | { type: 'lockExotic'; lockedExoticHash: number | undefined } | { type: 'removeLockedExotic' } | { type: 'dismissComparisonStats' } | { type: 'setSearchQuery'; query: string }; type LoadoutBuilderUIAction = | { type: 'openModPicker'; plugCategoryHashWhitelist?: number[] } | { type: 'closeModPicker' } | { type: 'openCompareDrawer'; set: ArmorSet; items: DimItem[] } | { type: 'closeCompareDrawer' }; export type LoadoutBuilderAction = | LoadoutBuilderConfigAction | LoadoutBuilderUIAction | { type: 'undo' } | { type: 'redo' }; function lbUIReducer(state: LoadoutBuilderUI, action: LoadoutBuilderUIAction) { switch (action.type) { case 'openCompareDrawer': return { ...state, compareSet: { set: action.set, items: action.items } }; case 'openModPicker': return { ...state, modPicker: { open: true, plugCategoryHashWhitelist: action.plugCategoryHashWhitelist, }, }; case 'closeCompareDrawer': return { ...state, compareSet: undefined }; case 'closeModPicker': return { ...state, modPicker: { open: false } }; } } function lbConfigReducer(defs: D2ManifestDefinitions) { return ( state: LoadoutBuilderConfiguration, action: LoadoutBuilderConfigAction, ): LoadoutBuilderConfiguration => { switch (action.type) { case 'setLoadout': { return updateLoadout(state, (loadout) => { const updatedLoadout = action.updateFn(loadout); // Always check to make sure Artifice mods haven't snuck in - if they have, remove them const originalMods = updatedLoadout.parameters?.mods ?? []; const strippedMods = stripAlwaysAutoAssignedMods(defs, originalMods); if (strippedMods.length !== originalMods.length) { return updateMods(strippedMods)(updatedLoadout); } return updatedLoadout; }); } case 'changeCharacter': { const { store } = action; const originalLoadout = state.loadout; let loadout: Loadout = { ...originalLoadout, classType: store.classType }; // Always remove the subclass loadout = clearSubclass(defs)(loadout); // And the exotic let loadoutParameters = { ...loadout.parameters, exoticArmorHash: undefined, }; // Apply stat constraint preferences const constraints = action.savedStatConstraintsByClass[store.classType]; if (constraints) { loadoutParameters = { ...loadoutParameters, statConstraints: constraints }; } loadout = setLoadoutParameters(loadoutParameters)(loadout); return { ...state, loadout, resolvedStatConstraints: resolveStatConstraints(loadoutParameters.statConstraints!), selectedStoreId: store.id, // Also clear out pinned/excluded items pinnedItems: {}, excludedItems: {}, }; } case 'statConstraintChanged': { const { constraint } = action; const newStatConstraints = state.resolvedStatConstraints.map((c) => c.statHash === constraint.statHash ? constraint : c, ); return updateStatConstraints(state, newStatConstraints); } case 'statConstraintReset': { return updateStatConstraints( state, armorStats.map((s) => ({ statHash: s, minStat: 0, maxStat: MAX_STAT, ignored: false })), ); } case 'statConstraintRandomize': { return updateStatConstraints( state, shuffle( armorStats.map((s) => ({ statHash: s, minStat: Math.floor(Math.random() * EFFECTIVE_MAX_STAT), maxStat: MAX_STAT, ignored: false, })), ), ); } case 'setStatConstraints': { const { constraints } = action; return updateStatConstraints(state, constraints); } case 'statOrderChanged': { const { sourceIndex, destinationIndex } = action; const newOrder = reorder(state.resolvedStatConstraints, sourceIndex, destinationIndex); return updateStatConstraints(state, newOrder); } case 'pinItem': { const { item } = action; const bucketHash = item.bucket.hash; return { ...state, // Remove any previously locked item in that bucket and add this one pinnedItems: { ...state.pinnedItems, [bucketHash]: item, }, // Locking an item clears excluded items in this bucket excludedItems: { ...state.excludedItems, [bucketHash]: undefined, }, }; } case 'setPinnedItems': { const { items } = action; return { ...state, pinnedItems: keyBy(items, (i) => i.bucket.hash), excludedItems: {}, }; } case 'unpinItem': { const { item } = action; const bucketHash = item.bucket.hash; return { ...state, pinnedItems: { ...state.pinnedItems, [bucketHash]: undefined, }, }; } case 'excludeItem': { const { item } = action; const bucketHash = item.bucket.hash; if (state.excludedItems[bucketHash]?.some((i) => i.id === item.id)) { return state; // item's already there } const existingExcluded = state.excludedItems[bucketHash] ?? []; return { ...state, // Also unpin items in this bucket pinnedItems: { ...state.pinnedItems, [bucketHash]: undefined, }, excludedItems: { ...state.excludedItems, [bucketHash]: [...existingExcluded, item], }, }; } case 'unexcludeItem': { const { item } = action; const bucketHash = item.bucket.hash; const newExcluded = (state.excludedItems[bucketHash] ?? []).filter((i) => i.id !== item.id); return { ...state, excludedItems: { ...state.excludedItems, [bucketHash]: newExcluded.length > 0 ? newExcluded : undefined, }, }; } case 'clearExcludedItems': return { ...state, excludedItems: {}, }; case 'lockedModsChanged': return updateLoadout(state, updateMods(action.lockedMods)); case 'assumeArmorMasterworkChanged': { const { assumeArmorMasterwork } = action; return updateLoadout(state, setLoadoutParameters({ assumeArmorMasterwork })); } case 'addGeneralMods': { const newMods = [...(state.loadout.parameters?.mods ?? [])]; let currentGeneralModsCount = count( newMods, (mod) => defs.InventoryItem.get(mod)?.plug?.plugCategoryHash === armor2PlugCategoryHashesByName.general, ) ?? 0; const failures: string[] = []; for (const mod of action.mods) { if (currentGeneralModsCount < 5) { newMods.push(mod.hash); currentGeneralModsCount++; } else { failures.push(mod.displayProperties.name); } } if (failures.length) { showNotification({ title: t('LoadoutBuilder.UnableToAddAllMods'), body: t('LoadoutBuilder.UnableToAddAllModsBody', { mods: failures.join(', ') }), type: 'warning', }); } return updateLoadout(state, updateMods(newMods)); } case 'setSetBonuses': { return updateLoadout( state, setLoadoutParameters({ setBonuses: isEmpty(action.setBonuses) ? undefined : action.setBonuses, }), ); } case 'removeSetBonuses': { return updateLoadout(state, setLoadoutParameters({ setBonuses: undefined })); } case 'removeLockedMod': return updateLoadout(state, removeMod(action.mod)); case 'lockExotic': { const { lockedExoticHash } = action; return updateLoadout(state, setLoadoutParameters({ exoticArmorHash: lockedExoticHash })); } case 'removeLockedExotic': return updateLoadout(state, setLoadoutParameters({ exoticArmorHash: undefined })); case 'autoStatModsChanged': return updateLoadout(state, setLoadoutParameters({ autoStatMods: action.autoStatMods })); case 'dismissComparisonStats': return { ...state, strictUpgradesStatConstraints: undefined }; case 'setSearchQuery': return updateLoadout(state, setLoadoutParameters({ query: action.query || undefined })); } }; } function updateLoadout(state: LoadoutBuilderConfiguration, updateFn: LoadoutUpdateFunction) { return { ...state, loadout: updateFn(state.loadout), }; } function updateStatConstraints( state: LoadoutBuilderConfiguration, resolvedStatConstraints: ResolvedStatConstraint[], ): LoadoutBuilderConfiguration { return { ...state, resolvedStatConstraints, loadout: setLoadoutParameters({ statConstraints: unresolveStatConstraints(resolvedStatConstraints), })(state.loadout), }; } export function useLbState( stores: DimStore[], defs: D2ManifestDefinitions, preloadedLoadout: Loadout | undefined, storeId: string | undefined, strictUpgradesStatConstraints: ResolvedStatConstraint[] | undefined, ) { const savedLoadoutBuilderParameters = useSelector(savedLoadoutParametersSelector); const savedStatConstraintsPerClass = useSelector(savedLoStatConstraintsByClassSelector); const allItems = useSelector(allItemsSelector); const { state: lbConfState, setState, redo, undo, canRedo, canUndo, } = useHistory( lbConfigInit({ stores, allItems, defs, preloadedLoadout, storeId, savedLoadoutBuilderParameters, savedStatConstraintsPerClass, strictUpgradesStatConstraints, }), ); const lbConfReducer = useMemo(() => lbConfigReducer(defs), [defs]); const [lbUIState, lbUIDispatch] = useReducer(lbUIReducer, { compareSet: undefined, modPicker: { open: false }, }); const dispatch = useCallback( (action: LoadoutBuilderAction) => { switch (action.type) { case 'undo': undo(); lbUIDispatch({ type: 'closeCompareDrawer' }); lbUIDispatch({ type: 'closeModPicker' }); break; case 'redo': redo(); lbUIDispatch({ type: 'closeCompareDrawer' }); lbUIDispatch({ type: 'closeModPicker' }); break; case 'openCompareDrawer': case 'closeCompareDrawer': case 'openModPicker': case 'closeModPicker': lbUIDispatch(action); break; default: setState((oldState) => lbConfReducer(oldState, action)); break; } }, [lbConfReducer, redo, setState, undo], ); return [ { ...lbConfState, ...lbUIState, canUndo, canRedo, }, dispatch, ] as const; } ================================================ FILE: src/app/loadout-builder/loadout-builder-vendors.ts ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { DimItem } from 'app/inventory/item-types'; import { VendorHashes } from 'app/search/d2-known-values'; import { emptyArray } from 'app/utils/empty'; import { currySelector } from 'app/utils/selectors'; import { useLoadVendors } from 'app/vendors/hooks'; import { characterVendorItemsSelector, vendorsByCharacterSelector } from 'app/vendors/selectors'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; /** * Everything is a vendor and everything is an item, so tons of "vendors" will * constantly sell "items" that somewhat look like armor but actually aren't. * Maybe there's a good way to figure out which is which -- going with an explicit * allow-list of vendors for now. */ const allowedVendorHashes = [ VendorHashes.AdaTransmog, VendorHashes.Xur, VendorHashes.DevrimKay, VendorHashes.Failsafe, VendorHashes.RivensWishesExotics, VendorHashes.XurLegendaryItems, VendorHashes.VanguardArms, ]; export const loVendorItemsSelector = currySelector( createSelector(characterVendorItemsSelector, (allVendorItems) => { const relevantItems = allVendorItems.filter( (item) => allowedVendorHashes.includes(item.vendor?.vendorHash ?? -1) && // filters out some dummy exotics item.bucket.hash !== -1, ); // some signs that vendor items aren't yet loaded. to prevent recalcs, only add in vendor items once they're all ready return !relevantItems.length || relevantItems.some((i) => i.missingSockets === 'not-loaded') ? emptyArray() : relevantItems; }), ); export function useLoVendorItems(selectedStoreId: string) { const account = useSelector(currentAccountSelector)!; const vendorItems = useSelector(loVendorItemsSelector(selectedStoreId)); const vendors = useSelector(vendorsByCharacterSelector); useLoadVendors(account, selectedStoreId); return { vendorItems, error: vendors[selectedStoreId]?.error, }; } ================================================ FILE: src/app/loadout-builder/loadout-params.test.ts ================================================ import { MAX_STAT } from 'app/loadout/known-values'; import { armorStats } from 'app/search/d2-known-values'; import { resolveStatConstraints, unresolveStatConstraints } from './loadout-params'; describe('resolveStatConstraints', () => { const cases = [ { name: 'fills in ignored stats', before: [ { statHash: armorStats[0], minStat: 20, maxStat: 60 }, { statHash: armorStats[2], minStat: 10 }, ], after: [ { statHash: armorStats[0], minStat: 20, maxStat: 60, ignored: false }, // Order is preserved { statHash: armorStats[2], minStat: 10, maxStat: MAX_STAT, ignored: false }, { statHash: armorStats[1], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[3], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[4], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[5], minStat: 0, maxStat: MAX_STAT, ignored: true }, ], }, { name: 'uses minTier/maxTier if minStat/maxStat are not provided', before: [{ statHash: armorStats[1], minTier: 3, maxTier: 5 }], after: [ { statHash: armorStats[1], minStat: 30, maxStat: 50, ignored: false }, { statHash: armorStats[0], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[2], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[3], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[4], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[5], minStat: 0, maxStat: MAX_STAT, ignored: true }, ], }, { name: 'fills in from an empty array', before: [], after: [ { statHash: armorStats[0], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[1], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[2], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[3], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[4], minStat: 0, maxStat: MAX_STAT, ignored: true }, { statHash: armorStats[5], minStat: 0, maxStat: MAX_STAT, ignored: true }, ], }, ]; for (const { name, before, after } of cases) { it(name, () => { const result = resolveStatConstraints(before); expect(result).toEqual(after); }); } }); describe('unresolveStatConstraints', () => { const cases = [ { name: 'converts resolved constraints back to StatConstraint[]', before: [ { statHash: armorStats[2], minStat: 10, maxStat: MAX_STAT, ignored: false }, { statHash: armorStats[0], minStat: 20, maxStat: 60, ignored: false }, { statHash: armorStats[1], minStat: 0, maxStat: MAX_STAT, ignored: true }, ], after: [ { statHash: armorStats[2], minStat: 10 }, { statHash: armorStats[0], minStat: 20, maxStat: 60 }, ], }, { name: 'omits minStat if 0 and maxStat if MAX_STAT', before: [{ statHash: armorStats[0], minStat: 0, maxStat: MAX_STAT, ignored: false }], after: [{ statHash: armorStats[0] }], }, ]; for (const { name, before, after } of cases) { it(name, () => { const result = unresolveStatConstraints(before); expect(result).toEqual(after); }); } }); ================================================ FILE: src/app/loadout-builder/loadout-params.ts ================================================ /* Functions for dealing with the LoadoutParameters structure we save with loadouts and use to save and share LO settings. */ import { StatConstraint } from '@destinyitemmanager/dim-api-types'; import { MAX_STAT } from 'app/loadout/known-values'; import { armorStats } from 'app/search/d2-known-values'; import { compareBy } from 'app/utils/comparators'; import { keyBy } from 'es-toolkit'; import { ResolvedStatConstraint } from './types'; /** * Stat constraints are already in priority order, but they do not include * ignored stats. This fills in the ignored stats as well, retaining stat order. */ export function resolveStatConstraints( statConstraints: StatConstraint[], ): ResolvedStatConstraint[] { const statConstraintsByStatHash = keyBy(statConstraints, (c) => c.statHash); const resolvedStatConstraints: ResolvedStatConstraint[] = armorStats.map((statHash) => { const c = statConstraintsByStatHash[statHash]; const minStat = c?.minStat ?? (c?.minTier ?? 0) * 10; const maxStat = c?.maxStat ?? (c?.maxTier !== undefined ? c.maxTier * 10 : MAX_STAT); return { statHash, minStat, maxStat, ignored: !c }; }); return resolvedStatConstraints.sort( compareBy((h) => { const index = statConstraints.findIndex((c) => c.statHash === h.statHash); return index >= 0 ? index : // Fall back to the in-game order 100 + armorStats.findIndex((c) => c === h.statHash); }), ); } export function unresolveStatConstraints( resolvedStatConstraints: ResolvedStatConstraint[], ): StatConstraint[] { return resolvedStatConstraints .filter((c) => !c.ignored) .map((c) => { const { statHash, minStat, maxStat } = c; const constraint: StatConstraint = { statHash }; if (minStat > 0) { constraint.minStat = minStat; } if (maxStat < MAX_STAT) { constraint.maxStat = maxStat; } return constraint; }); } ================================================ FILE: src/app/loadout-builder/process/mappers.test.ts ================================================ import { AssumeArmorMasterwork } from '@destinyitemmanager/dim-api-types'; import { DimItem } from 'app/inventory/item-types'; import { getTestStores } from 'testing/test-utils'; import { loDefaultArmorEnergyRules, MIN_LO_ITEM_ENERGY } from '../types'; import { mapDimItemToProcessItems } from './mappers'; describe('lo process mappers', () => { let classItem: DimItem; beforeAll(async () => { const stores = await getTestStores(); for (const store of stores) { for (const storeItem of store.items) { if (storeItem.energy && storeItem.stats?.every((stat) => stat.value === 0)) { classItem = storeItem; break; } } } }); test('mapped energy capacity is 10 when assumed masterwork is used', () => { const mappedItem = mapDimItemToProcessItems({ dimItem: classItem, armorEnergyRules: { ...loDefaultArmorEnergyRules, assumeArmorMasterwork: AssumeArmorMasterwork.All, }, modsForSlot: [], desiredStatRanges: [], autoStatMods: true, })[0]; expect(mappedItem.remainingEnergyCapacity).toBe(10); }); test('mapped energy capacity is the items when assumed masterwork is not used', () => { const modifiedItem: DimItem = { ...classItem, energy: { ...classItem.energy!, energyCapacity: 9 }, }; const mappedItem = mapDimItemToProcessItems({ dimItem: modifiedItem, armorEnergyRules: loDefaultArmorEnergyRules, modsForSlot: [], desiredStatRanges: [], autoStatMods: true, })[0]; expect(mappedItem.remainingEnergyCapacity).toBe(modifiedItem.energy?.energyCapacity); }); test('mapped energy capacity defaults to MIN_LO_ITEM_ENERGY when assumed masterwork is not used and the item energy is low', () => { const modifiedItem: DimItem = { ...classItem, energy: { ...classItem.energy!, energyCapacity: 2 }, }; const mappedItem = mapDimItemToProcessItems({ dimItem: modifiedItem, armorEnergyRules: loDefaultArmorEnergyRules, modsForSlot: [], desiredStatRanges: [], autoStatMods: true, })[0]; expect(mappedItem.remainingEnergyCapacity).toBe(MIN_LO_ITEM_ENERGY); }); }); ================================================ FILE: src/app/loadout-builder/process/mappers.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { isPluggableItem } from 'app/inventory/store/sockets'; import { isPlugStatActive, mapAndFilterInvestmentStats, } from 'app/inventory/store/stats-conditional'; import { calculateAssumedMasterworkStats } from 'app/loadout-drawer/loadout-utils'; import { calculateAssumedItemEnergy, isAssumedArtifice } from 'app/loadout/armor-upgrade-utils'; import { activityModPlugCategoryHashes, knownModPlugCategoryHashes, MAX_STAT, } from 'app/loadout/known-values'; import { armorStats } from 'app/search/d2-known-values'; import { filterMap, mapValues, sumBy } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { getArmor3TuningSocket } from 'app/utils/socket-utils'; import { emptyPlugHashes } from 'data/d2/empty-plug-hashes'; import { StatHashes } from 'data/d2/generated-enums'; import { minBy } from 'es-toolkit'; import { DimItem, PluggableInventoryItemDefinition } from '../../inventory/item-types'; import { getModTypeTagByPlugCategoryHash, getSpecialtySocketMetadata, } from '../../utils/item-utils'; import { AutoModData, ProcessItem, ProcessMod } from '../process-worker/types'; import { ArmorEnergyRules, artificeSocketReusablePlugSetHash, artificeStatBoost, AutoModDefs, DesiredStatRange, generalSocketReusablePlugSetHash, majorStatBoost, minorStatBoost, } from '../types'; export function mapArmor2ModToProcessMod(mod: PluggableInventoryItemDefinition): ProcessMod { const processMod: ProcessMod = { hash: mod.hash, energyCost: mod.plug.energyCost?.energyCost ?? 0, }; if ( activityModPlugCategoryHashes.includes(mod.plug.plugCategoryHash) || !knownModPlugCategoryHashes.includes(mod.plug.plugCategoryHash) ) { processMod.tag = getModTypeTagByPlugCategoryHash(mod.plug.plugCategoryHash); } return processMod; } /** * Turns a real DimItem, armor upgrade rules, and bucket specific mods into the bits of * information relevant for LO. * This may return multiple variations on the item, each with a different tuning mod plugged in. * This requires that bucket specific mods have been validated before. */ export function mapDimItemToProcessItems({ dimItem, armorEnergyRules, modsForSlot, desiredStatRanges, autoStatMods, }: { dimItem: DimItem; armorEnergyRules: ArmorEnergyRules; modsForSlot?: PluggableInventoryItemDefinition[]; desiredStatRanges: DesiredStatRange[]; autoStatMods: boolean; }): ProcessItem[] { const { id, hash, name, isExotic, power, setBonus } = dimItem; const stats = calculateAssumedMasterworkStats(dimItem, armorEnergyRules); const capacity = calculateAssumedItemEnergy(dimItem, armorEnergyRules); const compatibleActivityMod = getSpecialtySocketMetadata(dimItem)?.slotTag; const modsCost = modsForSlot ? sumBy(modsForSlot, (mod) => mod.plug.energyCost?.energyCost ?? 0) : 0; const assumeArtifice = isAssumedArtifice(dimItem, armorEnergyRules); const processItem: ProcessItem = { id, hash, name, isExotic, isArtifice: assumeArtifice, power, stats, remainingEnergyCapacity: capacity - modsCost, compatibleActivityMod: compatibleActivityMod, setBonus: setBonus?.hash, }; const tuningSocket = getArmor3TuningSocket(dimItem); // Make a version of the item for each possible tuning mod that could be applied. if (autoStatMods && tuningSocket?.reusablePlugItems?.length) { const processItems: ProcessItem[] = []; const allPlugs = tuningSocket.plugSet?.plugs; // By default, we'll sacrifice the last ignored stat, or the last from among the lowest maximums const defaultDumpStat = desiredStatRanges.findLast((r) => r.maxStat === 0)?.statHash ?? minBy(Array.from(desiredStatRanges.entries()), ([i, r]) => r.maxStat * 1000 - i)?.[1] .statHash; for (const { plugItemHash, enabled } of tuningSocket.reusablePlugItems) { if (!enabled || emptyPlugHashes.has(plugItemHash)) { continue; } const plug = allPlugs?.find((p) => p.plugDef.hash === plugItemHash); if (plug) { const def = plug.plugDef; if (isPluggableItem(def) && def.investmentStats?.length) { const tunedStats = { ...stats }; let dumpStatHash: StatHashes | undefined = undefined; for (const { statTypeHash, activationRule, value } of mapAndFilterInvestmentStats(def)) { if ( armorStats.includes(statTypeHash) && isPlugStatActive(activationRule, { item: dimItem, statHash: statTypeHash }) ) { tunedStats[statTypeHash] = Math.min(MAX_STAT, tunedStats[statTypeHash] + value); if (value < 0) { dumpStatHash = statTypeHash; } } } const desiredMax = dumpStatHash ? desiredStatRanges.find((r) => r.statHash === dumpStatHash)!.maxStat : 0; // If we are dumping if ( // This is balanced tuning dumpStatHash === undefined || // This is dumping the stat we want to dump (defaultDumpStat && dumpStatHash === defaultDumpStat) || // The maximum is low enough that we might actually want to dump this stat to benefit others (desiredMax > 0 && desiredMax <= 175) ) { processItems.push({ ...processItem, includedTuningMod: def.hash, stats: tunedStats }); } } } } return processItems; } return [processItem]; } export function mapAutoMods(defs: AutoModDefs): AutoModData { const defToAutoMod = (def: PluggableInventoryItemDefinition) => ({ hash: def.hash, cost: def.plug.energyCost?.energyCost ?? 0, }); const defToHash = (def: PluggableInventoryItemDefinition) => def.hash; return { artificeMods: mapValues(defs.artificeMods, defToHash), generalMods: mapValues(defs.generalMods, (modsForStat) => mapValues(modsForStat, defToAutoMod)), }; } /** * Build the automatically pickable mods for the store. * FIXME: Bungie created cheap copies of some mods, but they don't have stats, so * this code will not extract the reduced-cost copies even if they become available. * Re-evaluate this in future seasons if general mods can be affected by artifact cost reductions. */ export function getAutoMods(defs: D2ManifestDefinitions, allUnlockedPlugs: Set) { const autoMods: AutoModDefs = { generalMods: {}, artificeMods: {} }; // Only consider plugs that give stats const mapPlugSet = (plugSetHash: number) => filterMap(defs.PlugSet.get(plugSetHash)?.reusablePlugItems ?? [], (plugEntry) => { const def = defs.InventoryItem.get(plugEntry.plugItemHash); return isPluggableItem(def) && def.investmentStats?.length ? def : undefined; }); const generalPlugSet = mapPlugSet(generalSocketReusablePlugSetHash); const artificePlugSet = mapPlugSet(artificeSocketReusablePlugSetHash); for (const statHash of armorStats) { // Artifice mods give a small boost in a single stat, so find the mod for that stat const artificeMod = artificePlugSet.find((modDef) => modDef.investmentStats.some( (stat) => stat.statTypeHash === statHash && stat.value === artificeStatBoost, ), ); if ( artificeMod && (artificeMod.plug.energyCost === undefined || artificeMod.plug.energyCost.energyCost === 0) ) { autoMods.artificeMods[statHash] = artificeMod; } const findUnlockedModByValue = (value: number) => { const relevantMods = generalPlugSet.filter((def) => def.investmentStats.find((stat) => stat.statTypeHash === statHash && stat.value === value), ); relevantMods.sort(compareBy((def) => -(def.plug.energyCost?.energyCost ?? 0))); const [largeMod, smallMod] = relevantMods; return smallMod && allUnlockedPlugs.has(smallMod.hash) ? smallMod : largeMod; }; const majorMod = findUnlockedModByValue(majorStatBoost); const minorMod = findUnlockedModByValue(minorStatBoost); if (majorMod && minorMod) { autoMods.generalMods[statHash] = { majorMod, minorMod }; } } return autoMods; } ================================================ FILE: src/app/loadout-builder/process/process-wrapper.ts ================================================ import { SetBonusCounts } from '@destinyitemmanager/dim-api-types'; import { DimItem } from 'app/inventory/item-types'; import { ModMap } from 'app/loadout/mod-assignment-utils'; import { armorStats } from 'app/search/d2-known-values'; import { mapValues } from 'app/utils/collections'; import { getMaxParallelCores } from 'app/utils/parallel-cores'; import { proxy, releaseProxy, wrap } from 'comlink'; import { BucketHashes } from 'data/d2/generated-enums'; import { chunk, maxBy } from 'es-toolkit'; import { deepEqual } from 'fast-equals'; import type { ProcessInputs } from '../process-worker/process'; import { HeapSetTracker } from '../process-worker/set-tracker'; import { ProcessArmorSet, ProcessItem, ProcessItemsByBucket, ProcessResult, ProcessStatistics, } from '../process-worker/types'; import { ArmorBucketHash, ArmorEnergyRules, AutoModDefs, DesiredStatRange, ItemsByBucket, ModStatChanges, StatRanges, } from '../types'; import { mapArmor2ModToProcessMod, mapAutoMods, mapDimItemToProcessItems } from './mappers'; interface MappedItem { dimItem: DimItem; processItem: ProcessItem; } function createWorker() { const instance = new Worker( /* webpackChunkName: "lo-worker" */ new URL('../process-worker/ProcessWorker', import.meta.url), { type: 'module' }, ); const worker = wrap(instance); const cleanup = () => { worker[releaseProxy](); instance.terminate(); }; return { worker, cleanup }; } export type RunProcessResult = Omit & { sets: ProcessArmorSet[]; processTime: number; }; export function runProcess({ autoModDefs, filteredItems, setBonuses, lockedModMap, modStatChanges, armorEnergyRules, desiredStatRanges, anyExotic, autoStatMods, strictUpgrades, stopOnFirstSet, lastInput, onProgress, }: { autoModDefs: AutoModDefs; filteredItems: ItemsByBucket; setBonuses: SetBonusCounts; lockedModMap: ModMap; modStatChanges: ModStatChanges; armorEnergyRules: ArmorEnergyRules; desiredStatRanges: DesiredStatRange[]; anyExotic: boolean; autoStatMods: boolean; strictUpgrades: boolean; stopOnFirstSet: boolean; lastInput: ProcessInputs | undefined; onProgress?: (completed: number, total: number) => void; }): | { cleanup: () => void; resultPromise: Promise; input: ProcessInputs; } | undefined { const processStart = performance.now(); const { bucketSpecificMods, activityMods, generalMods } = lockedModMap; const lockedProcessMods = { generalMods: generalMods.map(mapArmor2ModToProcessMod), activityMods: activityMods.map(mapArmor2ModToProcessMod), }; const autoModsData = mapAutoMods(autoModDefs); const processItems: ProcessItemsByBucket = { [BucketHashes.Helmet]: [], [BucketHashes.Gauntlets]: [], [BucketHashes.ChestArmor]: [], [BucketHashes.LegArmor]: [], [BucketHashes.ClassArmor]: [], }; for (const [bucketHashStr, items] of Object.entries(filteredItems)) { const bucketHash = parseInt(bucketHashStr, 10) as ArmorBucketHash; processItems[bucketHash] = []; const mappedItems: MappedItem[] = items.flatMap((dimItem) => mapDimItemToProcessItems({ dimItem, armorEnergyRules, desiredStatRanges, modsForSlot: bucketSpecificMods[bucketHash] || [], autoStatMods, }).map((processItem) => ({ dimItem, processItem, })), ); for (const mappedItem of mappedItems) { processItems[bucketHash].push(mappedItem.processItem); } } const input: ProcessInputs = { filteredItems: processItems, modStatTotals: mapValues(modStatChanges, (stat) => stat.value), lockedMods: lockedProcessMods, setBonuses, desiredStatRanges, anyExotic, autoModOptions: autoModsData, autoStatMods, strictUpgrades, stopOnFirstSet, }; if (deepEqual(lastInput, input)) { // If the inputs are the same as last time, we can skip the worker and just // return the last result. return undefined; } // eslint-disable-next-line @typescript-eslint/no-empty-function let cleanup = () => {}; const workerPromises: Promise[] = []; const numCombinations = Object.values(processItems).reduce( (total, items) => total * Math.max(1, items.length), 1, ); const concurrency = getMaxParallelCores(); const longestItemsBucketHash = Number( maxBy(Object.entries(processItems), ([, items]) => items.length)![0], ); const inputSlices = sliceInputForConcurrency(input, longestItemsBucketHash, concurrency); let progressTotal = 0; for (let i = 0; i < inputSlices.length; i++) { const { worker, cleanup: cleanupWorker } = createWorker(); let cleanupRef: (() => void) | undefined = cleanupWorker; const existingCleanup = cleanup; const cleanupThisWorker = () => { cleanupRef?.(); cleanupRef = undefined; }; cleanup = () => { existingCleanup(); cleanupThisWorker(); }; const input = inputSlices[i]; const handleProgress = proxy((completed: number) => { if (cleanupRef === undefined) { return; } progressTotal += completed; onProgress?.(progressTotal, numCombinations); }); const workerPromise = (async () => { try { return await worker.process(i + 1, input, handleProgress); } finally { cleanupThisWorker(); } })(); workerPromises.push(workerPromise); } return { cleanup, input, resultPromise: Promise.all(workerPromises).then((results) => { const result = combineResults(results); const processTime = performance.now() - processStart; return { ...result, processTime }; }), }; } function combineResults(results: ProcessResult[]): ProcessResult { if (results.length === 1) { return results[0]; } const setTracker = new HeapSetTracker(200); for (const result of results) { for (const set of result.sets) { if (setTracker.couldInsert(set.enabledStatsTotal)) { setTracker.insert(set); } } } const topSets = setTracker.getArmorSets(); const firstResult = results.shift()!; return results.reduce( (combined, result) => ({ sets: topSets, combos: combined.combos + result.combos, statRangesFiltered: combineStatRanges(combined.statRangesFiltered, result.statRangesFiltered), processInfo: combineProcessInfo(combined.processInfo, result.processInfo), }), firstResult, ); } function combineStatRanges(a: StatRanges, b: StatRanges): StatRanges { for (const statHash of armorStats) { const range = a[statHash]; range.maxStat = Math.max(range.maxStat, b[statHash].maxStat); range.minStat = Math.min(range.minStat, b[statHash].minStat); } return a; } function combineProcessInfo(a: ProcessStatistics, b: ProcessStatistics): ProcessStatistics { a.numProcessed += b.numProcessed; a.numValidSets += b.numValidSets; a.statistics.lowerBoundsExceeded.timesChecked += b.statistics.lowerBoundsExceeded.timesChecked; a.statistics.lowerBoundsExceeded.timesFailed += b.statistics.lowerBoundsExceeded.timesFailed; a.statistics.modsStatistics.autoModsPick.timesChecked += b.statistics.modsStatistics.autoModsPick.timesChecked; a.statistics.modsStatistics.autoModsPick.timesFailed += b.statistics.modsStatistics.autoModsPick.timesFailed; a.statistics.modsStatistics.earlyModsCheck.timesChecked += b.statistics.modsStatistics.earlyModsCheck.timesChecked; a.statistics.modsStatistics.earlyModsCheck.timesFailed += b.statistics.modsStatistics.earlyModsCheck.timesFailed; a.statistics.modsStatistics.finalAssignment.autoModsAssignmentFailed += b.statistics.modsStatistics.finalAssignment.autoModsAssignmentFailed; a.statistics.modsStatistics.finalAssignment.modAssignmentAttempted += b.statistics.modsStatistics.finalAssignment.modAssignmentAttempted; a.statistics.modsStatistics.finalAssignment.modsAssignmentFailed += b.statistics.modsStatistics.finalAssignment.modsAssignmentFailed; a.statistics.skipReasons.doubleExotic += b.statistics.skipReasons.doubleExotic; a.statistics.skipReasons.insufficientSetBonus += b.statistics.skipReasons.insufficientSetBonus; a.statistics.skipReasons.noExotic += b.statistics.skipReasons.noExotic; a.statistics.skipReasons.skippedLowTier += b.statistics.skipReasons.skippedLowTier; return a; } function sliceInputForConcurrency( input: ProcessInputs, longestItemsBucketHash: number, concurrency: number, ) { if (concurrency <= 1) { return [input]; } const itemsToSlice = input.filteredItems[longestItemsBucketHash as ArmorBucketHash]; if (itemsToSlice.length <= 1) { return [input]; } const sliceSize = Math.ceil(itemsToSlice.length / concurrency); return chunk(itemsToSlice, sliceSize).map((itemsSlice) => ({ ...input, filteredItems: { ...input.filteredItems, [longestItemsBucketHash]: itemsSlice, }, })); } ================================================ FILE: src/app/loadout-builder/process/useProcess.ts ================================================ import { SetBonusCounts } from '@destinyitemmanager/dim-api-types'; import { PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { unlockedPlugSetItemsSelector } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { ModMap } from 'app/loadout/mod-assignment-utils'; import { useD2Definitions } from 'app/manifest/selectors'; import { infoLog } from 'app/utils/log'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import type { ProcessInputs } from '../process-worker/process'; import { ProcessArmorSet, ProcessStatistics } from '../process-worker/types'; import { ArmorEnergyRules, DesiredStatRange, ItemsByBucket, ModStatChanges, StatRanges, } from '../types'; import { getAutoMods } from './mappers'; import { runProcess } from './process-wrapper'; interface ProcessState { processing: boolean; startTime: number; resultStoreId: string; result: { sets: ProcessArmorSet[]; /** * The mods and rules used to generate the sets above. The sets * are guaranteed (modulo bugs in worker) to fit these mods given * these settings, so set rendering must use these to render sets. * Otherwise set rendering may render old sets with new settings/mods, * which will fail in ways indistinguishable from legitimate mismatches. */ mods: PluggableInventoryItemDefinition[]; armorEnergyRules: ArmorEnergyRules; modStatChanges: ModStatChanges; combos: number; processTime: number; statRangesFiltered?: StatRanges; // What the actual process did to remove some sets. processInfo: ProcessStatistics | undefined; } | null; totalCombos: number; completedCombos: number; } let lastProgress = 0; /** * Hook to process all the stat groups for LO in a web worker. */ export function useProcess({ selectedStore, filteredItems, lockedModMap, setBonuses, modStatChanges, armorEnergyRules, desiredStatRanges, anyExotic, autoStatMods, strictUpgrades, }: { selectedStore: DimStore; filteredItems: ItemsByBucket; lockedModMap: ModMap; setBonuses: SetBonusCounts; modStatChanges: ModStatChanges; armorEnergyRules: ArmorEnergyRules; desiredStatRanges: DesiredStatRange[]; anyExotic: boolean; autoStatMods: boolean; strictUpgrades: boolean; }) { const [{ result, processing, totalCombos, completedCombos, startTime, resultStoreId }, setState] = useState({ processing: false, startTime: 0, resultStoreId: selectedStore.id, result: null, totalCombos: 0, completedCombos: 0, }); const autoModDefs = useAutoMods(selectedStore.id); const firstTime = result === null; // Normally we'd just use the cleanup function in the main useEffect, but we // want to be able to short circuit updates without killing in-progress // processes. const cleanupRef = useRef<() => void>(undefined); useEffect( () => () => { // Cleanup the previous process if it exists cleanupRef.current?.(); cleanupRef.current = undefined; }, [], ); // This allows for some memoization of the inputs to the worker const inputsRef = useRef(undefined); useEffect(() => { const doProcess = async () => { const handleProgress = (completed: number, total: number) => { const now = Date.now(); // Save some UI recomputation cycles and prevent flickering, by updating the progress display at most every half second if (now - lastProgress > 500 || total === completed) { setState((state) => ({ ...state, totalCombos: total, completedCombos: completed, })); lastProgress = now; } }; const processInfo = runProcess({ autoModDefs, filteredItems, lockedModMap, setBonuses, modStatChanges, armorEnergyRules, desiredStatRanges, anyExotic, autoStatMods, stopOnFirstSet: false, strictUpgrades, lastInput: inputsRef.current, onProgress: handleProgress, }); if (processInfo === undefined) { infoLog('loadout optimizer', 'Inputs were equal to the previous run, not recalculating'); return; } const { cleanup, resultPromise, input } = processInfo; cleanupRef.current?.(); cleanupRef.current = cleanup; inputsRef.current = input; setState((state) => ({ processing: true, startTime: Date.now(), resultStoreId: selectedStore.id, result: selectedStore.id === state.resultStoreId ? state.result : null, totalCombos: 0, completedCombos: 0, })); try { const { sets, combos, statRangesFiltered, processInfo, processTime } = await resultPromise; setState((oldState) => ({ ...oldState, processing: false, result: { sets, mods: lockedModMap.allMods, armorEnergyRules, modStatChanges, combos, processTime, statRangesFiltered, processInfo, }, })); } finally { cleanup(); cleanupRef.current = undefined; } }; const timer = setTimeout( () => { doProcess(); }, firstTime ? 0 : 500, ); return () => { clearTimeout(timer); }; }, [ filteredItems, selectedStore.id, desiredStatRanges, anyExotic, armorEnergyRules, autoStatMods, lockedModMap, setBonuses, modStatChanges, autoModDefs, strictUpgrades, firstTime, ]); return { result: resultStoreId === selectedStore.id ? result : null, processing, startTime, totalCombos, completedCombos, }; } /** * Compute information about the mods LO could automatically assign. */ export function useAutoMods(storeId: string) { const defs = useD2Definitions()!; const unlockedPlugs = useSelector(unlockedPlugSetItemsSelector(storeId)); return useMemo(() => getAutoMods(defs, unlockedPlugs), [defs, unlockedPlugs]); } ================================================ FILE: src/app/loadout-builder/process-worker/ProcessWorker.ts ================================================ import { expose } from 'comlink'; import { process } from './process'; const exports = { process, }; export type ProcessWorker = typeof exports; expose(exports); ================================================ FILE: src/app/loadout-builder/process-worker/__snapshots__/auto-stat-mod-utils.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`process-utils auto mod structure different ways of hitting target stats with 0 remaining general mods (using artifice mods: true) 1`] = ` { "1": 1, "10": 1, "11": 1, "12": 1, "13": 1, "14": 1, "15": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1, "8": 1, "9": 1, } `; exports[`process-utils auto mod structure different ways of hitting target stats with 1 remaining general mods (using artifice mods: true) 1`] = ` { "1": 2, "10": 3, "11": 3, "12": 3, "13": 3, "14": 3, "15": 3, "16": 2, "17": 2, "18": 2, "19": 2, "2": 2, "20": 2, "21": 1, "22": 1, "23": 1, "24": 1, "25": 1, "3": 2, "4": 2, "5": 2, "6": 3, "7": 3, "8": 3, "9": 3, } `; exports[`process-utils auto mod structure different ways of hitting target stats with 3 remaining general mods (using artifice mods: false) 1`] = ` { "1": 1, "10": 2, "11": 2, "12": 2, "13": 2, "14": 2, "15": 2, "16": 2, "17": 2, "18": 2, "19": 2, "2": 1, "20": 2, "21": 1, "22": 1, "23": 1, "24": 1, "25": 1, "26": 1, "27": 1, "28": 1, "29": 1, "3": 1, "30": 1, "4": 1, "5": 1, "6": 2, "7": 2, "8": 2, "9": 2, } `; exports[`process-utils auto mod structure different ways of hitting target stats with 5 remaining general mods (using artifice mods: false) 1`] = ` { "1": 1, "10": 2, "11": 2, "12": 2, "13": 2, "14": 2, "15": 2, "16": 3, "17": 3, "18": 3, "19": 3, "2": 1, "20": 3, "21": 3, "22": 3, "23": 3, "24": 3, "25": 3, "26": 3, "27": 3, "28": 3, "29": 3, "3": 1, "30": 3, "31": 2, "32": 2, "33": 2, "34": 2, "35": 2, "36": 2, "37": 2, "38": 2, "39": 2, "4": 1, "40": 2, "41": 1, "42": 1, "43": 1, "44": 1, "45": 1, "46": 1, "47": 1, "48": 1, "49": 1, "5": 1, "50": 1, "6": 2, "7": 2, "8": 2, "9": 2, } `; exports[`process-utils auto mod structure different ways of hitting target stats with 5 remaining general mods (using artifice mods: true) 1`] = ` { "1": 2, "10": 4, "11": 6, "12": 6, "13": 6, "14": 6, "15": 6, "16": 8, "17": 8, "18": 8, "19": 8, "2": 2, "20": 8, "21": 10, "22": 10, "23": 10, "24": 10, "25": 10, "26": 11, "27": 11, "28": 11, "29": 11, "3": 2, "30": 11, "31": 11, "32": 11, "33": 11, "34": 11, "35": 11, "36": 10, "37": 10, "38": 10, "39": 10, "4": 2, "40": 10, "41": 8, "42": 8, "43": 8, "44": 8, "45": 8, "46": 6, "47": 6, "48": 6, "49": 6, "5": 2, "50": 6, "51": 4, "52": 4, "53": 4, "54": 4, "55": 4, "56": 2, "57": 2, "58": 2, "59": 2, "6": 4, "60": 2, "61": 1, "62": 1, "63": 1, "64": 1, "65": 1, "7": 4, "8": 4, "9": 4, } `; exports[`process-utils auto mod structure snapshot of mod defs when assuming cheapgeneral for auto mods 1`] = ` { "artificeMods": { "144602215": 3160845295, "1735777505": 617569843, "1943323491": 539459624, "2996146975": 2322202118, "392767087": 199176566, "4244567218": 2507624050, }, "generalMods": { "144602215": { "majorMod": { "cost": 3, "hash": 2724608735, }, "minorMod": { "cost": 1, "hash": 350061697, }, }, "1735777505": { "majorMod": { "cost": 3, "hash": 1435557120, }, "minorMod": { "cost": 1, "hash": 4021790309, }, }, "1943323491": { "majorMod": { "cost": 3, "hash": 4204488676, }, "minorMod": { "cost": 1, "hash": 1237786518, }, }, "2996146975": { "majorMod": { "cost": 3, "hash": 4183296050, }, "minorMod": { "cost": 1, "hash": 1703647492, }, }, "392767087": { "majorMod": { "cost": 3, "hash": 1180408010, }, "minorMod": { "cost": 1, "hash": 2532323436, }, }, "4244567218": { "majorMod": { "cost": 3, "hash": 4287799666, }, "minorMod": { "cost": 1, "hash": 2639422088, }, }, }, } `; exports[`process-utils auto mod structure snapshot of mod defs when assuming general for auto mods 1`] = ` { "artificeMods": { "144602215": 3160845295, "1735777505": 617569843, "1943323491": 539459624, "2996146975": 2322202118, "392767087": 199176566, "4244567218": 2507624050, }, "generalMods": { "144602215": { "majorMod": { "cost": 3, "hash": 2724608735, }, "minorMod": { "cost": 1, "hash": 350061697, }, }, "1735777505": { "majorMod": { "cost": 3, "hash": 1435557120, }, "minorMod": { "cost": 1, "hash": 4021790309, }, }, "1943323491": { "majorMod": { "cost": 3, "hash": 4204488676, }, "minorMod": { "cost": 1, "hash": 1237786518, }, }, "2996146975": { "majorMod": { "cost": 3, "hash": 4183296050, }, "minorMod": { "cost": 1, "hash": 1703647492, }, }, "392767087": { "majorMod": { "cost": 3, "hash": 1180408010, }, "minorMod": { "cost": 1, "hash": 2532323436, }, }, "4244567218": { "majorMod": { "cost": 3, "hash": 4287799666, }, "minorMod": { "cost": 1, "hash": 2639422088, }, }, }, } `; ================================================ FILE: src/app/loadout-builder/process-worker/__snapshots__/process-utils.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`process-utils auto mods the problem is solvable 1`] = ` [ { "exactStatPoints": 6, "generalModsCosts": [], "modEnergyCost": 0, "modHashes": [ 199176566, 199176566, ], "numArtificeMods": 2, "numGeneralMods": 0, "targetStatIndex": 0, }, { "exactStatPoints": 10, "generalModsCosts": [ 3, ], "modEnergyCost": 3, "modHashes": [ 1435557120, ], "numArtificeMods": 0, "numGeneralMods": 1, "targetStatIndex": 2, }, { "exactStatPoints": 13, "generalModsCosts": [ 3, ], "modEnergyCost": 3, "modHashes": [ 2724608735, 3160845295, ], "numArtificeMods": 1, "numGeneralMods": 1, "targetStatIndex": 3, }, { "exactStatPoints": 5, "generalModsCosts": [ 1, ], "modEnergyCost": 1, "modHashes": [ 1237786518, ], "numArtificeMods": 0, "numGeneralMods": 1, "targetStatIndex": 4, }, ] `; ================================================ FILE: src/app/loadout-builder/process-worker/auto-stat-mod-utils.test.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { armorStats } from 'app/search/d2-known-values'; import { mapValues } from 'app/utils/collections'; import { emptySet } from 'app/utils/empty'; import { getTestDefinitions } from 'testing/test-utils'; import { precalculateStructures } from '../process-worker/process-utils'; import { ProcessMod } from '../process-worker/types'; import { getAutoMods, mapAutoMods } from '../process/mappers'; import { generalSocketReusablePlugSetHash } from '../types'; // The tsconfig in the process worker folder messes with tests so they live outside of it. describe('process-utils auto mod structure', () => { let defs: D2ManifestDefinitions; beforeAll(async () => { defs = await getTestDefinitions(); }); const generalMods: ProcessMod[] = [ { hash: 7, energyCost: 3 }, { hash: 8, energyCost: 5 }, { hash: 9, energyCost: 2 }, { hash: 10, energyCost: 1 }, { hash: 11, energyCost: 2 }, ]; test.each(['general', 'cheapgeneral'] as const)( 'snapshot of mod defs when assuming %s for auto mods', (n) => { const unlockedPlugs = n === 'cheapgeneral' ? new Set([ ...defs.PlugSet.get(generalSocketReusablePlugSetHash).reusablePlugItems.map( (entry) => entry.plugItemHash, ), ]) : emptySet(); const autoModData = mapAutoMods(getAutoMods(defs, unlockedPlugs)); expect(autoModData).toMatchSnapshot(); }, ); test.each([ [5, false], [3, false], [1, true], [0, true], [5, true], ] as const)( 'different ways of hitting target stats with %s remaining general mods (using artifice mods: %s)', (numGeneralMods, useArtificeMods) => { const unlockedPlugs = emptySet(); const autoModData = mapAutoMods(getAutoMods(defs, unlockedPlugs)); if (!useArtificeMods) { autoModData.artificeMods = {}; } const sessionInfo = precalculateStructures( autoModData, generalMods.slice(0, 5 - numGeneralMods), [], true, armorStats, ); const waysOfHittingStat = mapValues(sessionInfo.autoModOptions[3], (y) => y?.length); // Things to watch out for in the snapshot: Keys are contiguous, values first ascend // to around the halfway point before descending in a vaguely binomial coefficient-like fashion expect(waysOfHittingStat).toMatchSnapshot(); }, ); }); ================================================ FILE: src/app/loadout-builder/process-worker/auto-stat-mod-utils.ts ================================================ import { chainComparator, compareBy } from 'app/utils/comparators'; import { objectValues } from 'app/utils/util-types'; import { ArmorStatHashes, artificeStatBoost, majorStatBoost, minorStatBoost } from '../types'; import { LoSessionInfo } from './process-utils'; import { AutoModData } from './types'; /** * A particular way of achieving a target stat value (for a single stat). */ export interface ModsPick { /** The number of artifice mods this pick contains. */ numArtificeMods: number; /** The number of general mods this pick contains. */ numGeneralMods: number; /** The cost of the general mods this pick contains, sorted descending. */ // TODO: Could be left out and calculated on-demand from AutoModData.generalMods generalModsCosts: number[]; /** General + artifice mod hashes */ // TODO: Could be left out and calculated on-demand from AutoModData.generalMods modHashes: number[]; /** Sum of generalModCosts */ modEnergyCost: number; /** Which stat this set of mods targets */ targetStatIndex: number; /** The exact number of points this set of mods provides (if we ask for 1 stat point, an artifice mod might give 3) */ exactStatPoints: number; } /** * Precalculated ways of hitting all possible stat boost values for a single stat. */ export interface CacheForStat { [targetStatValue: number]: ModsPick[] | undefined; } /** * Precalculated ways of hitting stat values, separated by stat hash. */ export interface AutoModsCache { [targetStatIndex: number]: CacheForStat; } /** * Pick auto mods (general mods and artifice mods) that provide at least the * given `neededStats` in `statOrder`. */ // TODO: This will be complicated by tuning mods, maybe? // // TODO: In the tierless world, I think we can just greedily pick mods (or use a // knapsack algorithm/constraint solver) because we don't need to worry about // stats not counting because they don't hit a tier. And even then we're really // just talking about whether to slot a +5 or +10 mod into each empty space. In // fact, we can probably just figure out the maximum number of +5 and +10 mods // we can slot in anywhere, and then assign those to stats in order of priority. export function chooseAutoMods( info: LoSessionInfo, neededStats: number[], numArtificeMods: number, remainingEnergyCapacities: number[][], remainingTotalEnergy: number, ): ModsPick[] | undefined { return recursivelyChooseMods( info.autoModOptions, info.generalModCosts, neededStats, 0, info.numAvailableGeneralMods, numArtificeMods, remainingEnergyCapacities, remainingTotalEnergy, undefined, ); } /** * Given a set of general mods we want to slot, and a list of remaining-energy possibilities, * check if the general mods fit into any of the remaining energy possibilities. */ function doGeneralModsFit( /** The cost of user-picked general mods, sorted descending. */ generalModCosts: number[], /** variants of remaining energy capacities given our activity mod assignment, each sorted descending */ remainingEnergyCapacities: number[][], pickedMods: ModsPick[] | undefined, ) { // eslint-disable-next-line @typescript-eslint/prefer-optional-chain if (pickedMods !== undefined && pickedMods.length) { generalModCosts = generalModCosts.slice(); // Intentionally open-coded for performance for (let i = 0; i < pickedMods.length; i++) { generalModCosts.push(...pickedMods[i].generalModsCosts); } generalModCosts.sort((a, b) => b - a); } return remainingEnergyCapacities.some((capacities) => { capacities.sort((a, b) => b - a); return generalModCosts.every((cost, index) => cost <= capacities[index]); }); } /** * Find a combination of artifice and general mods that can * help hit the `neededStats` starting from `statIndex` by recursively * enumerating all combinations. * `pickedMods` contains the mods chosen for earlier stats. */ function recursivelyChooseMods( autoModOptions: AutoModsCache, /** The cost of user-picked general mods, sorted descending. */ generalModCosts: number[], /** Incremental stat increases we need to hit. */ neededStats: number[], statIndex: number, remainingGeneralSlots: number, remainingArtificeSlots: number, /** variants of remaining energy capacities given our activity mod assignment, each sorted descending */ remainingEnergyCapacities: number[][], remainingTotalEnergy: number, pickedMods: ModsPick[] | undefined, ): ModsPick[] | undefined { // Skip over stats that we don't need to increase. while (statIndex < neededStats.length && neededStats[statIndex] === 0) { statIndex++; } if (statIndex === neededStats.length) { // We've hit the end of our needed stats, check if this is possible if (doGeneralModsFit(generalModCosts, remainingEnergyCapacities, pickedMods)) { return pickedMods ?? []; } else { return undefined; } } // Get a list of different mod combinations that could hit the needed stat boost for this stat. const possiblePicks = autoModOptions[statIndex][neededStats[statIndex]]; if (!possiblePicks) { // we can't possibly hit our target stats return undefined; } // Create a new array we append the pick for this stat to. const subArray = pickedMods !== undefined ? pickedMods.slice() : []; // Dummy value just so we don't repeatedly push and pop. subArray.push(subArray[0]); for (const pick of possiblePicks) { if ( pick.numArtificeMods > remainingArtificeSlots || pick.numGeneralMods > remainingGeneralSlots || pick.modEnergyCost > remainingTotalEnergy ) { continue; } subArray[subArray.length - 1] = pick; const solution = recursivelyChooseMods( autoModOptions, generalModCosts, neededStats, statIndex + 1, remainingGeneralSlots - pick.numGeneralMods, remainingArtificeSlots - pick.numArtificeMods, remainingEnergyCapacities, remainingTotalEnergy - pick.modEnergyCost, subArray, ); if (solution) { return solution; } } } /** * Previously we could use a simple algorithm to come up with all mod combinations to hit certain target stats * based on simple "mod splitting": Since minor and major mods give 5 and 10 respectively (common divisor 5), * we only needed to care about stat multiples of 5. And we could just start with +10 mods and derive variants by * splitting +10 mods into +5 mods. With stats ranging from 0 to 50 (11 values) and 6 stats, * this would end up with about 700,000 combinations that could be computed and cached on-demand. * * However, now we need to take care of the +3 artifice mods, and 3 is coprime with 5 and 10. So first of all, * it's a lot more difficult to come up with the pick variants that could hit certain stats, and even if we did, * we cannot efficiently cache the results since every single point matters and cache entries for 50^6 values would * mean our cache would simply explode. So instead we separate the caches by stat and then piece together the mod * picks when actually looking at sets. * * This unfortunately means a lot of `flatMap`ing down the road and is a lot less efficient. Improvements here * could make things a bit faster, especially when they remove equivalent combinations. */ function buildCacheForStat( autoModOptions: AutoModData, statHash: ArmorStatHashes, statIndex: number, availableGeneralStatMods: number, ) { const cache: CacheForStat = {}; // Note: All of these could be undefined for whatever reason. // In that case, the loop bounds are 0 <= numMods <= 0. // Major and minor mod always exist together or not at all. const artificeMod = autoModOptions.artificeMods[statHash]; const minorMod = autoModOptions.generalMods[statHash]?.minorMod; const majorMod = autoModOptions.generalMods[statHash]?.majorMod; // TODO: Pull up the costs here for (let numArtificeMods = 0; numArtificeMods <= (artificeMod ? 5 : 0); numArtificeMods++) { for ( let numMinorMods = 0; numMinorMods <= (minorMod ? availableGeneralStatMods : 0); numMinorMods++ ) { for ( let numMajorMods = 0; numMajorMods <= (majorMod ? availableGeneralStatMods - numMinorMods : 0); numMajorMods++ ) { const statValue = numArtificeMods * artificeStatBoost + numMinorMods * minorStatBoost + numMajorMods * majorStatBoost; if (statValue === 0) { continue; } // We are allowed to provide more stat points than needed -- within reason. // If we have a major mod, this satisfies stat needs of 6,7,8,9,10 // 5 can be satisfied by a strictly better pick that includes only a minor mod // If we have a major mod and an artifice mod, this satisfies 11,12,13. // 10 can be satisfied by dropping the artifice mod. // So if we have any artifice pieces, we are allowed to overshoot by 2, and if // not then we're allowed to overshoot by 4. // This ensures pareto-optimality of the various ways of hitting a stat target. // Note: Assumptions here are artificeStatBoost < minorStatBoost // and majorStatBoost = 2 * minorStatBoost const lowerRange = statValue - (numArtificeMods > 0 ? artificeStatBoost - 1 : minorStatBoost - 1); const obj: ModsPick = { numArtificeMods, numGeneralMods: numMinorMods + numMajorMods, generalModsCosts: [ ...Array(numMajorMods).fill(majorMod?.cost ?? 0), ...Array(numMinorMods).fill(minorMod?.cost ?? 0), ], modHashes: [ ...Array(numMajorMods).fill(majorMod?.hash ?? 0), ...Array(numMinorMods).fill(minorMod?.hash ?? 0), ...Array(numArtificeMods).fill(artificeMod ?? 0), ], modEnergyCost: numMinorMods * (minorMod?.cost ?? 0) + numMajorMods * (majorMod?.cost ?? 0), targetStatIndex: statIndex, exactStatPoints: statValue, }; for (let achievableValue = lowerRange; achievableValue <= statValue; achievableValue++) { (cache[achievableValue] ??= []).push(obj); } } } } // Prefer picks that use artifice mods, since they are free. for (const pickArray of objectValues(cache)) { pickArray!.sort( chainComparator( compareBy((pick) => -pick.numArtificeMods), compareBy((pick) => -pick.numGeneralMods), ), ); } return cache; } export function buildAutoModsMap( autoModOptions: AutoModData, availableGeneralStatMods: number, statOrder: ArmorStatHashes[], ): AutoModsCache { return Object.fromEntries( statOrder.map((statHash, statIndex) => [ statIndex, buildCacheForStat(autoModOptions, statHash, statIndex, availableGeneralStatMods), ]), ); } ================================================ FILE: src/app/loadout-builder/process-worker/process-utils.test.ts ================================================ import { AssumeArmorMasterwork } from '@destinyitemmanager/dim-api-types'; import { PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { MAX_STAT } from 'app/loadout/known-values'; import { armorStats } from 'app/search/d2-known-values'; import { emptySet } from 'app/utils/empty'; import { StatHashes } from 'data/d2/generated-enums'; import { classStatModHash, enhancedOperatorAugmentModHash, isArmor2Arms, isArmor2Chest, isArmor2ClassItem, isArmor2Helmet, isArmor2Legs, } from 'testing/test-item-utils'; import { getTestDefinitions, getTestStores } from 'testing/test-utils'; import { getAutoMods, mapArmor2ModToProcessMod, mapAutoMods, mapDimItemToProcessItems, } from '../process/mappers'; import { ArmorStatHashes, DesiredStatRange, MIN_LO_ITEM_ENERGY, MinMaxStat, ResolvedStatConstraint, } from '../types'; import { LoSessionInfo, generateProcessModPermutations, pickAndAssignSlotIndependentMods, pickOptimalStatMods, precalculateStructures, updateMaxStats, } from './process-utils'; import { AutoModData, ModAssignmentStatistics, ProcessItem, ProcessMod } from './types'; // We don't really pay attention to this in the tests but the parameter is needed const modStatistics: ModAssignmentStatistics = { earlyModsCheck: { timesChecked: 0, timesFailed: 0 }, autoModsPick: { timesChecked: 0, timesFailed: 0 }, finalAssignment: { modAssignmentAttempted: 0, modsAssignmentFailed: 0, autoModsAssignmentFailed: 0, }, }; function modifyMod({ mod, energyCost, tag, }: { mod: ProcessMod; energyCost?: number; tag?: string | null; }) { const newMod = { ...mod }; if (energyCost !== undefined) { newMod.energyCost = energyCost; } if (tag !== undefined) { newMod.tag = tag !== null ? tag : undefined; } return newMod; } function modifyItem({ item, remainingEnergyCapacity, compatibleActivityMod, isArtifice, }: { item: ProcessItem; remainingEnergyCapacity?: number; compatibleActivityMod?: string; isArtifice?: boolean; }) { const newItem = { ...item }; if (remainingEnergyCapacity !== undefined) { newItem.remainingEnergyCapacity = remainingEnergyCapacity; } if (compatibleActivityMod !== undefined) { newItem.compatibleActivityMod = compatibleActivityMod; } if (isArtifice !== undefined) { newItem.isArtifice = isArtifice; } return newItem; } describe('process-utils mod assignment', () => { let generalMod: ProcessMod; let activityMod: ProcessMod; let helmet: ProcessItem; let arms: ProcessItem; let chest: ProcessItem; let legs: ProcessItem; let classItem: ProcessItem; // use these for testing as they are reset after each test let items: ProcessItem[]; let generalMods: ProcessMod[]; let activityMods: ProcessMod[]; const armorEnergyRules = { assumeArmorMasterwork: AssumeArmorMasterwork.None, minItemEnergy: MIN_LO_ITEM_ENERGY, }; beforeAll(async () => { const [defs, stores] = await Promise.all([getTestDefinitions(), getTestStores()]); for (const store of stores) { for (const storeItem of store.items) { if (!helmet && isArmor2Helmet(storeItem)) { helmet = mapDimItemToProcessItems({ dimItem: storeItem, armorEnergyRules, desiredStatRanges: [], autoStatMods: true, })[0]; } if (!arms && isArmor2Arms(storeItem)) { arms = mapDimItemToProcessItems({ dimItem: storeItem, armorEnergyRules, desiredStatRanges: [], autoStatMods: true, })[0]; } if (!chest && isArmor2Chest(storeItem)) { chest = mapDimItemToProcessItems({ dimItem: storeItem, armorEnergyRules, desiredStatRanges: [], autoStatMods: true, })[0]; } if (!legs && isArmor2Legs(storeItem)) { legs = mapDimItemToProcessItems({ dimItem: storeItem, armorEnergyRules, desiredStatRanges: [], autoStatMods: true, })[0]; } if (!classItem && isArmor2ClassItem(storeItem)) { classItem = mapDimItemToProcessItems({ dimItem: storeItem, armorEnergyRules, desiredStatRanges: [], autoStatMods: true, })[0]; } if (helmet && arms && chest && legs && classItem) { break; } } } generalMod = mapArmor2ModToProcessMod( defs.InventoryItem.get(classStatModHash) as PluggableInventoryItemDefinition, ); activityMod = mapArmor2ModToProcessMod( defs.InventoryItem.get(enhancedOperatorAugmentModHash) as PluggableInventoryItemDefinition, ); items = [helmet, arms, chest, legs, classItem]; generalMods = [generalMod, generalMod, generalMod, generalMod, generalMod]; activityMods = [activityMod, activityMod, activityMod, activityMod, activityMod]; }); const canTakeSlotIndependentMods = ( generalMods: ProcessMod[], activityMods: ProcessMod[], items: ProcessItem[], ) => { const autoMods = { generalMods: {}, artificeMods: {} }; const neededStats = [0, 0, 0, 0, 0, 0]; const precalculatedInfo = precalculateStructures( autoMods, generalMods, activityMods, false, armorStats, ); return ( pickAndAssignSlotIndependentMods(precalculatedInfo, modStatistics, items, neededStats, 0) !== undefined ); }; // Answers are derived as permutations of multisets // e.g. for energy levels [1, 2, 1, 2, 1] we have 3 1's and 2 2's. The formula for the // correct number of permutations is 5!/(3!2!) = 120/(6 * 2) = 10 // for [1, 2, 3, 1, 2] we have 5!/(2!2!1!) = 120/(2 * 2) = 30 test.each([ [1, 1], [2, 10], [3, 30], [4, 60], [5, 120], ])('generates the correct number of permutations for %i unique mods', (n, result) => { const mods = generalMods.map((mod, i) => modifyMod({ mod, energyCost: i % n })); expect(generateProcessModPermutations(mods)).toHaveLength(result); }); it('can fit all mods when there are no mods', () => { expect(canTakeSlotIndependentMods([], [], items)).toBe(true); }); it('can fit five general mods', () => { const modifiedItems = items.map((item) => modifyItem({ item, remainingEnergyCapacity: generalMod.energyCost }), ); expect(canTakeSlotIndependentMods(generalMods, [], modifiedItems)).toBe(true); }); test.each([0, 1, 2, 3, 4])( 'it can fit a general mod into a single item at index %i', (itemIndex) => { const modifiedItems = items.map((item, i) => modifyItem({ item, remainingEnergyCapacity: itemIndex === i ? generalMod.energyCost : 0, }), ); expect(canTakeSlotIndependentMods([], [], modifiedItems)).toBe(true); }, ); test.each([ ['can', 'deepstonecrypt'], ["can't", 'not-a-tag'], ])('it %s fit five activity mods', (canFit, tag) => { const modifiedItems = items.map((item) => modifyItem({ item, remainingEnergyCapacity: activityMod.energyCost, compatibleActivityMod: tag, }), ); // sanity check expect(canTakeSlotIndependentMods([], activityMods, modifiedItems)).toBe(canFit === 'can'); }); test.each([0, 1, 2, 3, 4])( 'it can fit a activity mod into a single item at index %i', (itemIndex) => { const modifiedItems = items.map((item, i) => modifyItem({ item, remainingEnergyCapacity: 2, compatibleActivityMod: i === itemIndex ? activityMod.tag! : undefined, }), ); expect(canTakeSlotIndependentMods([], [activityMod], modifiedItems)).toBe(true); }, ); it('can fit general, activity, and combat mods if there is enough energy', () => { const modifiedItems: ProcessItem[] = [...items]; modifiedItems[4] = modifyItem({ item: modifiedItems[4], remainingEnergyCapacity: 6, compatibleActivityMod: activityMod.tag!, }); const modifiedGeneralMod = modifyMod({ mod: generalMod, energyCost: 3, }); const modifiedActivityMod = modifyMod({ mod: activityMod, energyCost: 3, }); expect( canTakeSlotIndependentMods([modifiedGeneralMod], [modifiedActivityMod], modifiedItems), ).toBe(true); }); it("can't fit general, activity, and combat mods if there isn't enough energy", () => { const modifiedItems: ProcessItem[] = [...items]; modifiedItems[4] = modifyItem({ item: modifiedItems[4], remainingEnergyCapacity: 1, compatibleActivityMod: activityMod.tag!, }); const modifiedGeneralMod = modifyMod({ mod: generalMod, energyCost: 3, }); const modifiedActivityMod = modifyMod({ mod: activityMod, energyCost: 3, }); expect( canTakeSlotIndependentMods([modifiedGeneralMod], [modifiedActivityMod], modifiedItems), ).toBe(false); }); test.each(['general', 'combat', 'activity'])( "can't fit mods if %s mods have too much energy", (modType) => { const modifiedItems: ProcessItem[] = [...items]; modifiedItems[4] = modifyItem({ item: modifiedItems[4], remainingEnergyCapacity: 1, compatibleActivityMod: activityMod.tag!, }); const modifiedGeneralMod = modifyMod({ mod: generalMod, energyCost: modType === 'general' ? 6 : 5, }); const modifiedActivityMod = modifyMod({ mod: activityMod, energyCost: modType === 'activity' ? 6 : 5, }); expect( canTakeSlotIndependentMods([modifiedGeneralMod], [modifiedActivityMod], modifiedItems), ).toBe(false); }, ); }); /** * To test auto mod picks to hit certain stats, we set up some constraints that give us one solution, * and then constrain the problem some more and expect no solution. * * Our constraints/picked mods+stats are: * * The user picked two general mods (cost 4 and 3) and one activity mod (cost 1). * * We have 4 artifice slots, and 3 remaining general mod slots. * * We need 4 mobility, 0 resilience, 10 recovery, 12 discipline, 4 intellect, 0 strength. * * Our armor pieces have [3, 4, 1, 3, 4] energy left * * the activity pieces are ^ ^ * (one of them is a trap; the 4-cost piece must hold one of the 4-cost general mods and can't hold the activity mod) * * The expected solution uses 4 artifice discipline mods, a 4 cost major recovery mod, a 1 cost small mobility mod and a 2 cost small intellect mod. * The activity mod goes into the 3-energy piece for the mods to fit. */ describe('process-utils auto mods', () => { let generalMod: ProcessMod; let generalModCopy: ProcessMod; let activityMod: ProcessMod; let helmet: ProcessItem; let arms: ProcessItem; let chest: ProcessItem; let legs: ProcessItem; let classItem: ProcessItem; // use these for testing as they are reset after each test let items: ProcessItem[]; let generalMods: ProcessMod[]; let activityMods: ProcessMod[]; let loSessionInfo: LoSessionInfo; let neededStats: number[]; beforeAll(async () => { const defs = await getTestDefinitions(); const makeItem = ( artifice: boolean, index: number, energyCapacity: number, season: string | undefined, ): ProcessItem => ({ hash: index, id: index.toString(), isArtifice: artifice, isExotic: false, name: `Item ${index}`, power: 1500, stats: [0, 0, 0, 0, 0, 0], compatibleActivityMod: season, remainingEnergyCapacity: energyCapacity, }); helmet = makeItem(true, 1, 3, undefined); arms = makeItem(true, 2, 4, 'deepstonecrypt'); chest = makeItem(false, 3, 1, undefined); legs = makeItem(true, 4, 3, 'deepstonecrypt'); classItem = makeItem(true, 5, 4, undefined); generalMod = mapArmor2ModToProcessMod( defs.InventoryItem.get(classStatModHash) as PluggableInventoryItemDefinition, ); generalMod.energyCost = 4; generalModCopy = { ...generalMod, energyCost: 3 }; activityMod = mapArmor2ModToProcessMod( defs.InventoryItem.get(enhancedOperatorAugmentModHash) as PluggableInventoryItemDefinition, ); activityMod.energyCost = 1; items = [helmet, arms, chest, legs, classItem]; generalMods = [generalModCopy, generalMod]; activityMods = [activityMod]; const autoModData = mapAutoMods(getAutoMods(defs, emptySet())); loSessionInfo = precalculateStructures( autoModData, generalMods, activityMods, true, armorStats, ); neededStats = [4, 0, 10, 12, 4, 0]; }); it('the problem is solvable', () => { const solution = pickAndAssignSlotIndependentMods( loSessionInfo, modStatistics, items, neededStats, 4, ); expect(solution).not.toBe(undefined); expect(solution).toMatchSnapshot(); }); it.skip('higher stats means we cannot find a viable set of picks', () => { for (let i = 0; i < 6; i++) { const newNeededStats = [...neededStats]; newNeededStats[i] += 2; expect( pickAndAssignSlotIndependentMods(loSessionInfo, modStatistics, items, newNeededStats, 4), ).toBe(undefined); } }); it.skip('we need all artifice mod slots', () => { expect( pickAndAssignSlotIndependentMods(loSessionInfo, modStatistics, items, neededStats, 3), ).toBe(undefined); }); it.skip('we need all the energy capacity in all general mod slots', () => { const ourItems = [...items]; ourItems[1] = modifyItem({ item: items[1], remainingEnergyCapacity: 3 }); expect( pickAndAssignSlotIndependentMods(loSessionInfo, modStatistics, ourItems, neededStats, 4), ).toBe(undefined); }); it.skip('activity mod cannot go into the other item if we want to hit stats', () => { const ourItems = [...items]; ourItems[1] = modifyItem({ item: items[3] }); expect( pickAndAssignSlotIndependentMods(loSessionInfo, modStatistics, ourItems, neededStats, 4), ).toBe(undefined); }); }); /** * To test optimal stat mod picking, we set up a bunch of sets defined by armor stats, remaining energies, and artifice slots, * and expect it to correctly find the highest total tier. */ describe('process-utils optimal mods', () => { let helmet: ProcessItem; let arms: ProcessItem; let chest: ProcessItem; let legs: ProcessItem; let classItem: ProcessItem; // use these for testing as they are reset after each test let items: ProcessItem[]; const defaultConstraints: DesiredStatRange[] = armorStats.map((statHash) => ({ statHash, minStat: 30, maxStat: 80, })); let autoModData: AutoModData; beforeAll(async () => { const defs = await getTestDefinitions(); const makeItem = (index: number): ProcessItem => ({ hash: index, id: index.toString(), isArtifice: false, isExotic: false, name: `Item ${index}`, power: 1500, stats: [0, 0, 0, 0, 0, 0], compatibleActivityMod: undefined, remainingEnergyCapacity: 10, }); helmet = makeItem(1); arms = makeItem(2); chest = makeItem(3); legs = makeItem(4); classItem = makeItem(5); items = [helmet, arms, chest, legs, classItem]; autoModData = mapAutoMods(getAutoMods(defs, emptySet())); }); // TODO: These cases don't exactly make sense in the tierless world but it's hard to think through what they should do const tierCases: [ setStats: number[], remainingEnergy: number[], numArtifice: number, expectedStats: number[], statMinMaxes: DesiredStatRange[], ][] = [ // the trick here is that we can use two small mods to boost resilience, // but it's better to use two large mods to boost discipline (cheaper mods...) [[80, 70, 80, 40, 30, 30], [0, 3, 0, 3, 0], 0, [80, 80, 80, 50, 30, 30], defaultConstraints], // ensure we combine artifice and small mods if needed (all goes to first stat) [[63, 70, 59, 35, 30, 30], [2, 0, 0, 0, 0], 3, [77, 70, 59, 35, 30, 30], defaultConstraints], // ensure we can use a cheap +5 mod to bump the 35 dis to 40 while using artifice on resilience // TODO: Broken by removing the cost-dominance distinction between different mods (which would have happened in Edge of Fate anyway) // [[80, 65, 80, 35, 30, 30], [1, 0, 0, 0, 0], 2, [80, 71, 80, 40, 30, 30], defaultConstraints], // ensure we get two tiers in mobility [[68, 66, 30, 30, 30, 30], [0, 0, 0, 0, 0], 4, [80, 66, 30, 30, 30, 30], defaultConstraints], // do everything we can to hit min bounds [[68, 66, 30, 30, 11, 30], [2, 2, 0, 0, 0], 4, [71, 66, 30, 30, 30, 30], defaultConstraints], // ensure that negative stat amounts aren't clamped too early [[30, 61, 30, 30, 30, -14], [5, 5, 5, 5, 5], 5, [50, 61, 30, 30, 30, 31], defaultConstraints], // We should assign two +10 mods to the fourth stat using remaining energy, // but we had a bug where it assigned to the fifth, skipping the fourth stat [ [82, 44, 73, 39, 40, 50], [10, 10, 10, 10, 10], 5, [82, 80, 82, 59, 40, 50], // Pick specific stat constraints for this test [ { statHash: StatHashes.Grenade, minStat: 0, maxStat: 82 }, { statHash: StatHashes.Melee, minStat: 0, maxStat: 82 }, { statHash: StatHashes.Class, minStat: 0, maxStat: 82 }, { statHash: StatHashes.Super, minStat: 0, maxStat: 200 }, { statHash: StatHashes.Weapons, minStat: 0, maxStat: 200 }, { statHash: StatHashes.Health, minStat: 0, maxStat: 200 }, ], ], ]; const pickMods = ( setStats: number[], remainingEnergy: number[], numArtifice: number, statMinMaxes: DesiredStatRange[], ) => { const ourItems = [...items]; for (let i = 0; i < ourItems.length; i++) { ourItems[i] = modifyItem({ item: ourItems[i], remainingEnergyCapacity: remainingEnergy[i], isArtifice: i < numArtifice, }); } const statMods = pickOptimalStatMods( precalculateStructures( autoModData, [], [], true, statMinMaxes.map(({ statHash }) => statHash), ), ourItems, setStats, statMinMaxes, ); const finalStats = [...setStats]; for (let i = 0; i < armorStats.length; i++) { finalStats[i] += statMods!.bonusStats[i]; } return finalStats; }; test.each(tierCases)( 'set with stats %p, energies %p, numArtifice %p yields tiers %p', (setStats, remainingEnergy, numArtifice, expectedTiers, statMinMaxes) => { const finalStats = pickMods(setStats, remainingEnergy, numArtifice, statMinMaxes); expect(finalStats).toStrictEqual(expectedTiers); }, ); // Tests that our algorithm, and thus the worker accurately reports the resulting stats const exactStatCases: [ setStats: number[], remainingEnergy: number[], numArtifice: number, expectedStats: number[], statConstraints: DesiredStatRange[], ][] = [ // Nice [[18, 80, 80, 26, 80, 30], [0, 0, 0, 3, 0], 4, [34, 80, 80, 32, 80, 30], defaultConstraints], // TODO: This is the same problem as above, only with reordered stats. The solution // is still optimal in terms of reached stats, but worse in terms of mod usage [[26, 80, 80, 18, 80, 30], [0, 0, 0, 3, 0], 4, [36, 80, 80, 30, 80, 30], defaultConstraints], ]; test.each(exactStatCases)( 'set with stats %p, energies %p, numArtifice %p produces exact stats %p', (setStats, remainingEnergy, numArtifice, expectedStats, statMinMaxes) => { const finalStats = pickMods(setStats, remainingEnergy, numArtifice, statMinMaxes); expect(finalStats).toStrictEqual(expectedStats); }, ); }); // This tests against a bug where an activity mod would accidentally be considered // eligible and fitting if it required as much or more energy than was remaining in any item, // even if it didn't have the mod slot. test('process-utils activity mods', async () => { const defs = await getTestDefinitions(); const makeItem = (index: number, remainingEnergyCapacity: number): ProcessItem => ({ hash: index, id: index.toString(), isArtifice: false, isExotic: index === 2, name: `Item ${index}`, power: 1500, stats: [0, 0, 0, 0, 0, 0], compatibleActivityMod: index === 2 ? undefined : 'nightmare', remainingEnergyCapacity, }); const statOrder: ArmorStatHashes[] = [ StatHashes.Health, // expensive StatHashes.Class, // expensive StatHashes.Melee, // cheap StatHashes.Grenade, // cheap StatHashes.Super, // expensive StatHashes.Weapons, // cheap ]; // The setup here is the following: All items have one or two energy // remaining, but the mods cost at least one energy, so all items have // at most one energy remaining, which is not enough for an expensive minor mod. // Under the buggy condition, the 2-cost mod can be assigned to the arms piece, // even though it is an exotic without the relevant mod slot, leaving 2 energy for // a +5 resilience mod in an item that actually needs to hold an activity mod. const helmet = makeItem(1, 2); const arms = makeItem(2, 1); const chest = makeItem(3, 1); const legs = makeItem(4, 1); const classItem = makeItem(5, 2); const items = [helmet, arms, chest, legs, classItem]; // Costs 1, 1, 1, 2 const modHashes = [ 1560574695, // InventoryItem "Nightmare Breaker" 1560574695, // InventoryItem "Nightmare Breaker" 1560574695, // InventoryItem "Nightmare Breaker" 1565861116, // InventoryItem "Enhanced Nightmare Crusher" ]; const activityMods = modHashes.map( (hash) => defs.InventoryItem.get(hash) as PluggableInventoryItemDefinition, ); const autoModData = mapAutoMods(getAutoMods(defs, emptySet())); const loSessionInfo = precalculateStructures( autoModData, [], activityMods.map(mapArmor2ModToProcessMod), true, statOrder, ); const resolvedStatConstraints = statOrder.map( (statHash): ResolvedStatConstraint => ({ statHash, ignored: false, maxStat: MAX_STAT, minStat: 0, }), ); const setStats = [55, 55, 55, 50, 50, 50]; // First, verify that our set can fit the mods const result = pickAndAssignSlotIndependentMods( loSessionInfo, modStatistics, items, [0, 0, 0, 0, 0, 0], 0, )!; expect(result).not.toBeUndefined(); // Then check that optimal and maximally available tiers only report // the cheaper stats where the mods can actually fit const autoMods = pickOptimalStatMods(loSessionInfo, items, setStats, resolvedStatConstraints); expect(autoMods).not.toBeUndefined(); expect(autoMods!.bonusStats).toEqual([10, 0, 0, 0, 0, 0]); const minMaxesInStatOrder: MinMaxStat[] = [ { minStat: 0, maxStat: 0 }, { minStat: 0, maxStat: 0 }, { minStat: 0, maxStat: 0 }, { minStat: 0, maxStat: 0 }, { minStat: 0, maxStat: 0 }, { minStat: 0, maxStat: 0 }, ]; updateMaxStats(loSessionInfo, items, setStats, 0, resolvedStatConstraints, minMaxesInStatOrder); expect(minMaxesInStatOrder.map((stat) => stat.maxStat)).toEqual([65, 65, 65, 60, 60, 60]); }); describe('process-utils updateMaxStats', () => { let items: ProcessItem[]; let loSessionInfo: LoSessionInfo; beforeAll(async () => { const defs = await getTestDefinitions(); items = Array(5) .fill(null) .map((_, i) => ({ hash: i, id: i.toString(), isArtifice: false, isExotic: false, name: `Item ${i}`, power: 1500, stats: [0, 0, 0, 0, 0, 0], compatibleModSeasons: [], remainingEnergyCapacity: 10, })); const autoModData = mapAutoMods(getAutoMods(defs, emptySet())); loSessionInfo = precalculateStructures(autoModData, [], [], true, armorStats); }); const testCases: [ description: string, setStats: number[], initialMinMaxes: { minStat: number; maxStat: number }[], filterMinStat: number, filterMaxStat: number | ((i: number) => number), foundAnyImprovement: boolean, expectedFirstMaxStat: number, ][] = [ [ 'updates maxStat when minMax.maxStat < filter.minStat', [50, 50, 50, 50, 50, 50], Array(6).fill({ minStat: 0, maxStat: 55 }), // Lower than minStat 60, // Higher than current maxStat 100, false, // foundAnyImprovement is false when only updating to minStat requirement 60, ], [ 'handles stat > minMax.maxStat condition', [85, 50, 50, 50, 50, 50], Array(6) .fill(null) .map((_, i) => ({ minStat: 0, maxStat: i === 0 ? 70 : 50 })), // First stat maxStat is 70 30, (i: number) => (i === 0 ? 100 : 70), // First stat allows improvement true, 135, // Can reach 135 with 5 large stat mods ], [ 'skips stat max already at MAX_STAT', [180, 50, 50, 50, 50, 50], Array(6) .fill(null) .map((_, i) => ({ minStat: 0, maxStat: i === 0 ? MAX_STAT : 50 })), // First stat already at MAX_STAT 30, MAX_STAT, true, MAX_STAT, // Should remain unchanged ], ]; test.each(testCases)( '%s', ( _description, setStats, minMaxes, filterMinStat, filterMaxStat, expectedResult, expectedFirstMaxStat, ) => { const statFilters = armorStats.map((statHash, i) => ({ statHash, minStat: filterMinStat, maxStat: typeof filterMaxStat === 'function' ? filterMaxStat(i) : filterMaxStat, })); const foundAnyImprovement = updateMaxStats( loSessionInfo, items, setStats, 0, statFilters, minMaxes, ); expect(foundAnyImprovement).toBe(expectedResult); expect(minMaxes[0].maxStat).toBe(expectedFirstMaxStat); }, ); }); // TODO: Edge of Fate: The mod cost changes have invalidated these tests, so they are skipped for now. describe('process-utils general mod assignment', () => { let items: ProcessItem[]; let loSessionInfo: LoSessionInfo; let generalMod: ProcessMod; let autoModData: AutoModData; beforeAll(async () => { const defs = await getTestDefinitions(); generalMod = mapArmor2ModToProcessMod( defs.InventoryItem.get(classStatModHash) as PluggableInventoryItemDefinition, ); items = Array(5) .fill(null) .map((_, i) => ({ hash: i, id: i.toString(), isArtifice: false, isExotic: false, name: `Item ${i}`, power: 1500, stats: [0, 0, 0, 0, 0, 0], compatibleModSeasons: [], remainingEnergyCapacity: 10, })); autoModData = mapAutoMods(getAutoMods(defs, emptySet())); loSessionInfo = precalculateStructures(autoModData, [generalMod], [], true, armorStats); }); it('returns empty array when no required stats and all general mods fit', () => { const result = pickAndAssignSlotIndependentMods( loSessionInfo, modStatistics, items, undefined, // No needed stats 0, ); expect(result).toEqual([]); }); it('returns undefined when general mods do not fit', () => { const lowEnergyItems = items.map((item) => modifyItem({ item, remainingEnergyCapacity: 1 })); const result = pickAndAssignSlotIndependentMods( loSessionInfo, modStatistics, lowEnergyItems, undefined, 0, ); expect(result).toBeUndefined(); }); it('handles auto mods off', () => { const result = pickAndAssignSlotIndependentMods( loSessionInfo, modStatistics, items, undefined, // No needed stats 0, ); expect(result).toEqual([]); }); }); describe('process-utils pickOptimalStatMods edge cases', () => { let items: ProcessItem[]; let loSessionInfo: LoSessionInfo; let autoModData: AutoModData; beforeAll(async () => { const defs = await getTestDefinitions(); items = Array(5) .fill(null) .map((_, i) => ({ hash: i, id: i.toString(), isArtifice: false, isExotic: false, name: `Item ${i}`, power: 1500, stats: [0, 0, 0, 0, 0, 0], compatibleModSeasons: [], remainingEnergyCapacity: 0, // No energy available })); autoModData = mapAutoMods(getAutoMods(defs, emptySet())); loSessionInfo = precalculateStructures(autoModData, [], [], true, armorStats); }); it('returns empty when no mods can be picked', () => { const setStats = [0, 0, 0, 0, 0, 0]; const desiredStatRanges = armorStats.map((statHash) => ({ statHash, minStat: 100, // Impossible to achieve with no energy maxStat: 100, })); const result = pickOptimalStatMods(loSessionInfo, items, setStats, desiredStatRanges); expect(result).toBeDefined(); expect(result!.bonusStats).toEqual([0, 0, 0, 0, 0, 0]); expect(result!.mods).toEqual([]); }); it('returns empty result when auto stat mods are off', () => { loSessionInfo = precalculateStructures(autoModData, [], [], false, armorStats); const setStats = [65, 42, 60, 76, 60, 87]; // No constraint const desiredStatRanges = armorStats.map((statHash) => ({ statHash, minStat: 0, maxStat: 200, })); const result = pickOptimalStatMods(loSessionInfo, items, setStats, desiredStatRanges); expect(result).toBeDefined(); expect(result!.bonusStats).toEqual([0, 0, 0, 0, 0, 0]); expect(result!.mods).toEqual([]); }); }); ================================================ FILE: src/app/loadout-builder/process-worker/process-utils.ts ================================================ import { MAX_STAT } from 'app/loadout/known-values'; import { generatePermutationsOfFive } from 'app/loadout/mod-permutations'; import { count } from 'app/utils/collections'; import { ArmorStatHashes, artificeStatBoost, DesiredStatRange, MinMaxStat } from '../types'; import { AutoModsCache, buildAutoModsMap, chooseAutoMods, ModsPick } from './auto-stat-mod-utils'; import { AutoModData, ModAssignmentStatistics, ProcessItem, ProcessMod } from './types'; /** * Data that stays the same in a given LO run. */ export interface LoSessionInfo { autoModOptions: AutoModsCache; hasActivityMods: boolean; /** The total cost of all user-picked general and activity mods. */ totalModEnergyCost: number; /** The cost of user-picked general mods, sorted descending. */ generalModCosts: number[]; /** How many general mod slots are available for auto stat mods. */ numAvailableGeneralMods: number; /** All uniquely distinguishable activity mod permutations */ activityModPermutations: (ProcessMod | null)[][]; /** How many activity mods we have per tag. */ activityTagCounts: { [tag: string]: number }; } export function precalculateStructures( autoModOptions: AutoModData, generalMods: ProcessMod[], activityMods: ProcessMod[], autoStatMods: boolean, statOrder: ArmorStatHashes[], ): LoSessionInfo { const generalModCosts = generalMods.map((m) => m.energyCost).sort((a, b) => b - a); const numAvailableGeneralMods = autoStatMods ? 5 - generalModCosts.length : 0; return { autoModOptions: buildAutoModsMap(autoModOptions, numAvailableGeneralMods, statOrder), hasActivityMods: activityMods.length > 0, generalModCosts, numAvailableGeneralMods, totalModEnergyCost: generalModCosts.reduce((acc, cost) => acc + cost, 0) + activityMods.reduce((acc, mod) => acc + mod.energyCost, 0), activityModPermutations: generateProcessModPermutations(activityMods), activityTagCounts: activityMods.reduce<{ [tag: string]: number }>((acc, mod) => { if (mod.tag) { acc[mod.tag] = (acc[mod.tag] || 0) + 1; } return acc; }, {}), }; } /** * For each possible permutation of activity mods, see which ones can fit on * this item set, and how much energy you'd have remaining in each piece if you * do. */ function getRemainingEnergiesPerAssignment( activityModPermutations: (ProcessMod | null)[][], items: readonly ProcessItem[], ): { /** Total remaining energy capacity across the set */ setEnergy: number; /** * For each valid permutation, how much energy is left on per item? These * lists are NOT sorted. */ remainingEnergiesPerAssignment: number[][]; } { const remainingEnergiesPerAssignment: number[][] = []; let setEnergy = 0; for (let i = 0; i < items.length; i++) { setEnergy += items[i].remainingEnergyCapacity; } activityModLoop: for (let p = 0; p < activityModPermutations.length; p++) { const activityPermutation = activityModPermutations[p]; const remainingEnergyCapacities = [0, 0, 0, 0, 0]; // Check each item to see if it's possible to slot the activity mods in this // permutation. for (let i = 0; i < items.length; i++) { const activityMod = activityPermutation[i]; const item = items[i]; remainingEnergyCapacities[i] = item.remainingEnergyCapacity; if (activityMod) { const tag = activityMod.tag!; const energyCost = activityMod.energyCost; // The activity mod for this slot won't fit in the item so move on to // the next permutation. if (energyCost > item.remainingEnergyCapacity || item.compatibleActivityMod !== tag) { continue activityModLoop; } remainingEnergyCapacities[i] -= activityMod.energyCost; } } remainingEnergiesPerAssignment.push(remainingEnergyCapacities); } return { setEnergy, remainingEnergiesPerAssignment }; } // How many extra points we need to add to each stat to hit the minimums. We // reuse a single array to avoid allocations. const requiredMinimumExtraStats = [0, 0, 0, 0, 0, 0]; /** * Updates the max stat range by trying to individually get the highest value in * each stat. * @returns true if it's possible to bump at least one stat to higher * than the desired range's minStat for that stat. */ export function updateMaxStats( info: LoSessionInfo, armor: readonly ProcessItem[], /** Stats for the current set. */ setStats: readonly number[], /** Total number of available artifice mods, */ numArtificeMods: number, /** The min/max stat value the user has requested. */ desiredStatRanges: readonly DesiredStatRange[], /** Current stat ranges across all sets we've seen so far. */ statRanges: MinMaxStat[], // mutated ): boolean { let foundAnyImprovement = false; // First, track absolutely required stats (and update existing maxes) for (let statIndex = 0; statIndex < desiredStatRanges.length; statIndex++) { const value = setStats[statIndex]; const filter = desiredStatRanges[statIndex]; const statRange = statRanges[statIndex]; if (statRange.maxStat < filter.minStat) { // This is only called with sets that satisfy stat constraints, // so optimistically bump these up statRange.maxStat = filter.minStat; } if (value > statRange.maxStat) { statRange.maxStat = value; // statRange.maxStat is guaranteed to be at least filter.minStat above, so // if the value is larger than that, we've found an improvement - unless // the filter also has a maxStat that's equal to the minStat, in which // case it's impossible to improve this stat within the user's desired // range. foundAnyImprovement ||= filter.minStat < filter.maxStat; } const neededValue = filter.minStat - value; if (neededValue > 0) { // All sets need at least these extra stats to hit minimums requiredMinimumExtraStats[statIndex] = neededValue; } else { requiredMinimumExtraStats[statIndex] = 0; } } if (info.numAvailableGeneralMods === 0 && numArtificeMods === 0) { // If there are no general mods or artifice mods available, we can't improve // stats any further. return foundAnyImprovement; } let remainingEnergyResult: ReturnType | undefined; // You wouldn't believe it, but Firefox is actually slow loading constants // from another module. const maxStat = MAX_STAT; // Then, for every stat where we haven't shown that we can hit MAX_STAT with any // set, try to see if we can exceed the previous max by adding auto stat mods. for (let statIndex = 0; statIndex < desiredStatRanges.length; statIndex++) { const value = setStats[statIndex]; const filter = desiredStatRanges[statIndex]; const statRange = statRanges[statIndex]; if (statRange.maxStat >= maxStat) { // We can already hit MAX_STAT for this stat, so skip it. continue; } remainingEnergyResult ??= getRemainingEnergiesPerAssignment( info.activityModPermutations, armor, ); const { remainingEnergiesPerAssignment, setEnergy } = remainingEnergyResult; // Since we calculate the maximum stat value we can hit for a stat in // isolation, require all other stats to hit their constrained minimums, but // for this stat we start from the highest stat max we've observed. Remember // that this array is expressed in terms of additional stat points. const previousRequiredMinimum = requiredMinimumExtraStats[statIndex]; requiredMinimumExtraStats[statIndex] = statRange.maxStat - value; // TODO: Rather than iterating one point at a time, we could run our greedy // assignment search that maximizes stats but with stat ranges that prevent // us from going over our minimum? Or maybe do a binary search for the // maximum we can reach? while (statRange.maxStat < maxStat) { // Now that tiers no longer matter (since Edge of Fate), we consider any // stat point increase a "tier". This should be a short-term change - // ideally we'd reconsider all these algorithms to see if they could be // simplified now that the tier concept is gone. requiredMinimumExtraStats[statIndex] += 1; // Now see if there's any way to hit that stat with mods. if ( !chooseAutoMods( info, requiredMinimumExtraStats, numArtificeMods, remainingEnergiesPerAssignment, setEnergy - info.totalModEnergyCost, ) ) { break; } const newValue = value + requiredMinimumExtraStats[statIndex]; // filter.minStat < filter.maxStat just checks to make sure you can // actually improve the stat given the user's new constraints. foundAnyImprovement ||= filter.minStat < filter.maxStat && newValue > filter.minStat; statRange.maxStat = newValue; // Keep going until we hit the max or we can no longer find mods to improve the stat. } requiredMinimumExtraStats[statIndex] = previousRequiredMinimum; } return foundAnyImprovement; } /** * This figures out if all user-chosen general, combat and activity mods can be * assigned to an armour set and auto stat mods can be picked to provide the * neededStats. This is a version of pickOptimalStatMods that only cares about * hitting the neededStats (minimum stat targets) and not finding the optimal * stat mods for the highest tier. * * The param info.activityModPermutations is assumed to be the result from * processUtils.ts#generateModPermutations, i.e. all permutations of activity * mods. By preprocessing all the assignments we skip a lot of work in the * middle of the big process algorithm. * * Returns a ModsPick representing the automatically picked stat mods in the * success case, even if no auto stat mods were requested/needed, in which case * the arrays will be empty. * * TODO: Doesn't need to return anything in its current usage */ export function pickAndAssignSlotIndependentMods( info: LoSessionInfo, modStatistics: ModAssignmentStatistics, // mutated items: readonly ProcessItem[], neededStats: number[] | undefined, numArtifice: number, ): ModsPick[] | undefined { modStatistics.earlyModsCheck.timesChecked++; let setEnergy = 0; for (const item of items) { setEnergy += item.remainingEnergyCapacity; } if (setEnergy < info.totalModEnergyCost) { modStatistics.earlyModsCheck.timesFailed++; return undefined; } // An early check to ensure we have enough activity mod combos // It works by creating an index of tags to totals of said tag // we can then ensure we have enough items with said tags. if (info.hasActivityMods) { for (const [tag, tagCount] of Object.entries(info.activityTagCounts)) { let socketsCount = 0; for (const item of items) { if (item.compatibleActivityMod === tag) { socketsCount++; } } if (socketsCount < tagCount) { modStatistics.earlyModsCheck.timesFailed++; return undefined; } } } let assignedModsAtLeastOnce = false; const remainingEnergyCapacities = [0, 0, 0, 0, 0]; modStatistics.finalAssignment.modAssignmentAttempted++; // Now we begin looping over all the mod permutations. activityModLoop: for (const activityPermutation of info.activityModPermutations) { activityItemLoop: for (let i = 0; i < items.length; i++) { const activityMod = activityPermutation[i]; // If a mod is null there is nothing being socketed into the item so move on if (!activityMod) { continue activityItemLoop; } const item = items[i]; const tag = activityMod.tag!; const energyCost = activityMod.energyCost; // The activity mods won't fit in the item set so move on to the next set of mods if (energyCost > item.remainingEnergyCapacity || item.compatibleActivityMod !== tag) { continue activityModLoop; } } assignedModsAtLeastOnce = true; // This is a valid activity and combat mod assignment. See how much energy is left over per piece for (let idx = 0; idx < items.length; idx++) { const item = items[idx]; remainingEnergyCapacities[idx] = item.remainingEnergyCapacity - (activityPermutation[idx]?.energyCost || 0); } if (neededStats) { const result = chooseAutoMods( info, neededStats, numArtifice, [remainingEnergyCapacities], setEnergy - info.totalModEnergyCost, ); if (result) { return result; } } else if ( info.generalModCosts.every((cost, index) => cost <= remainingEnergyCapacities[index]) ) { return []; } } if (assignedModsAtLeastOnce && neededStats) { modStatistics.finalAssignment.autoModsAssignmentFailed++; } else { modStatistics.finalAssignment.modsAssignmentFailed++; } return undefined; } /** * Optimizes the auto stat mod picks to maximize total stats, prioritizing stats * earlier in the stat order. This differs from * `pickAndAssignSlotIndependentMods` in that it assumes that the stat minimums * can definitely be hit, and instead tries to maximize the total stats by * picking the best auto stat mods. */ export function pickOptimalStatMods( info: LoSessionInfo, items: ProcessItem[], setStats: number[], desiredStatRanges: DesiredStatRange[], ): { mods: number[]; bonusStats: number[] } | undefined { const { remainingEnergiesPerAssignment, setEnergy } = getRemainingEnergiesPerAssignment( info.activityModPermutations, items, ); if (remainingEnergiesPerAssignment.length === 0) { // No valid activity mod assignments return undefined; } // The amount of additional stat points after which stats don't give us a benefit anymore. const maxAddedStats = [0, 0, 0, 0, 0, 0]; // The amount of additional stat points we need to add to each stat to hit the minimums. const explorationStats = [0, 0, 0, 0, 0, 0]; for (let statIndex = setStats.length - 1; statIndex >= 0; statIndex--) { const filter = desiredStatRanges[statIndex]; if (filter.maxStat > 0) { const value = setStats[statIndex]; if (filter.minStat > 0) { const neededValue = filter.minStat - value; if (neededValue > 0) { explorationStats[statIndex] = neededValue; } } maxAddedStats[statIndex] = filter.maxStat - value; } } const numArtificeMods = count(items, (i) => i.isArtifice); const picks = greedyPickStatMods( info, explorationStats, maxAddedStats, numArtificeMods, remainingEnergiesPerAssignment, setEnergy - info.totalModEnergyCost, ); const bonusStats = [0, 0, 0, 0, 0, 0]; for (const pick of picks) { bonusStats[pick.targetStatIndex] += pick.exactStatPoints; } return { mods: picks.flatMap((pick) => pick.modHashes), bonusStats, }; } // const majorMinorRatio = majorStatBoost / minorStatBoost; /** * In the post-Edge of Fate world, there are no more stat tiers - every stat * point gives some linear benefit. So we can use a much simpler greedy * algorithm to pick stat mods. */ // TODO: I think this is roughly right - each armor piece can have up to 1 // artifice mod and one general mod. If we start with a list of items' remaining // energy capacities, we can just greedily pick the best stat mods for each stat // until we hit the max or run out of mods or energy for mods. The one bit // that's glaringly missing is we probably need to do this for each combination // of activity mod permutations, since those can affect the remaining energy // capacities of the items. So we need to loop over all activity mod // permutations and then for each one, greedily pick stat mods. We can maybe // even short-circuit based on a total stat high water mark (or if we've already // used 5 major mods... ). This might be fast enough that we can do it within // the process loop! // // This also has the concept of avoiding wasting stats, which is a bit different // from the old setup. It's unclear whether this is something we'd want to make // optional (at the very least we'd need to disable it for some calculations). // I'm also not sure we do the *best* job of avoiding wasted stats, since there // might be a situation where it'd be better to not assign an artifice mod to a // stat but instead give it a major stat mod on its own, or something like that. // (e.g. if the stat is at 190, it's better to give it a +10 mod than a +3 and // +5 or a +3 and +10). So maybe the exploration algorithm is still worthwhile // with a tier size of 1? export function greedyPickStatMods( info: LoSessionInfo, explorationStats: number[], /** * The highest allowed additional stat values. we are not allowed to boost stats beyond this, * otherwise we would go over the stats' tier maxes (or MAX_STAT if no max) */ maxAddedStats: number[], /** How many artifice mods this set has */ numArtificeMods: number, /** The different permutations of leftover energy after assigning activity mods permutations */ remainingEnergyCapacities: number[][], /** The total amount of energy left over in this set */ totalModEnergyCapacity: number, ): ModsPick[] { if (remainingEnergyCapacities[0].every((e) => e === 0) && numArtificeMods === 0) { return []; } let picks: ModsPick[] | undefined = chooseAutoMods( info, explorationStats, numArtificeMods, remainingEnergyCapacities, totalModEnergyCapacity, ); if (!picks) { // If we can't hit the target stats with the current exploration stats, we // can't do anything. return []; } for (let i = 0; i < explorationStats.length; i++) { if (maxAddedStats[i] <= 0) { continue; // No need to boost this stat } let candidatePick: ModsPick[] | undefined; const originalExplorationStat = explorationStats[i]; // Binary search for the best stat boost we can get for this stat. let lastGoodCandidatePick: ModsPick[] | undefined = undefined; let lastGoodCandidatePickExplorationStat = 0; let minBoost = Math.max(artificeStatBoost, originalExplorationStat); let maxBoost = maxAddedStats[i] - 1; while (minBoost < maxBoost) { explorationStats[i] = Math.floor((minBoost + maxBoost) / 2); if (explorationStats[i] <= 0) { break; // No need to boost this stat } candidatePick = chooseAutoMods( info, explorationStats, numArtificeMods, remainingEnergyCapacities, totalModEnergyCapacity, ); if (candidatePick) { // We can hit this stat with the current exploration stats, so try to // increase it. minBoost = explorationStats[i] + 1; lastGoodCandidatePick = candidatePick; lastGoodCandidatePickExplorationStat = explorationStats[i]; } else { // We can't hit this stat with the current exploration stats, so try to // decrease it. maxBoost = explorationStats[i]; } } if (candidatePick) { picks = candidatePick; } else if (lastGoodCandidatePick) { picks = lastGoodCandidatePick; explorationStats[i] = lastGoodCandidatePickExplorationStat; } else { // Reset explorationStats[i] = originalExplorationStat; } } return picks; } // only exported for testing purposes export function generateProcessModPermutations(mods: (ProcessMod | null)[]) { // Creates a string from the mod permutation containing the unique properties // that we care about, so we can reduce to the minimum number of permutations. // If two different mods that fit in the same socket have the same cost, they // are identical from the mod assignment perspective. // This works because we check to see if we have already recorded this string // in heaps algorithm before we add the permutation to the result. const createPermutationKey = (permutation: (ProcessMod | null)[]) => permutation.map((mod) => (mod ? `${mod.energyCost}${mod.tag}` : undefined)).join(','); return generatePermutationsOfFive(mods, createPermutationKey); } ================================================ FILE: src/app/loadout-builder/process-worker/process.ts ================================================ import { SetBonusCounts } from '@destinyitemmanager/dim-api-types'; import { fotlWildcardHashes, MAX_STAT } from 'app/loadout/known-values'; import { compact, filterMap } from 'app/utils/collections'; import { BucketHashes } from 'data/d2/generated-enums'; import { sum } from 'es-toolkit'; import { infoLog } from '../../utils/log'; import { ArmorBucketHashes, ArmorStatHashes, ArmorStats, artificeStatBoost, DesiredStatRange, majorStatBoost, MinMaxStat, StatRanges, } from '../types'; import { getPower } from '../utils'; import { pickAndAssignSlotIndependentMods, pickOptimalStatMods, precalculateStructures, updateMaxStats, } from './process-utils'; import { encodeStatMix, HeapSetTracker } from './set-tracker'; import { AutoModData, LockedProcessMods, ProcessItem, ProcessItemsByBucket, ProcessResult, ProcessStatistics, } from './types'; /** Caps the maximum number of total armor sets that'll be returned */ const RETURNED_ARMOR_SETS = 200; export interface ProcessInputs { filteredItems: ProcessItemsByBucket; /** Selected mods' total contribution to each stat */ modStatTotals: ArmorStats; /** Mods to add onto the sets */ lockedMods: LockedProcessMods; /** If we're requiring any set bonuses, the number of items desired from each set */ setBonuses: SetBonusCounts; /** The user's chosen stat ranges, in priority order. */ desiredStatRanges: DesiredStatRange[]; /** Ensure every set includes one exotic */ anyExotic: boolean; /** Which artifice/tuning mods, large, and small stat mods are available */ autoModOptions: AutoModData; /** Use stat mods to hit stat minimums */ autoStatMods: boolean; /** If set, only sets where at least one stat **exceeds** `desiredStatRanges` minimums will be returned */ strictUpgrades: boolean; /** If set, LO will exit after finding at least one set that fits all constraints (and is a strict upgrade if `strictUpgrades` is set) */ stopOnFirstSet: boolean; } /** * This processes all permutations of armor to build sets * @param filteredItems pared down list of items to process sets from * @param modStatTotals Stats that are applied to final stat totals, think general and other mod stats */ export async function process( workerNum: number, { filteredItems, modStatTotals, lockedMods, setBonuses, desiredStatRanges, anyExotic, autoModOptions, autoStatMods, strictUpgrades, stopOnFirstSet, }: ProcessInputs, onProgress: (completed: number) => void, ): Promise { const pstart = performance.now(); // For efficiency, we'll handle most stats as flat arrays in the order the user prioritized their stats. const statOrder = desiredStatRanges.map(({ statHash }) => statHash as ArmorStatHashes); // The maximum stat constraints for each stat const maxStatConstraints = desiredStatRanges.map(({ maxStat }) => maxStat); // Convert the list of stat bonuses from mods into a flat array in the same order as `statOrder`. const modStatsInStatOrder = statOrder.map((h) => modStatTotals[h]); // This stores the computed min and max value for each stat as we process all sets, so we // can display it on the stat constraint editor. const statRanges = statOrder.map((): MinMaxStat => ({ minStat: MAX_STAT, maxStat: 0 })); // Precompute stat arrays for each item in stat order const statsCache = new Map(); for (const item of ArmorBucketHashes.flatMap((h) => filteredItems[h])) { statsCache.set( item, statOrder.map((statHash) => item.stats[statHash]), ); } // Each of these groups has already been reduced (in useProcess.ts) to the // minimum number of items that are worth considering. const helms = filteredItems[BucketHashes.Helmet]; const gauntlets = filteredItems[BucketHashes.Gauntlets]; const chests = filteredItems[BucketHashes.ChestArmor]; const legs = filteredItems[BucketHashes.LegArmor]; const classItems = filteredItems[BucketHashes.ClassArmor]; // The maximum possible combos we could possibly have const combos = helms.length * gauntlets.length * chests.length * legs.length * classItems.length; const numItems = helms.length + gauntlets.length + chests.length + legs.length + classItems.length; infoLog( `loadout optimizer thread ${workerNum}`, 'Processing', combos, 'combinations from', numItems, 'items', { helms: helms.length, gauntlets: gauntlets.length, chests: chests.length, legs: legs.length, classItems: classItems.length, }, ); const setStatistics: ProcessStatistics['statistics'] = { skipReasons: { doubleExotic: 0, noExotic: 0, skippedLowTier: 0, insufficientSetBonus: 0, }, lowerBoundsExceeded: { timesChecked: 0, timesFailed: 0 }, modsStatistics: { earlyModsCheck: { timesChecked: 0, timesFailed: 0 }, autoModsPick: { timesChecked: 0, timesFailed: 0 }, finalAssignment: { modAssignmentAttempted: 0, modsAssignmentFailed: 0, autoModsAssignmentFailed: 0, }, }, }; const processStatistics: ProcessStatistics = { numProcessed: 0, numValidSets: 0, statistics: setStatistics, }; if (combos === 0) { const statRangesFiltered = Object.fromEntries( statOrder.map((h) => [h, { minStat: 0, maxStat: MAX_STAT }]), ) as StatRanges; return { sets: [], combos: 0, statRangesFiltered, processInfo: processStatistics }; } const setTracker = new HeapSetTracker<{ /** The armor items in this set. */ armor: ProcessItem[]; /** The stats associated with this armor set. */ stats: number[]; mods: number[]; bonusStats: number[]; }>(RETURNED_ARMOR_SETS); const { activityMods, generalMods } = lockedMods; const precalculatedInfo = precalculateStructures( autoModOptions, generalMods, activityMods, autoStatMods, statOrder, ); const hasMods = Boolean(activityMods.length || generalMods.length); const setBonusHashes = Object.keys(setBonuses).map((h) => Number(h)); const setBonusCounts = Object.values(setBonuses) as number[]; // TS won't figure this out itself? interface Scheduler { scheduler?: { yield: () => Promise }; } let yieldTask: (() => Promise) | undefined = undefined; if ((globalThis as unknown as Scheduler).scheduler && navigator.userAgent.includes('Firefox')) { // Unlike Chrome, Firefox won't deliver postMessage until the thread yields. // This relatively new API lets you yield without having to rewrite your // whole loop. yieldTask = () => (globalThis as unknown as Scheduler).scheduler!.yield(); } let comboCount = 0; itemLoop: for (let helmIdx = 0; helmIdx < helms.length; helmIdx++) { const helm = helms[helmIdx]; const helmExotic = Number(helm.isExotic); const helmArtifice = Number(helm.isArtifice); const helmStats = statsCache.get(helm)!; for (let gauntIdx = 0; gauntIdx < gauntlets.length; gauntIdx++) { const gaunt = gauntlets[gauntIdx]; const gauntletExotic = Number(gaunt.isExotic); const gauntArtifice = Number(gaunt.isArtifice); const gauntStats = statsCache.get(gaunt)!; for (let chestIdx = 0; chestIdx < chests.length; chestIdx++) { const chest = chests[chestIdx]; const chestExotic = Number(chest.isExotic); const chestArtifice = Number(chest.isArtifice); const chestStats = statsCache.get(chest)!; for (let legIdx = 0; legIdx < legs.length; legIdx++) { const leg = legs[legIdx]; const legExotic = Number(leg.isExotic); const legArtifice = Number(leg.isArtifice); const legStats = statsCache.get(leg)!; innerloop: for (let classItemIdx = 0; classItemIdx < classItems.length; classItemIdx++) { const classItem = classItems[classItemIdx]; comboCount++; if (comboCount >= 100000) { onProgress(comboCount); comboCount = 0; if (yieldTask) { await yieldTask(); } } const classItemExotic = Number(classItem.isExotic); const classItemArtifice = Number(classItem.isArtifice); const classItemStats = statsCache.get(classItem)!; // Check exotic constraints const exoticSum = classItemExotic + helmExotic + gauntletExotic + chestExotic + legExotic; if (exoticSum > 1) { setStatistics.skipReasons.doubleExotic += 1; continue; } if (anyExotic && exoticSum === 0) { setStatistics.skipReasons.noExotic += 1; continue; } // Check set bonus requirements let wildcardAvailable = true; for (let i = 0; i < setBonusHashes.length; i++) { const setHash = setBonusHashes[i]; const setNeededCount = setBonusCounts[i]; const setCount = Number(helm.setBonus === setHash) + Number(gaunt.setBonus === setHash) + Number(chest.setBonus === setHash) + Number(leg.setBonus === setHash) + Number(classItem.setBonus === setHash); if (setCount < setNeededCount) { if ( wildcardAvailable && fotlWildcardHashes.has(helm.hash!) && setCount + 1 === setNeededCount ) { wildcardAvailable = false; } else { setStatistics.skipReasons.insufficientSetBonus += 1; continue innerloop; } } } processStatistics.numProcessed++; // Sum up the stats of each piece to form the overall set stats. // Note that mod stats could theoretically take these negative, but // none do in practice. // // Note: JavaScript engines apparently don't unroll loops // automatically and this makes a big difference in speed. const stats = [ modStatsInStatOrder[0] + helmStats[0] + gauntStats[0] + chestStats[0] + legStats[0] + classItemStats[0], modStatsInStatOrder[1] + helmStats[1] + gauntStats[1] + chestStats[1] + legStats[1] + classItemStats[1], modStatsInStatOrder[2] + helmStats[2] + gauntStats[2] + chestStats[2] + legStats[2] + classItemStats[2], modStatsInStatOrder[3] + helmStats[3] + gauntStats[3] + chestStats[3] + legStats[3] + classItemStats[3], modStatsInStatOrder[4] + helmStats[4] + gauntStats[4] + chestStats[4] + legStats[4] + classItemStats[4], modStatsInStatOrder[5] + helmStats[5] + gauntStats[5] + chestStats[5] + legStats[5] + classItemStats[5], ]; // A version of the set stats that have been clamped to the max stat // constraint. const effectiveStats = [ Math.min(stats[0], maxStatConstraints[0]), Math.min(stats[1], maxStatConstraints[1]), Math.min(stats[2], maxStatConstraints[2]), Math.min(stats[3], maxStatConstraints[3]), Math.min(stats[4], maxStatConstraints[4]), Math.min(stats[5], maxStatConstraints[5]), ]; // neededStats is the extra stats we'd need in each stat in order to // hit the stat minimums, and totalNeededStats is just the sum of // those. This informs the logic for deciding how to add stat mods. const neededStats = [0, 0, 0, 0, 0, 0]; let totalNeededStats = 0; // Check which stats we're under the stat minimums on. let totalStats = 0; for (let index = 0; index < 6; index++) { const filter = desiredStatRanges[index]; if (filter.maxStat > 0 /* non-ignored stat */) { const value = effectiveStats[index]; // Update the minimum stat range while we're here const statRange = statRanges[index]; if (value < statRange.minStat) { statRange.minStat = value; } totalStats += value; if (filter.minStat > 0) { const neededValue = filter.minStat - value; if (neededValue > 0) { totalNeededStats += neededValue; neededStats[index] = neededValue; } } } } const numArtifice = helmArtifice + gauntArtifice + chestArtifice + legArtifice + classItemArtifice; // The most total stat points we could get from mods, assuming // everything was perfectly assignable. const maxModBonus = numArtifice * artificeStatBoost + precalculatedInfo.numAvailableGeneralMods * majorStatBoost; // Check to see if it would be at all possible to hit the needed // stat total with the best case mod bonuses. If totalNeededStats is // 0 this passes trivially. setStatistics.lowerBoundsExceeded.timesChecked++; if (totalNeededStats > maxModBonus) { setStatistics.lowerBoundsExceeded.timesFailed++; continue; } const armor = [helm, gaunt, chest, leg, classItem]; // Items that individually can't fit their slot-specific mods were // filtered out before even passing them to the worker, so we only // do this combined mods + auto-stats check if we need to check // whether the set can fit the mods and hit target stats. This is a // fast check to see if enough mods can fit to hit needed stat // minimums. if ( (hasMods || totalNeededStats > 0) && !pickAndAssignSlotIndependentMods( precalculatedInfo, setStatistics.modsStatistics, armor, totalNeededStats > 0 ? neededStats : undefined, numArtifice, ) ) { // There's no way for this set to fit all requested mods while // satisfying tier lower bounds, so continue on. setStatistics // have been updated in pickAndAssignSlotIndependentMods. continue; } // At this point we know this set satisfies all constraints. // Update the max stat ranges. We need to do this before we short // circuit anything so that the stat ranges are accurate. // TODO: Then updateMaxStats assigns auto mods AGAIN, potentially many times, to figure out the max possible stats in each stat individually. const foundAnyImprovement = updateMaxStats( precalculatedInfo, armor, stats, numArtifice, desiredStatRanges, statRanges, ); // Drop this set if it could never make it into our top // RETURNED_ARMOR_SETS sets. We do this only after confirming that // any required stat mods fit and updating our max tiers so that the // max available tier info stays accurate. if (!setTracker.couldInsert(totalStats + maxModBonus)) { setStatistics.skipReasons.skippedLowTier++; continue; } const optimalResult = pickOptimalStatMods( precalculatedInfo, armor, stats, desiredStatRanges, ); if (!optimalResult) { // This means we couldn't assign mods in a way that satisfied // minimum stat constraints. This can happen if the mods that // would be needed don't fit into the available slots. setStatistics.modsStatistics.finalAssignment.modsAssignmentFailed++; continue; } const { bonusStats, mods } = optimalResult; const finalStats = [ effectiveStats[0] + bonusStats[0], effectiveStats[1] + bonusStats[1], effectiveStats[2] + bonusStats[2], effectiveStats[3] + bonusStats[3], effectiveStats[4] + bonusStats[4], effectiveStats[5] + bonusStats[5], ]; const finalTotalStats = finalStats[0] + finalStats[1] + finalStats[2] + finalStats[3] + finalStats[4] + finalStats[5]; // Now use our more accurate extra tiers prediction if (!setTracker.couldInsert(finalTotalStats)) { setStatistics.skipReasons.skippedLowTier++; continue; } // Calculate the numeric stat mix for fast integer comparison. // This encodes each stat value (0-200) into 8 bits, packed into a single integer. // Only non-ignored stats are included, maintaining lexical ordering for priority. const numericStatMix = encodeStatMix(finalStats, desiredStatRanges); // Add on any tuning mods that were preset on the items. mods.push( // It's important that we keep the order of these tuning mods in // the order of the armor (even when we assign mods dynamically, // later), so that when we assign them in fitMostMods they get // assigned to the same item. Otherwise, we could end up swapping // between one balanced mod and one tuning mod, and the balanced // mod's stat bonuses could be slightly different. ...compact([ helm.includedTuningMod, gaunt.includedTuningMod, chest.includedTuningMod, leg.includedTuningMod, classItem.includedTuningMod, ]), ); processStatistics.numValidSets++; // And now insert our set using the predicted total tier and numeric stat mix. setTracker.insert({ enabledStatsTotal: finalTotalStats, statMix: numericStatMix, power: getPower(armor), armor, stats, statsTotal: sum(stats), mods, bonusStats, }); if (stopOnFirstSet) { if (strictUpgrades) { if (foundAnyImprovement) { break itemLoop; } } else { break itemLoop; } } } } } } } const finalSets = setTracker.getArmorSets(); const sets = filterMap(finalSets, ({ armor, stats, mods, bonusStats, ...rest }) => { const armorOnlyStats: Partial = {}; const fullStats: Partial = {}; let hasStrictUpgrade = false; const helmStats = statsCache.get(armor[0])!; const gauntStats = statsCache.get(armor[1])!; const chestStats = statsCache.get(armor[2])!; const legStats = statsCache.get(armor[3])!; const classItemStats = statsCache.get(armor[4])!; for (let i = 0; i < statOrder.length; i++) { const statHash = statOrder[i]; const value = stats[i] + bonusStats[i]; fullStats[statHash] = value; const statFilter = desiredStatRanges[i]; if ( statFilter.maxStat > 0 /* enabled stat */ && strictUpgrades && statFilter.minStat < statFilter.maxStat && !hasStrictUpgrade ) { hasStrictUpgrade ||= value > statFilter.minStat; } armorOnlyStats[statHash] = helmStats[i] + gauntStats[i] + chestStats[i] + legStats[i] + classItemStats[i]; } if (strictUpgrades && !hasStrictUpgrade) { return undefined; } return { ...rest, armor: armor.map((item) => item.id), stats: fullStats as ArmorStats, armorStats: armorOnlyStats as ArmorStats, statMods: mods, }; }); const totalTime = performance.now() - pstart; infoLog( `loadout optimizer thread ${workerNum}`, 'found', processStatistics.numValidSets, 'stat mixes after processing', combos, 'stat combinations in', totalTime, 'ms - ', Math.floor((combos * 1000) / totalTime), 'combos/s', // Split into multiple objects so console.log will show them all expanded 'sets outright skipped:', setStatistics.skipReasons, 'lower bounds:', setStatistics.lowerBoundsExceeded, 'mod assignment stats:', 'early check:', setStatistics.modsStatistics.earlyModsCheck, 'auto mods pick:', setStatistics.modsStatistics.autoModsPick, setStatistics.modsStatistics, ); const statRangesFiltered = Object.fromEntries( statRanges.map((h, i) => [statOrder[i], h]), ) as StatRanges; return { sets, combos, statRangesFiltered, processInfo: processStatistics, }; } ================================================ FILE: src/app/loadout-builder/process-worker/set-tracker.test.ts ================================================ import { armorStats } from 'app/search/d2-known-values'; import { sum } from 'es-toolkit'; import { getPower } from '../utils'; import { decodeStatMix, encodeStatMix, HeapEntry, HeapSetTracker } from './set-tracker'; import { ProcessItem } from './types'; const createMockArmor = (id: string, power: number): ProcessItem => ({ id, isExotic: false, isArtifice: false, remainingEnergyCapacity: 10, power, stats: {}, }); /** * Essential functional tests for both SetTracker and HeapSetTracker. * Covers core behaviors needed by process.ts. */ const trackerImplementations = [ { name: 'HeapSetTracker', ctor: HeapSetTracker<{ armor: ProcessItem[] }> }, ]; for (const { name, ctor } of trackerImplementations) { describe(name, () => { const desiredStatRanges = armorStats.map((statHash) => ({ statHash, maxStat: 100, minStat: 10, })); const makeInsert = (tracker: HeapSetTracker<{ armor: ProcessItem[] }>) => ( enabledStatsTotal: number, statMix: number, armor: ProcessItem[], stats: number[], ): boolean => { const power = getPower(armor); const entry: HeapEntry<{ armor: ProcessItem[] }> = { enabledStatsTotal, statMix, power, armor, statsTotal: sum(stats), }; return tracker.insert(entry); }; it('should handle basic insertion, ordering, and retrieval', () => { const tracker = new ctor(5); const insert = makeInsert(tracker); // Insert sets with different tiers and stat mixes expect( insert( 10, encodeStatMix([5, 5, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('a', 1000)], [5, 5, 0, 0, 0, 0], ), ).toBe(true); expect( insert( 12, encodeStatMix([6, 6, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('b', 1200)], [6, 6, 0, 0, 0, 0], ), ).toBe(true); expect( insert( 10, encodeStatMix([4, 6, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('c', 1100)], [4, 6, 0, 0, 0, 0], ), ).toBe(true); expect(tracker.totalSets).toBe(3); // Verify ordering: tier desc, then statMix desc, then power desc const sets = tracker.getArmorSets(); expect(sets[0].armor[0].id).toBe('b'); // tier 12 expect(sets[1].armor[0].id).toBe('a'); // tier 10, mix 550000 expect(sets[2].armor[0].id).toBe('c'); // tier 10, mix 460000 }); it('should handle capacity limits and trimming correctly', () => { const tracker = new ctor(3); const insert = makeInsert(tracker); // Fill to capacity expect( insert( 10, encodeStatMix([5, 5, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('a', 1000)], [5, 5, 0, 0, 0, 0], ), ).toBe(true); expect( insert( 12, encodeStatMix([6, 6, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('b', 1000)], [6, 6, 0, 0, 0, 0], ), ).toBe(true); expect( insert( 8, encodeStatMix([4, 4, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('c', 1000)], [4, 4, 0, 0, 0, 0], ), ).toBe(true); expect(tracker.totalSets).toBe(3); // Insert low tier - should be rejected const lowResult = insert( 6, encodeStatMix([3, 3, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('d', 1000)], [3, 3, 0, 0, 0, 0], ); expect(lowResult).toBe(false); expect(tracker.totalSets).toBe(3); // Insert high tier - should succeed but cause trimming const highResult = insert( 14, encodeStatMix([7, 7, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('e', 1000)], [7, 7, 0, 0, 0, 0], ); expect(highResult).toBe(false); // trimWorstSet returns false expect(tracker.totalSets).toBe(3); // Verify worst item was removed const sets = tracker.getArmorSets(); expect(sets.find((s) => s.armor[0].id === 'c')).toBe(undefined); // tier 8 removed expect(sets.find((s) => s.armor[0].id === 'e')).not.toBe(undefined); // tier 14 kept }); it('should implement couldInsert correctly for hot path optimization', () => { const tracker = new ctor(2); const insert = makeInsert(tracker); // Empty tracker accepts everything expect(tracker.couldInsert(5)).toBe(true); expect(tracker.couldInsert(50)).toBe(true); // Fill to capacity expect( insert( 10, encodeStatMix([5, 5, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('a', 1000)], [5, 5, 0, 0, 0, 0], ), ).toBe(true); expect( insert( 8, encodeStatMix([4, 4, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('b', 1000)], [4, 4, 0, 0, 0, 0], ), ).toBe(true); // At capacity: reject < worst, accept >= worst expect(tracker.couldInsert(7)).toBe(false); // < 8 expect(tracker.couldInsert(8)).toBe(true); // >= 8 (matches SetTracker behavior) expect(tracker.couldInsert(15)).toBe(true); // > 8 }); it('should handle duplicate detection', () => { const tracker = new ctor(5); const insert = makeInsert(tracker); // Insert first item insert( 10, encodeStatMix([5, 5, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('a', 1000)], [5, 5, 0, 0, 0, 0], ); // SetTracker contract allows duplicates const result = insert( 10, encodeStatMix([5, 5, 0, 0, 0, 0], desiredStatRanges), [createMockArmor('b', 900)], [5, 5, 0, 0, 0, 0], ); expect(result).toBe(true); expect(tracker.totalSets).toBe(2); }); }); } describe('stat mix encoding/decoding', () => { const desiredStatRanges = armorStats.map((statHash) => ({ statHash, maxStat: 100, minStat: 10, })); const desiredStatRangesWithIgnored = armorStats.map((statHash, i) => ({ statHash, maxStat: i === 1 || i === 3 ? 0 : 100, // resilience and discipline ignored minStat: i === 1 || i === 3 ? 0 : 10, })); describe('encodeStatMix', () => { it('should only encode non-ignored stats', () => { const stats = [50, 999, 100, 999, 25, 150]; // ignored stats have high values const encoded = encodeStatMix(stats, desiredStatRangesWithIgnored); // Only mobility(50), recovery(100), intellect(25), strength(150) should be encoded // The 999 values should be skipped entirely const decoded = decodeStatMix(encoded, 4); // 4 non-ignored stats expect(decoded).toEqual([50, 100, 25, 150]); }); it('should handle zero values correctly', () => { const stats = [0, 0, 0, 0, 0, 0]; const encoded = encodeStatMix(stats, desiredStatRanges); expect(encoded).toBe(0); const decoded = decodeStatMix(encoded, 6); expect(decoded).toEqual([0, 0, 0, 0, 0, 0]); }); it('should maintain lexical ordering for comparison', () => { const stats1 = [100, 50, 75, 25, 150, 200]; const stats2 = [100, 50, 75, 25, 150, 199]; // last stat one less const stats3 = [100, 50, 75, 25, 149, 200]; // second-to-last stat one less const encoded1 = encodeStatMix(stats1, desiredStatRanges); const encoded2 = encodeStatMix(stats2, desiredStatRanges); const encoded3 = encodeStatMix(stats3, desiredStatRanges); // Higher stats should produce higher encoded values (for same priority positions) expect(encoded1).toBeGreaterThan(encoded2); expect(encoded1).toBeGreaterThan(encoded3); // Earlier stat positions should have higher priority - stats2 has higher value in position 4 (more significant) // so stats2 should be greater than stats3 even though stats3 has higher value in position 5 (less significant) expect(encoded2).toBeGreaterThan(encoded3); }); }); describe('decodeStatMix', () => { it('should correctly decode encoded values', () => { const originalStats = [50, 75, 100, 25, 150, 200]; const encoded = encodeStatMix(originalStats, desiredStatRanges); const decoded = decodeStatMix(encoded, 6); expect(decoded).toEqual(originalStats); }); it('should handle partial decoding for ignored stats', () => { const stats = [50, 999, 100, 999, 25, 150]; const encoded = encodeStatMix(stats, desiredStatRangesWithIgnored); const decoded = decodeStatMix(encoded, 4); // Should decode only the 4 non-ignored stats expect(decoded).toEqual([50, 100, 25, 150]); }); it('should handle edge case of empty encoding', () => { const decoded = decodeStatMix(0, 0); expect(decoded).toEqual([]); }); }); }); ================================================ FILE: src/app/loadout-builder/process-worker/set-tracker.ts ================================================ import { DesiredStatRange } from '../types'; /** * Heap entry for the heap-based SetTracker. T is all the other properties you * want to store that aren't core to the operation of the tracker. */ export type HeapEntry = { /** Sum of enabled stats values. */ enabledStatsTotal: number; /** Sum of all stats including disabled/maxed stats. */ statsTotal: number; /** Encoded stat mix as a 48-bit integer. */ statMix: number; /** Power level of the armor set. */ power: number; } & T; /** * Comparison: true if a is worse than b (lower priority in min-heap). * This creates a min-heap where the root is the worst item. * Ordering: tier asc, statMix asc, power asc (opposite of SetTracker for min-heap) */ function isWorse(a: HeapEntry, b: HeapEntry): boolean { if (a.enabledStatsTotal !== b.enabledStatsTotal) { return a.enabledStatsTotal < b.enabledStatsTotal; } if (a.statMix !== b.statMix) { return a.statMix < b.statMix; } if (a.statsTotal !== b.statsTotal) { return a.statsTotal < b.statsTotal; } return a.power < b.power; } /** * Min-heap based SetTracker that maintains the top N armor sets. * Uses a min-heap where the root is the worst of the top N sets. * This allows O(1) access to worst element and O(log n) operations. */ export class HeapSetTracker { private heap: HeapEntry[] = []; readonly capacity: number; constructor(capacity: number) { this.capacity = capacity; } /** * Can we insert a set with this total tier? * Fast O(1) check using the root (worst set) of our min-heap. */ couldInsert(totalTier: number): boolean { if (this.heap.length < this.capacity) { return true; } // In min-heap, root is the worst item - check if new item is better return totalTier >= this.heap[0].enabledStatsTotal; } /** * Insert a set into the heap. * Matches SetTracker behavior: allows duplicates, returns true unless trimming. */ insert(entry: HeapEntry): boolean { if (this.heap.length < this.capacity) { this.heap.push(entry); this.bubbleUp(this.heap.length - 1); return true; } // Check with full comparison if (isWorse(entry, this.heap[0])) { return false; // Not good enough after full comparison } // Replace the worst set (root) with the new one this.heap[0] = entry; this.bubbleDown(0); return false; // Match original SetTracker behavior (trimWorstSet returns false) } /** * Get the top N armor sets in order (best first). * Since we have a min-heap, we sort a copy and take the best items. */ getArmorSets(): HeapEntry[] { // Copy heap and sort in SetTracker order (best first) return this.heap.toSorted((a, b) => { // Sort by tier desc, statMix desc, power desc (opposite of min-heap order) if (a.enabledStatsTotal !== b.enabledStatsTotal) { return b.enabledStatsTotal - a.enabledStatsTotal; } if (a.statMix !== b.statMix) { return b.statMix - a.statMix; } return b.power - a.power; }); } get totalSets(): number { return this.heap.length; } // Heap maintenance methods private bubbleUp(index: number): void { while (index > 0) { const parentIndex = Math.floor((index - 1) / 2); if (isWorse(this.heap[index], this.heap[parentIndex])) { [this.heap[index], this.heap[parentIndex]] = [this.heap[parentIndex], this.heap[index]]; index = parentIndex; } else { break; } } } private bubbleDown(index: number): void { const length = this.heap.length; while (true) { let smallest = index; const leftChild = 2 * index + 1; const rightChild = 2 * index + 2; if (leftChild < length && isWorse(this.heap[leftChild], this.heap[smallest])) { smallest = leftChild; } if (rightChild < length && isWorse(this.heap[rightChild], this.heap[smallest])) { smallest = rightChild; } if (smallest !== index) { [this.heap[index], this.heap[smallest]] = [this.heap[smallest], this.heap[index]]; index = smallest; } else { break; } } } } /** * Encodes stat values into a 48-bit integer for fast comparison. * Each stat uses 8 bits (sufficient for 0-200 range), packed in priority order. * Only non-ignored stats (with maxStat > 0) are included in the encoding. * * @param stats Array of stat values in stat priority order * @param desiredStatRanges Stat ranges to determine which stats are ignored * @returns 48-bit integer representation that maintains lexical ordering */ export function encodeStatMix( stats: readonly number[], desiredStatRanges: readonly DesiredStatRange[], ): number { let encoded = 0; for (let i = 0; i < 6; i++) { const filter = desiredStatRanges[i]; if (filter.maxStat > 0) { // non-ignored stat // Use multiplication instead of bit shifting to avoid 32-bit overflow encoded = encoded * 256 + Math.min(stats[i], 255); } } return encoded; } /** * Decodes a stat mix integer back to individual stat values. * Useful for debugging or display purposes. * * @param encoded The encoded stat mix integer * @param numStats Number of stats that were encoded * @returns Array of decoded stat values */ export function decodeStatMix(encoded: number, numStats: number): number[] { const stats: number[] = []; let remaining = encoded; for (let i = 0; i < numStats; i++) { // Extract the rightmost 8 bits using modulo, then divide for next stat stats.unshift(remaining % 256); remaining = Math.floor(remaining / 256); } return stats; } ================================================ FILE: src/app/loadout-builder/process-worker/tsconfig.json ================================================ { "compilerOptions": { "strictNullChecks": true, "strictBindCallApply": true, "noUnusedParameters": true, "target": "esnext", "module": "esnext", "lib": ["webworker", "esnext"], "moduleResolution": "node", "noUnusedLocals": true, "sourceMap": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "allowJs": false, "baseUrl": ".", "types": ["node", "jest"], "paths": { "app/*": ["../../*"], "data/*": ["../../../data/*"], "testing/*": ["../../../testing/*"] } }, "include": ["./*", "../../../global.d.ts"] } ================================================ FILE: src/app/loadout-builder/process-worker/types.ts ================================================ import { ArmorBucketHash, ArmorStatHashes, ArmorStats, StatRanges } from '../types'; export interface ProcessResult { /** A small number of the best sets, depending on operation mode. */ sets: ProcessArmorSet[]; /** The total number of combinations considered. */ combos: number; /** The stat ranges of all sets that matched our filters & mod selection. */ statRangesFiltered: StatRanges; /** Statistics about how many sets passed/failed the constraints, for error reporting */ processInfo: ProcessStatistics; } /** * The minimum information about an item that is needed to consider it when * processing optimal sets. This is calculated from the full item before being * passed into the process worker. */ export interface ProcessItem { id: string; hash?: number; // Included for debugging purposes, not used in processing name?: string; // Included for debugging purposes, not used in processing isExotic: boolean; isArtifice: boolean; /** * The remaining energy capacity for this item, after assuming energy upgrades * and assigning slot-specific mods. This can be spent on stat mods. */ remainingEnergyCapacity: number; power: number; stats: { [statHash: number]: number }; /** The activity (raid) mod type that can be slotted on this item, if any. */ compatibleActivityMod?: string; setBonus?: number; /** * This is a pre-set tuning mod on the item. This hash should be passed along * to the ArmorSet.statMods list. */ includedTuningMod?: number; } export type ProcessItemsByBucket = { [bucketHash in ArmorBucketHash]: ProcessItem[]; }; export interface ProcessArmorSet { /** The overall stats for the loadout as a whole, including subclass, mods and including auto stat mods. */ readonly stats: Readonly; /** The assumed stats from the armor items themselves only. */ readonly armorStats: Readonly; /** For each armor type (see ArmorBucketHashes), this is the list of items that could interchangeably be put into this loadout. */ readonly armor: readonly string[]; /** Which stat mods were added? */ readonly statMods: number[]; /** Sum of enabled stats values. */ readonly enabledStatsTotal: number; /** Sum of all stats including disabled/maxed stats. */ readonly statsTotal: number; /** Encoded stat mix as a 48-bit integer. */ readonly statMix: number; /** Power level of the armor set. */ readonly power: number; } export interface ProcessMod { hash: number; /** The energy cost of the mod. */ energyCost: number; /** This should only be available in legacy, combat and raid mods */ tag?: string; } /** * Data describing the mods that can be automatically picked. This takes into * account the fact that stat mods for different stats cost different amounts of * energy - and sometimes there are even discounted versions that can be * unlocked. */ export interface AutoModData { generalMods: { [key in ArmorStatHashes]?: { majorMod: { hash: number; cost: number }; minorMod: { hash: number; cost: number }; }; }; artificeMods: { [key in ArmorStatHashes]?: number }; } export interface LockedProcessMods { generalMods: ProcessMod[]; activityMods: ProcessMod[]; } export interface RejectionRate { timesFailed: number; timesChecked: number; } export interface ModAssignmentStatistics { /** Mod-tag and mod element counts check. */ earlyModsCheck: RejectionRate; /** How many times we couldn't possibly hit the target stats with any number of auto mods picks */ autoModsPick: RejectionRate; finalAssignment: { /** How many times we tried mod permutations for permutations that worked. */ modAssignmentAttempted: number; /** How many times we failed to assign user-picked slot-independent mods. */ modsAssignmentFailed: number; /** How many times we failed to assign auto stat mods. */ autoModsAssignmentFailed: number; }; } /** * Information about the operation of the worker process. */ export interface ProcessStatistics { numProcessed: number; numValidSets: number; statistics: { /** Sets skipped for really uninteresting/coarse reasons. */ skipReasons: { noExotic: number; doubleExotic: number; skippedLowTier: number; insufficientSetBonus: number; }; lowerBoundsExceeded: RejectionRate; modsStatistics: ModAssignmentStatistics; }; } ================================================ FILE: src/app/loadout-builder/types.ts ================================================ import { AssumeArmorMasterwork, StatConstraint } from '@destinyitemmanager/dim-api-types'; import { D2Categories } from 'app/destiny2/d2-bucket-categories'; import { DimCharacterStat } from 'app/inventory/store-types'; import { BucketHashes, PlugCategoryHashes, StatHashes } from 'data/d2/generated-enums'; import { DimItem, PluggableInventoryItemDefinition } from '../inventory/item-types'; import { ProcessItem } from './process-worker/types'; export interface MinMaxStat { minStat: number; // 0 to 200 maxStat: number; // 0 to 200 } /** * Resolved stat constraints take the compact form of the API stat constraints * and expand them so that each stat has a corresponding constraint, the min and * max are defined, and the ignored flag is set. Tiers are replaced with exact * stat values. In the API version, stat constraints are simply missing if * ignored, and min-0/max-10 is omitted as implied. */ export interface ResolvedStatConstraint extends Required< Omit > { /** * An ignored stat has an effective maximum stat of 0, so that any stats in * excess of 0 are deemed worthless. */ ignored: boolean; } /** * When a stat is ignored, we treat it as if it were effectively a constraint * with a max desired stat of 0. DesiredStatRange is the same as StatConstraint, * but with the ignored flag removed, and maxStat set to 0 for ignored sets. */ export type DesiredStatRange = Required>; /** A map from bucketHash to the pinned item if there is one. */ export interface PinnedItems { [bucketHash: number]: DimItem | undefined; } /** A map from bucketHash to any excluded items. */ export interface ExcludedItems { [bucketHash: number]: DimItem[] | undefined; } /** * An individual "stat mix" of loadouts where each slot has a list of items with the same stat options. */ export interface ArmorSet { /** The overall stats for the loadout as a whole, including subclass, mods and including auto stat mods. */ readonly stats: Readonly; /** The assumed stats from the armor items themselves only. */ readonly armorStats: Readonly; /** For each armor type (see ArmorBucketHashes), this is the list of items in the loadout. */ readonly armor: DimItem[]; /** Which stat mods were added? */ readonly statMods: number[]; } export type ItemsByBucket = Readonly<{ [bucketHash in ArmorBucketHash]: readonly DimItem[]; }>; /** * Data describing the mods that can be automatically picked. */ export interface AutoModDefs { generalMods: { [key in ArmorStatHashes]?: { majorMod: PluggableInventoryItemDefinition; minorMod: PluggableInventoryItemDefinition; }; }; artificeMods: { [key in ArmorStatHashes]?: PluggableInventoryItemDefinition }; } /** * An item group mapping to the same process item. All items in this group * must be interchangeable subject to the armor energy rules, always, for any * given mod assignment. */ export type ItemGroup = Readonly<{ canonicalProcessItem: ProcessItem; items: DimItem[]; }>; /** A restricted set of bucket hashes for armor. */ export type ArmorBucketHash = | BucketHashes.Helmet | BucketHashes.Gauntlets | BucketHashes.ChestArmor | BucketHashes.LegArmor | BucketHashes.ClassArmor; export const ArmorBucketHashes = D2Categories.Armor as ArmorBucketHash[]; export type ModStatChanges = { [statHash in ArmorStatHashes]: Pick; }; export type ArmorStatHashes = | StatHashes.Weapons | StatHashes.Health | StatHashes.Class | StatHashes.Grenade | StatHashes.Super | StatHashes.Melee; export type StatRanges = { [statHash in ArmorStatHashes]: MinMaxStat }; export type ArmorStats = { [statHash in ArmorStatHashes]: number }; /** Do not allow the user to choose artifice/tuning mods manually in Loadout Optimizer since we're supposed to be doing that */ export const autoAssignmentPCHs = [ PlugCategoryHashes.EnhancementsArtifice, PlugCategoryHashes.CoreGearSystemsArmorTieringPlugsTuningMods, ]; /** * The reusablePlugSetHash from armour 2.0's general socket. * TODO: Find a way to generate this in d2ai. */ export const generalSocketReusablePlugSetHash = 731468111; /** * The reusablePlugSetHash for artifice armor's artifice socket, with +3 mods. * TODO: Find a way to generate this in d2ai. */ export const artificeSocketReusablePlugSetHash = 4285066582; /** * The reusablePlugSetHash for the tuning socket, which lets you trade off two * stats. * TODO: Find a way to generate this in d2ai. */ export const tuningSocketReusablePlugSetHash = 1155052024; /** Bonus to a single stat given by plugs in artifice armor's exclusive mod slot */ export const artificeStatBoost = 3; /** Bonus to a single stat given by the "half tier mods" plugs in all armor's general mod slot */ export const minorStatBoost = 5; /** * Bonus to a single stat given by the "full tier mods" plugs in all armor's general mod slot. * The fact that a major mod gives exactly 1 tier without changing the number of remainder points * is fairly engrained in some of the algorithms, so it wouldn't be quite trivial to change this. */ export const majorStatBoost = 10; /** Bonus/sacrifice made to a stat when using a tuning mod. */ export const tuningStatBoost = 5; /** Bonus to the three lowest stats when using "Balanced Tuning" */ export const balancedTuningStatBoost = 1; /** * Special value for lockedExoticHash indicating the user would not like any exotics included in their loadouts. */ export const LOCKED_EXOTIC_NO_EXOTIC = -1; /** * Special value for lockedExoticHash indicating the user would like an exotic, but doesn't care which one. */ export const LOCKED_EXOTIC_ANY_EXOTIC = -2; /** * The minimum armour energy value used in the LO Builder */ export const MIN_LO_ITEM_ENERGY = 9; /** * The armor energy rules that Loadout Optimizer uses by default. * Requires a reasonable and inexpensive amount of upgrade materials. */ export const loDefaultArmorEnergyRules: ArmorEnergyRules = { assumeArmorMasterwork: AssumeArmorMasterwork.None, minItemEnergy: MIN_LO_ITEM_ENERGY, }; /** * The armor energy rules that describe the changes DIM can * make in-game -- none as of now. */ export const inGameArmorEnergyRules: ArmorEnergyRules = { assumeArmorMasterwork: AssumeArmorMasterwork.None, minItemEnergy: 1, }; /** * Armor energy rules that allow fully masterworking everything. */ export const permissiveArmorEnergyRules: ArmorEnergyRules = { assumeArmorMasterwork: AssumeArmorMasterwork.ArtificeExotic, // implied to be 10 by the above minItemEnergy: 1, }; /** * Rules describing how armor can change energy capacity * to accommodate mods and hit optimal stats. */ export interface ArmorEnergyRules { assumeArmorMasterwork: AssumeArmorMasterwork; /** * How much energy capacity items have at least. */ minItemEnergy: number; } ================================================ FILE: src/app/loadout-builder/updated-loadout.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { convertToLoadoutItem } from 'app/loadout-drawer/loadout-utils'; import { Loadout } from 'app/loadout/loadout-types'; import { BucketHashes } from 'data/d2/generated-enums'; import { sum } from 'es-toolkit'; import { ArmorBucketHashes, ArmorSet } from './types'; /** * Create a new loadout from the original prototype loadout, but with the armor * items replaced with this loadout's armor. Used for equipping or creating a * new saved loadout. */ export function updateLoadoutWithArmorSet( defs: D2ManifestDefinitions, loadout: Loadout, set: ArmorSet, items: DimItem[], lockedMods: PluggableInventoryItemDefinition[], loadoutParameters = loadout.parameters, ): Loadout { const data = { statTotal: sum(Object.values(set.stats)), }; const existingItemsWithoutArmor = loadout.items.filter( (li) => // The new item might already be in the loadout (but unequipped), remove it !items.some((i) => i.id === li.id) && // Remove equipped armor items !( li.equip && ArmorBucketHashes.includes(defs.InventoryItem.get(li.hash)?.inventory?.bucketTypeHash ?? 0) ), ); const loadoutItems = items.map((item) => convertToLoadoutItem(item, true)); // We need to add in this set's specific stat mods (artifice, general) to the // list of user-chosen mods We can't start with the list of mods in the // existing loadout parameters because lockedMods has filtered out invalid // mods, mods that don't fit, and general mods if we're auto-assigning general // mods. const allMods = [...lockedMods.map((m) => m.hash), ...set.statMods]; return { ...loadout, parameters: { ...loadoutParameters, mods: allMods.length ? allMods : undefined, }, items: [...existingItemsWithoutArmor, ...loadoutItems], name: loadout.name ?? t('Loadouts.Generated', data), }; } /** * Create a new loadout from an original prototype loadout, using mods and * subclass from another loadout, and the items from an armor set. Used for the * "compare loadout" drawer. */ export function mergeLoadout( defs: D2ManifestDefinitions, originalLoadout: Loadout, newLoadout: Loadout, set: ArmorSet, items: DimItem[], lockedMods: PluggableInventoryItemDefinition[], ): Loadout { const loadoutWithArmorSet = updateLoadoutWithArmorSet( defs, originalLoadout, set, items, lockedMods, newLoadout.parameters, ); loadoutWithArmorSet.parameters = { ...newLoadout.parameters, mods: loadoutWithArmorSet.parameters?.mods, }; const newSubclass = newLoadout.items.find( (li) => defs.InventoryItem.get(li.hash)?.inventory?.bucketTypeHash === BucketHashes.Subclass, ); if (newSubclass) { const itemsWithoutSubclass = loadoutWithArmorSet.items.filter( (li) => defs.InventoryItem.get(li.hash)?.inventory?.bucketTypeHash !== BucketHashes.Subclass, ); itemsWithoutSubclass.push(newSubclass); loadoutWithArmorSet.items = itemsWithoutSubclass; } return loadoutWithArmorSet; } ================================================ FILE: src/app/loadout-builder/useEquippedHashes.ts ================================================ import { LoadoutParameters } from '@destinyitemmanager/dim-api-types'; import { getSubclassPlugHashes } from 'app/loadout/loadout-item-utils'; import { ResolvedLoadoutItem } from 'app/loadout/loadout-types'; import { useMemo } from 'react'; export default function useEquippedHashes( params: LoadoutParameters, subclass: ResolvedLoadoutItem | undefined, ) { return useMemo(() => { const exoticArmorHash = params.exoticArmorHash; // Fill in info about selected items / subclass options for Clarity character stats const equippedHashes = new Set(); if (exoticArmorHash) { equippedHashes.add(exoticArmorHash); } for (const { plugHash } of getSubclassPlugHashes(subclass)) { equippedHashes.add(plugHash); } return equippedHashes; }, [params.exoticArmorHash, subclass]); } ================================================ FILE: src/app/loadout-builder/utils.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import { ProcessItem } from './process-worker/types'; /** Gets the effective stat tier from a stat value, clamping between 0-10 */ export function statTier(stat: number) { return Math.min(Math.max(Math.floor(stat / 10), 0), 10); } /** * Calculates the remainder of euclidean division `dividend / divisor`, * i.e. returns `rem` such that `dividend = divisor * n + rem` and * `0 <= rem < |divisor|`. * Remainder is always non-negative, behavior differs from `%` for * negative dividends. Find comparisons at * https://en.wikipedia.org/wiki/Modulo_operation#Variants_of_the_definition */ export function remEuclid(dividend: number, divisor: number) { return ((dividend % divisor) + divisor) % divisor; } /** * Get the maximum average power for a particular set of armor. */ export function getPower(items: DimItem[] | ProcessItem[]) { let power = 0; let numPoweredItems = 0; for (const item of items) { if (item.power) { power += item.power; numPoweredItems++; } } return Math.floor(power / numPoweredItems); } ================================================ FILE: src/app/loadout-drawer/LoadoutDrawer.m.scss ================================================ @use '../variables.scss' as *; .body { padding: 10px; @include desktop { max-height: 50vh; } } .notes { font-size: 14px; margin-top: 4px; summary { font-weight: bold; cursor: pointer; } &[open] summary { margin-bottom: 4px; } } // Group together buttons so they don't wrap onto different lines .inputGroup { display: flex; flex-flow: row wrap; align-items: center; gap: 4px; } .header { composes: flexRow from '../dim-ui/common.m.scss'; gap: 0 12px; } .headerDetails { composes: flexColumn from '../dim-ui/common.m.scss'; flex: 1; gap: 4px; } .classType { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; gap: 0.25em; svg { height: 16px; } } ================================================ FILE: src/app/loadout-drawer/LoadoutDrawer.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'body': string; 'classType': string; 'header': string; 'headerDetails': string; 'inputGroup': string; 'notes': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-drawer/LoadoutDrawer.tsx ================================================ import { apiPermissionGrantedSelector } from 'app/dim-api/selectors'; import { AlertIcon } from 'app/dim-ui/AlertIcon'; import CheckButton from 'app/dim-ui/CheckButton'; import ClassIcon from 'app/dim-ui/ClassIcon'; import { WithSymbolsPicker } from 'app/dim-ui/destiny-symbols/SymbolsPicker'; import { useAutocomplete } from 'app/dim-ui/text-complete/text-complete'; import { t } from 'app/i18next-t'; import { getStore } from 'app/inventory/stores-helpers'; import InGameLoadoutIdentifiersSelectButton from 'app/loadout/ingame/InGameLoadoutIdentifiersSelectButton'; import { useDefinitions } from 'app/manifest/selectors'; import { searchFilterSelector } from 'app/search/items/item-search-filter'; import { AppIcon, addIcon, faRandom } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { stubTrue } from 'app/utils/functions'; import { useEventBusListener } from 'app/utils/hooks'; import { infoLog, warnLog } from 'app/utils/log'; import { useHistory } from 'app/utils/undo-redo-history'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import React, { useCallback, useRef } from 'react'; import { useSelector } from 'react-redux'; import TextareaAutosize from 'react-textarea-autosize'; import Sheet from '../dim-ui/Sheet'; import { DimItem } from '../inventory/item-types'; import { allItemsSelector, artifactUnlocksSelector, storesSelector, unlockedPlugSetItemsSelector, } from '../inventory/selectors'; import { deleteLoadout, updateLoadout } from '../loadout/actions'; import LoadoutEdit from '../loadout/loadout-edit/LoadoutEdit'; import { Loadout } from '../loadout/loadout-types'; import { loadoutsHashtagsSelector } from '../loadout/selectors'; import * as styles from './LoadoutDrawer.m.scss'; import LoadoutDrawerDropTarget from './LoadoutDrawerDropTarget'; import LoadoutDrawerFooter from './LoadoutDrawerFooter'; import LoadoutDrawerHeader from './LoadoutDrawerHeader'; import { LoadoutUpdateFunction, addItem, fillLoadoutFromEquipped, fillLoadoutFromUnequipped, randomizeFullLoadout, setClassType, setName, setNotes, } from './loadout-drawer-reducer'; import { addItem$ } from './loadout-events'; import { filterLoadoutToAllowedItems } from './loadout-utils'; /** * The Loadout editor that shows up as a sheet on the Inventory screen. You can build and edit * loadouts from this interface. * * This component will always be launched after defs/stores are loaded. */ export default function LoadoutDrawer({ initialLoadout, storeId, fromExternal, onClose, }: { initialLoadout: Loadout; /** * The store that provides context to how this loadout is being edited from. * The store this edit session was launched from. This is to help pick which * mods are enabled, which subclass items to show, etc. */ storeId: string; fromExternal: boolean; onClose: () => void; }) { const dispatch = useThunkDispatch(); const defs = useDefinitions()!; const stores = useSelector(storesSelector); const allItems = useSelector(allItemsSelector); const unlockedPlugs = useSelector(unlockedPlugSetItemsSelector(storeId)); const searchFilter = useSelector(searchFilterSelector); const { state: loadout, setState: setLoadout, undo, redo, canUndo, canRedo, } = useHistory(initialLoadout); const apiPermissionGranted = useSelector(apiPermissionGrantedSelector); function withUpdater(fn: (...args: T) => LoadoutUpdateFunction) { return (...args: T) => setLoadout(fn(...args)); } const store = getStore(stores, storeId); const onAddItem = useCallback( (item: DimItem, equip?: boolean) => setLoadout(addItem(defs, item, equip)), [defs, setLoadout], ); /** * If an item comes in on the addItem$ observable, add it. */ useEventBusListener(addItem$, onAddItem); const handleSaveLoadout = (e: React.FormEvent, close: () => void, saveAsNew: boolean) => { e.preventDefault(); if (!loadout) { return; } let loadoutToSave = loadout; if (saveAsNew) { loadoutToSave = { ...loadout, id: globalThis.crypto.randomUUID(), // Let it be a new ID }; } if (loadoutToSave.name === t('Loadouts.FromEquipped')) { loadoutToSave = { ...loadoutToSave, name: `${loadoutToSave.name} ${new Date().toLocaleString()}`, }; } loadoutToSave = filterLoadoutToAllowedItems(defs, loadoutToSave); if ( $featureFlags.warnNoSync && !apiPermissionGranted && 'storage' in navigator && 'persist' in navigator.storage ) { navigator.storage.persist().then((isPersisted) => { if (isPersisted) { infoLog('storage', 'Persisted storage granted'); } else { warnLog('storage', 'Persisted storage not granted'); } }); } dispatch(updateLoadout(loadoutToSave)); close(); }; const ref = useRef(null); const tags = useSelector(loadoutsHashtagsSelector); useAutocomplete(ref, tags); const artifactUnlocks = useSelector(artifactUnlocksSelector(storeId)); if (!loadout || !store) { return null; } const handleDeleteLoadout = (close: () => void) => { dispatch(deleteLoadout(loadout.id)); close(); }; const handleNotesChanged: React.ChangeEventHandler = (e) => setLoadout(setNotes(e.target.value)); const handleNameChanged = withUpdater(setName); const handleFillLoadoutFromEquipped = () => setLoadout(fillLoadoutFromEquipped(defs, store, artifactUnlocks)); const handleFillLoadoutFromUnequipped = () => setLoadout(fillLoadoutFromUnequipped(defs, store)); const handleRandomizeLoadout = () => setLoadout(randomizeFullLoadout(defs, store, allItems, searchFilter, unlockedPlugs)); const toggleAnyClass = (checked: boolean) => setLoadout(setClassType(checked ? DestinyClass.Unknown : store.classType)); const showInGameLoadoutIdentifiers = $featureFlags.editInGameLoadoutIdentifiers && (Boolean(loadout.parameters?.inGameIdentifiers) || loadout.items.length > 0); const header = (
{showInGameLoadoutIdentifiers && ( )}
{fromExternal && (
{t('Loadouts.ClassType', { className: store.className, context: store.genderName })}
)}
{t('MovePopup.Notes')} setLoadout(setNotes(val))}>
); const footer = ({ onClose }: { onClose: () => void }) => ( handleSaveLoadout(e, onClose, saveAsNew)} onDeleteLoadout={() => handleDeleteLoadout(onClose)} undo={undo} redo={redo} hasUndo={canUndo} hasRedo={canRedo} /> ); // TODO: minimize for better dragging/picking? // TODO: how to choose equipped/unequipped // TODO: contextual buttons! // TODO: undo/redo stack? // TODO: build and publish a "loadouts API" via context? return ( {$featureFlags.warnNoSync && !apiPermissionGranted && (

{t('Storage.DimSyncNotEnabled')}

)}
{t('Loadouts.Any')}
); } ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerContainer.tsx ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { storesSelector } from 'app/inventory/selectors'; import { getCurrentStore } from 'app/inventory/stores-helpers'; import { warnMissingClass } from 'app/loadout-builder/loadout-builder-reducer'; import { decodeUrlLoadout } from 'app/loadout/loadout-share/loadout-import'; import { useD2Definitions } from 'app/manifest/selectors'; import { showNotification } from 'app/notifications/notifications'; import { errorMessage } from 'app/utils/errors'; import { useEventBusListener } from 'app/utils/hooks'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { Suspense, lazy, useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { useLocation, useNavigate } from 'react-router'; import { EditLoadoutState, addItem$, editLoadout$ } from './loadout-events'; import { convertToLoadoutItem, newLoadout, pickBackingStore } from './loadout-utils'; const LoadoutDrawer = lazy( () => import(/* webpackChunkName: "loadout-drawer" */ './LoadoutDrawer'), ); const D1LoadoutDrawer = lazy( () => import( /* webpackChunkName: "d1-loadout-drawer" */ 'app/destiny1/loadout-drawer/D1LoadoutDrawer' ), ); /** * A launcher for the LoadoutDrawer. This is used both so we can lazy-load the * LoadoutDrawer, and so we can make sure defs are defined when the loadout * drawer is rendered. */ export default function LoadoutDrawerContainer({ account }: { account: DestinyAccount }) { const defs = useD2Definitions(); const navigate = useNavigate(); const { search: queryString, pathname } = useLocation(); // This state only holds the initial version of the loadout that launches the // drawer - after that, the loadout drawer itself manages edits to the // loadout. // TODO: Alternately we could come up with the concept of a // `useControlledReducer` that applied a reducer to mutate an object whose // state is handled outside the component. const [initialLoadout, setInitialLoadout] = useState(); const handleDrawerClose = useCallback(() => { setInitialLoadout(undefined); }, []); const stores = useSelector(storesSelector); // The loadout to edit comes in from the editLoadout$ observable useEventBusListener( editLoadout$, useCallback( (state) => { const { storeId, loadout } = state; // Fall back to current store because otherwise there's no way to delete loadouts // the user doesn't have a class for. const editingStore = pickBackingStore(stores, storeId, loadout.classType) ?? getCurrentStore(stores); if (!editingStore) { if (defs) { warnMissingClass(loadout.classType, defs); } return; } setInitialLoadout({ ...state, storeId: editingStore.id }); }, [stores, defs], ), ); const hasInitialLoadout = Boolean(initialLoadout); // Only react to add item if there's not a loadout open (otherwise it'll be handled in the loadout drawer!) useEventBusListener( addItem$, useCallback( (item: DimItem) => { if (!hasInitialLoadout) { // If we don't have a loadout, this action was invoked via the "+ Loadout" button // in item actions, so pick the best store to back this loadout with const owner = pickBackingStore(stores, item.owner, item.classType); if (!owner) { showNotification({ type: 'warning', title: t('Loadouts.ClassTypeMissing', { className: item.classTypeNameLocalized }), }); return; } const classType = item.classType === DestinyClass.Unknown ? owner.classType : item.classType; const draftLoadout = newLoadout('', [], classType); draftLoadout.items.push(convertToLoadoutItem(item, true)); setInitialLoadout({ loadout: draftLoadout, storeId: owner.id, showClass: true, fromExternal: true, }); } }, [hasInitialLoadout, stores], ), ); // Load in a full loadout specified in the URL useEffect(() => { if (!stores.length || !defs?.isDestiny2) { return; } try { const parsedLoadout = decodeUrlLoadout(queryString); if (parsedLoadout) { const storeId = pickBackingStore(stores, undefined, parsedLoadout.classType)?.id; if (!storeId) { warnMissingClass(parsedLoadout.classType, defs); return; } setInitialLoadout({ loadout: parsedLoadout, storeId, showClass: false, fromExternal: true, }); // Clear the loadout from params if the URL contained one... navigate(pathname, { replace: true }); } } catch (e) { showNotification({ type: 'error', title: t('Loadouts.BadLoadoutShare'), body: t('Loadouts.BadLoadoutShareBody', { error: errorMessage(e) }), }); // ... or if it contained errors navigate(pathname, { replace: true }); } }, [defs, queryString, navigate, pathname, stores]); // Close the loadout on navigation // TODO: prompt for saving? useEffect(() => { // Don't close if moving to the inventory or loadouts screen if (!pathname.endsWith('inventory') && !pathname.endsWith('loadouts')) { handleDrawerClose(); } }, [handleDrawerClose, pathname]); if (initialLoadout) { return ( {account.destinyVersion === 2 ? ( ) : ( )} ); } return null; } ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerDropTarget.m.scss ================================================ .over { box-shadow: inset 0 0 6px 0 rgb(200, 200, 200, 0.7); } ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerDropTarget.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'over': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerDropTarget.tsx ================================================ import { bucketsSelector, storesSelector } from 'app/inventory/selectors'; import { emptyArray } from 'app/utils/empty'; import { isItemLoadoutCompatible, itemCanBeInLoadout } from 'app/utils/item-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import React from 'react'; import { DropTargetMonitor, useDrop } from 'react-dnd'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import { DimItem } from '../inventory/item-types'; import * as styles from './LoadoutDrawerDropTarget.m.scss'; export const bucketTypesSelector = createSelector( bucketsSelector, storesSelector, (buckets, stores) => buckets ? [ 'postmaster', // TODO: we don't really need every possible bucket right? ...Object.values(buckets.byHash).flatMap((bucket) => [ bucket.hash.toString(), ...stores.flatMap((store) => `${store.id}-${bucket.hash}`), ]), ] : emptyArray(), ); export default function LoadoutDrawerDropTarget({ children, className, classType, onDroppedItem, }: { children?: React.ReactNode; className?: string; classType: DestinyClass; onDroppedItem: (item: DimItem, equip?: boolean) => void; }) { const bucketTypes = useSelector(bucketTypesSelector); const [{ isOver }, dropRef] = useDrop( () => ({ accept: bucketTypes, drop: (item: DimItem, monitor: DropTargetMonitor) => { const result = monitor.getDropResult(); onDroppedItem(item, result?.equipped); }, canDrop: (i) => itemCanBeInLoadout(i) && isItemLoadoutCompatible(i.classType, classType), collect: (monitor) => ({ isOver: monitor.isOver() && monitor.canDrop() }), }), [bucketTypes, onDroppedItem], ); return (
{ dropRef(el); }} > {children}
); } ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerFooter.m.scss ================================================ @use '../variables.scss' as *; .loadoutOptions { width: 100%; @include phone-portrait { font-size: 14px; } form { display: flex; flex-flow: row wrap; margin-right: 4px; margin-bottom: 4px; width: 100%; gap: 4px; &:last-child { margin-bottom: 0; } > a { margin-left: auto; } } summary { font-weight: bold; margin-bottom: 4px; } details { margin-top: 4px; } } ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerFooter.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'loadoutOptions': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerFooter.tsx ================================================ import { ConfirmButton } from 'app/dim-ui/ConfirmButton'; import { PressTip } from 'app/dim-ui/PressTip'; import UserGuideLink from 'app/dim-ui/UserGuideLink'; import { t } from 'app/i18next-t'; import { loadoutSavedSelector } from 'app/loadout/selectors'; import { AppIcon, deleteIcon, redoIcon, undoIcon } from 'app/shell/icons'; import { RootState } from 'app/store/types'; import { isEmpty } from 'app/utils/collections'; import { isClassCompatible } from 'app/utils/item-utils'; import { currySelector } from 'app/utils/selectors'; import React from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import { Loadout } from '../loadout/loadout-types'; import { loadoutsSelector } from '../loadout/loadouts-selector'; import * as styles from './LoadoutDrawerFooter.m.scss'; /** * Find a loadout with the same name that could overlap with this one * Note that this might be the saved version of this very same loadout! */ const clashingLoadoutSelector = currySelector( createSelector( loadoutsSelector, (_: RootState, loadout: Loadout) => loadout, (loadouts, loadout) => loadouts.find( (l) => loadout.name === l.name && isClassCompatible(l.classType, loadout.classType), ), ), ); export default function LoadoutDrawerFooter({ loadout, onSaveLoadout, onDeleteLoadout, undo, redo, hasUndo, hasRedo, }: { loadout: Readonly; undo?: () => void; redo?: () => void; hasUndo?: boolean; hasRedo?: boolean; onSaveLoadout: (e: React.FormEvent, saveAsNew: boolean) => void; onDeleteLoadout: () => void; }) { const isSaved = useSelector(loadoutSavedSelector(loadout.id)); const clashingLoadout = useSelector(clashingLoadoutSelector(loadout)); // There's an existing loadout with the same name & class and it's not the loadout we are currently editing const clashesWithAnotherLoadout = clashingLoadout && clashingLoadout.id !== loadout.id; const saveDisabledReasons: string[] = []; if (!loadout.name.length) { saveDisabledReasons.push(t('Loadouts.SaveDisabled.NoName')); } if (clashesWithAnotherLoadout) { saveDisabledReasons.push(t('Loadouts.SaveDisabled.AlreadyExists')); } const loadoutEmpty = !loadout.items.length && // Allow mod only loadouts !loadout.parameters?.mods?.length && !loadout.parameters?.clearMods && // Allow fashion only loadouts isEmpty(loadout.parameters?.modsByBucket); if (loadoutEmpty) { saveDisabledReasons.push(t('Loadouts.SaveDisabled.Empty')); } const saveDisabled = saveDisabledReasons.length > 0; // Don't show "Save as New" if this is a new loadout or we haven't changed the name const showSaveAsNew = isSaved; const saveAsNewDisabled = saveDisabled || // There's an existing loadout with the same name & class Boolean(clashingLoadout); return (
onSaveLoadout(e, !isSaved)}> 0 ? saveDisabledReasons.map((reason) =>
{reason}
) : undefined } >
{showSaveAsNew && ( )} {isSaved && ( )} {undo && ( )} {redo && ( )}
); } ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerHeader.m.scss ================================================ @use '../variables.scss' as *; .dimInput { composes: flexRow from '../dim-ui/common.m.scss'; padding: 2px 25px 2px 5px; height: 23px; background: var(--theme-input-bg); border: none; color: var(--theme-text); width: 100%; font-size: 18px; max-width: 50em; box-sizing: border-box; @include phone-portrait { height: 32px; } @include interactive($hover: true, $focusWithin: true) { outline: none; box-shadow: inset 0 0 0 1px var(--theme-search-dropdown-border); } ::placeholder { color: #999; } > input { all: initial; outline: none; flex: 1; font: inherit; color: inherit; caret-color: var(--theme-accent-primary); // because our fake input is higher and this is used by textcomplete line-height: 1.5; } // Vertically center the symbol picker > *:last-child { height: 100%; display: flex; flex-direction: column; justify-content: center; } } ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerHeader.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'dimInput': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/loadout-drawer/LoadoutDrawerHeader.tsx ================================================ import { WithSymbolsPicker } from 'app/dim-ui/destiny-symbols/SymbolsPicker'; import { useAutocomplete } from 'app/dim-ui/text-complete/text-complete'; import { t } from 'app/i18next-t'; import React, { useRef } from 'react'; import { useSelector } from 'react-redux'; import { Loadout } from '../loadout/loadout-types'; import { loadoutsHashtagsSelector } from '../loadout/selectors'; import * as styles from './LoadoutDrawerHeader.m.scss'; export default function LoadoutDrawerHeader({ loadout, onNameChanged, }: { loadout: Readonly; onNameChanged: (name: string) => void; }) { const setName = (e: React.ChangeEvent) => onNameChanged(e.target.value); const inputRef = useRef(null); const tags = useSelector(loadoutsHashtagsSelector); useAutocomplete(inputRef, tags); return ( ); } ================================================ FILE: src/app/loadout-drawer/auto-loadouts.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { SocketOverrides } from 'app/inventory/store/override-sockets'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { ItemRarityMap } from 'app/search/d2-known-values'; import { ItemFilter } from 'app/search/filter-types'; import { mapValues, sumBy } from 'app/utils/collections'; import { isD1Item, itemCanBeEquippedBy } from 'app/utils/item-utils'; import { aspectSocketCategoryHashes, fragmentSocketCategoryHashes, getSocketsByCategoryHashes, subclassAbilitySocketCategoryHashes, } from 'app/utils/socket-utils'; import { BucketHashes } from 'data/d2/generated-enums'; import { sample } from 'es-toolkit'; import { DimItem, DimSocket } from '../inventory/item-types'; import { DimStore } from '../inventory/store-types'; import { Loadout } from '../loadout/loadout-types'; import { convertToLoadoutItem, getLoadoutSubclassFragmentCapacity, newLoadout, optimalItemSet, optimalLoadout, } from './loadout-utils'; /** * A dynamic loadout set up to level weapons and armor */ export function itemLevelingLoadout(allItems: DimItem[], store: DimStore): Loadout { const applicableItems = allItems.filter( (i) => isD1Item(i) && itemCanBeEquippedBy(i, store) && i.talentGrid && !i.talentGrid.xpComplete && // Still need XP i.hash !== 2168530918 && // Husk of the pit has a weirdo one-off xp mechanic i.hash !== 3783480580 && i.hash !== 2576945954 && i.hash !== 1425539750, ); const bestItemFn = (item: DimItem) => { let value = 0; if (item.owner === store.id) { // Prefer items owned by this character value += 0.5; // Leave equipped items alone if they need XP, and on the current character if (item.equipped) { return 1000; } } else if (item.owner === 'vault') { // Prefer items in the vault over items owned by a different character // (but not as much as items owned by this character) value += 0.05; } // Prefer locked items (they're stuff you want to use/keep) if (item.locked) { value += 500; } value += ItemRarityMap[item.rarity] * 10; // Choose the item w/ the highest XP if (isD1Item(item) && item.talentGrid) { value += 10 * (item.talentGrid.totalXP / item.talentGrid.totalXPRequired); } value += item.power / 1000; return value; }; return optimalLoadout(applicableItems, store, bestItemFn, t('Loadouts.ItemLeveling')); } /** * A loadout that's dynamically calculated to maximize Light level (preferring not to change currently-equipped items) */ export function maxLightLoadout(allItems: DimItem[], store: DimStore): Loadout { const { equippable } = maxLightItemSet(allItems, store); const maxLightLoadout = newLoadout( store.destinyVersion === 2 ? t('Loadouts.MaximizePower') : t('Loadouts.MaximizeLight'), equippable.map((i) => convertToLoadoutItem(i, true)), store.classType, ); return maxLightLoadout; } /** * A loadout that's dynamically calculated to maximize Light level (preferring not to change currently-equipped items) */ export function maxLightItemSet( allItems: DimItem[], store: DimStore, ): ReturnType { const applicableItems: DimItem[] = []; for (const i of allItems) { if ((i.power && i.bucket.inWeapons) || i.bucket.inArmor) { applicableItems.push(i); } } const bestItemFn = (item: DimItem) => { let value = item.power; // Break ties when items have the same stats. Note that this should only // add less than 0.25 total, since in the exotics special case there can be // three items in consideration and you don't want to go over 1 total. if (item.owner === store.id) { // Prefer items owned by this character value += 0.1; if (item.equipped) { // Prefer them even more if they're already equipped value += 0.1; } } else if (item.owner === 'vault') { // Prefer items in the vault over items owned by a different character // (but not as much as items owned by this character) value += 0.05; } return value; }; return optimalItemSet(applicableItems, store, bestItemFn); } /** * A loadout to maximize a specific stat */ export function maxStatLoadout(statHash: number, allItems: DimItem[], store: DimStore): Loadout { const applicableItems = allItems.filter( (i) => i.power && i.stats?.some((stat) => stat.statHash === statHash) && // contains our selected stat itemCanBeEquippedBy(i, store, true), ); const bestItemFn = (item: DimItem) => { let value = item.stats!.find((stat) => stat.statHash === statHash)!.value; // Break ties when items have the same stats. Note that this should only // add less than 0.25 total, since in the exotics special case there can be // three items in consideration and you don't want to go over 1 total. if (item.owner === store.id) { // Prefer items owned by this character value += 0.1; if (item.equipped) { // Prefer them even more if they're already equipped value += 0.1; } } else if (item.owner === 'vault') { // Prefer items in the vault over items owned by a different character // (but not as much as items owned by this character) value += 0.05; } return value; }; return optimalLoadout(applicableItems, store, bestItemFn, t('Loadouts.MaximizeStat')); } /** * Move a list of items to a store */ export function itemMoveLoadout(items: DimItem[], store: DimStore): Loadout { // Don't move things from the postmaster or that can't move items = items.filter((i) => !i.location.inPostmaster && !i.notransfer); items = addUpStackables(items); const itemsByType = mapValues( Object.groupBy(items, (i) => i.bucket.hash), (items) => limitToBucketSize(items, store), ); // Copy the items and mark them equipped and put them in arrays, so they look like a loadout const finalItems = Object.values(itemsByType) .flat() .map((i) => convertToLoadoutItem(i, false)); return newLoadout(t('Loadouts.FilteredItems'), finalItems); } /** * Take a number of items from the list of items we mean to transfer to store, * such that no more items are selected than can fit into the destination * bucket, and the equipped item won't be changed. */ function limitToBucketSize(items: DimItem[], store: DimStore) { if (!items.length) { return []; } const item = items[0]; const isVault = store.isVault; const bucket = isVault ? item.bucket.vaultBucket : item.bucket; if (!bucket) { return isVault ? items : items.slice(0, 9); } const enum BucketLocation { AlreadyThereAndEquipped, AlreadyThereAndUnequipped, NotThere, } // Separate out any item that's already on the target store and further by equipped and not equipped - // this won't count against our space. const { [BucketLocation.AlreadyThereAndEquipped]: alreadyEquipped = [], [BucketLocation.AlreadyThereAndUnequipped]: alreadyUnequipped = [], [BucketLocation.NotThere]: otherItems = [], } = Object.groupBy(items, (item) => item.owner === store.id ? item.equipped ? BucketLocation.AlreadyThereAndEquipped : BucketLocation.AlreadyThereAndUnequipped : BucketLocation.NotThere, ); // TODO: this doesn't take into account stacks that need to split return ( // move the ones that are already there to the front to minimize moves [...alreadyEquipped, ...alreadyUnequipped, ...otherItems].slice( 0, // If a matching item is already equipped we can take 10, otherwise we have // to subtract one for the equipped item because we don't want to displace // it bucket.capacity - (item.equipment && !alreadyEquipped.length ? 1 : 0), ) ); } // Add up stackable items so we don't have duplicates. This helps us actually move them, see // https://github.com/DestinyItemManager/DIM/issues/2691#issuecomment-373970255 function addUpStackables(items: DimItem[]) { return Object.values(Object.groupBy(items, (t) => t.hash)).flatMap((items) => { if (items[0].maxStackSize > 1) { const item = { ...items[0], amount: sumBy(items, (i) => i.amount) }; return [item]; } else { return items; } }); } const randomLoadoutTypes = new Set([ BucketHashes.Subclass, BucketHashes.KineticWeapons, BucketHashes.EnergyWeapons, BucketHashes.PowerWeapons, BucketHashes.Helmet, BucketHashes.Gauntlets, BucketHashes.ChestArmor, BucketHashes.LegArmor, BucketHashes.ClassArmor, D1BucketHashes.Artifact, BucketHashes.Ghost, BucketHashes.Vehicle, BucketHashes.Ships, BucketHashes.Emblems, ]); /** * Create a random loadout from items across the whole inventory. Optionally filter items with the filter method. */ export function randomLoadout(store: DimStore, allItems: DimItem[], filter: ItemFilter) { // Do not allow random loadouts to pull cosmetics from other characters or the vault because it's obnoxious const onAcceptableRandomizeStore = (item: DimItem) => item.bucket.sort !== 'General' || item.owner === store.id; // Any item equippable by this character in the given types const applicableItems = allItems.filter( (i) => randomLoadoutTypes.has(i.bucket.hash) && itemCanBeEquippedBy(i, store) && onAcceptableRandomizeStore(i) && filter(i), ); // Use "random" as the value function return optimalLoadout(applicableItems, store, () => Math.random(), t('Loadouts.Random')); } export function randomSubclassConfiguration( defs: D2ManifestDefinitions, item: DimItem, ): SocketOverrides | undefined { if (!item.sockets) { return undefined; } const socketOverrides: SocketOverrides = {}; // Pick abilities const abilityAndSuperSockets = getSocketsByCategoryHashes( item.sockets, subclassAbilitySocketCategoryHashes, ); for (const socket of abilityAndSuperSockets) { // Stasis has no super plugSet if (socket.plugSet) { socketOverrides[socket.socketIndex] = sample(socket.plugSet.plugs).plugDef.hash; } } const randomizeSocketSeries = (sockets: DimSocket[], maxCount: number) => { if (sockets.length && maxCount > 0) { const blockedPlugs = [sockets[0].emptyPlugItemHash]; for (const socket of sockets) { if (maxCount === 0) { break; } maxCount--; const chosenHash = sample( socket.plugSet!.plugs.filter((plug) => !blockedPlugs.includes(plug.plugDef.hash)), ).plugDef.hash; if (chosenHash === undefined) { break; } socketOverrides[socket.socketIndex] = chosenHash; blockedPlugs.push(chosenHash); } } }; // Pick aspects const aspectSockets = getSocketsByCategoryHashes(item.sockets, aspectSocketCategoryHashes); randomizeSocketSeries(aspectSockets, aspectSockets.length); // Pick as many fragments as allowed const resolved = { item, loadoutItem: { ...convertToLoadoutItem(item, false), socketOverrides }, }; const fragmentCount = getLoadoutSubclassFragmentCapacity(defs, resolved, true); const fragmentSockets = getSocketsByCategoryHashes(item.sockets, fragmentSocketCategoryHashes); randomizeSocketSeries(fragmentSockets, fragmentCount); return socketOverrides; } ================================================ FILE: src/app/loadout-drawer/loadout-apply-state.ts ================================================ // State tracking and publishing for the loadout apply process. import { DimItem } from 'app/inventory/item-types'; import { Observable } from 'app/utils/observable'; import { produce } from 'immer'; /** * What part of the loadout application process are we currently in? */ export const enum LoadoutApplyPhase { NotStarted, /** De-equip loadout items from other characters so they can be moved */ Deequip, /** Moving items to the selected store */ MoveItems, /** Equip any items marked as equip */ EquipItems, /** Apply perk/socket configurations to specific items */ SocketOverrides, /** Assign mods to armor and apply them */ ApplyMods, /** Clear out empty space */ ClearSpace, /** Applying in game loadout */ InGameLoadout, /** Terminal state, loadout succeeded */ Succeeded, /** Terminal state, loadout failed */ Failed, } export const enum LoadoutItemState { Pending, /** A successful state (maybe we don't need to distinguish this) for items that didn't need to be moved. */ AlreadyThere, DequippedPendingMove, /** The item was moved but still needs to be equipped */ MovedPendingEquip, Succeeded, FailedDequip, FailedMove, FailedEquip, } export interface LoadoutItemResult { readonly item: DimItem; readonly state: LoadoutItemState; readonly equip: boolean; readonly error?: Error; } export const enum LoadoutModState { Pending, Unassigned, Applied, Failed, } export interface LoadoutModResult { readonly modHash: number; readonly state: LoadoutModState; readonly error?: Error; } export const enum LoadoutSocketOverrideState { Pending, Applied, Failed, } export interface LoadoutSocketOverrideResult { readonly item: DimItem; readonly results: { readonly [socketIndex: number]: { readonly plugHash: number; readonly state: LoadoutSocketOverrideState; readonly error?: Error; }; }; } /** * This is the current state of the loadout application, which can be accessed * or updated from within the loadout application process. It also powers the * loadout progress and result notification. */ export interface LoadoutApplyState { /** What phase of the loadout application process are we currently on? */ readonly phase: LoadoutApplyPhase; /** * This is set if we've discovered that the player is in a location that does * not allow equipping items/mods. We will short circuit many actions and show * a custom result. */ readonly equipNotPossible: boolean; /** * For each item in the loadout, how did it fare in the loadout application process? */ readonly itemStates: { readonly [itemIndex: string]: LoadoutItemResult; }; /** * For each item with socket overrides, how did the overrides go? */ readonly socketOverrideStates: { readonly [itemIndex: string]: LoadoutSocketOverrideResult; }; /** * For each mod to be applied, how did it go? */ // TODO: how to get a consistent display sort? readonly modStates: LoadoutModResult[]; /** Whether the in game loadout could not be equipped because you're in an activity. */ readonly inGameLoadoutInActivity: boolean; } export type LoadoutStateGetter = () => LoadoutApplyState; export type LoadoutStateUpdater = (update: (state: LoadoutApplyState) => LoadoutApplyState) => void; /** * Build a state tracker that can be subscribed to, checked, and updated. This * is basically a little private Redux state per-loadout-application - we could * go through Redux but it seems like too much. */ export function makeLoadoutApplyState(): [ /** Get the current state */ get: LoadoutStateGetter, /** Set the current state to a new state */ set: LoadoutStateUpdater, /** An observable that can be used to subscribe to state updates. */ observable: Observable, ] { // TODO: fill in more of the initial state from the loadout, or wait for loadout-apply to do it? const initialLoadoutApplyState: LoadoutApplyState = { phase: LoadoutApplyPhase.NotStarted, equipNotPossible: false, itemStates: {}, socketOverrideStates: {}, modStates: [], inGameLoadoutInActivity: false, }; const observable = new Observable(initialLoadoutApplyState); const get = () => observable.getCurrentValue(); const set = (update: (state: LoadoutApplyState) => LoadoutApplyState) => { const newState = update(observable.getCurrentValue()); observable.next(newState); }; return [get, set, observable]; } export function setLoadoutApplyPhase(phase: LoadoutApplyPhase) { return (state: LoadoutApplyState) => ({ ...state, phase, }); } export function setModResult(result: LoadoutModResult, equipNotPossible?: boolean) { return produce((state) => { const mod = state.modStates.find( (m) => m.modHash === result.modHash && m.state === LoadoutModState.Pending, ); if (mod) { mod.state = result.state; mod.error = result.error; } else { state.modStates.push(result); } state.equipNotPossible ||= equipNotPossible || false; }); } export function setSocketOverrideResult( item: DimItem, socketIndex: number, socketState: LoadoutSocketOverrideState, error?: Error, equipNotPossible?: boolean, ) { return produce((state) => { const thisSocketResult = state.socketOverrideStates[item.index].results[socketIndex]; // don't insert a state or error for anything that wasn't given an initial tracking state if (!thisSocketResult) { return; } thisSocketResult.state = socketState; thisSocketResult.error = error; state.equipNotPossible ||= equipNotPossible || false; }); } /** * Has any part of the loadout application process failed? */ export function anyActionFailed(state: LoadoutApplyState) { if (state.inGameLoadoutInActivity) { return true; } if ( Object.values(state.itemStates).some( (s) => s.state !== LoadoutItemState.Succeeded && s.state !== LoadoutItemState.AlreadyThere, ) ) { return true; } if ( Object.values(state.socketOverrideStates).some((s) => Object.values(s.results).some((r) => r.state !== LoadoutSocketOverrideState.Applied), ) ) { return true; } return state.modStates.some((s) => s.state !== LoadoutModState.Applied); } ================================================ FILE: src/app/loadout-drawer/loadout-apply.ts ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { equipInGameLoadout } from 'app/bungie-api/destiny2-api'; import { D1Categories } from 'app/destiny1/d1-bucket-categories'; import { D2Categories } from 'app/destiny2/d2-bucket-categories'; import { interruptFarming, resumeFarming } from 'app/farming/basic-actions'; import { t } from 'app/i18next-t'; import { loadoutNotification } from 'app/inventory/MoveNotifications'; import { canInsertPlug, insertPlug } from 'app/inventory/advanced-write-actions'; import { updateCharacters } from 'app/inventory/d2-stores'; import { Exclusion, MoveReservations, MoveSession, createMoveSession, equipItems, executeMoveItem, getSimilarItem, } from 'app/inventory/item-move-service'; import { DimItem, DimSocket, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { updateManualMoveTimestamp } from 'app/inventory/manual-moves'; import { allItemsSelector, storesSelector, unlockedPlugSetItemsSelector, } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { hashesToPluggableItems, isPluggableItem } from 'app/inventory/store/sockets'; import { amountOfItem, findItemsByBucket, getStore, getVault, spaceLeftForItem, } from 'app/inventory/stores-helpers'; import { ArmorBucketHashes, inGameArmorEnergyRules } from 'app/loadout-builder/types'; import { updateAfterInGameLoadoutApply } from 'app/loadout/ingame/ingame-loadout-apply'; import { createPluggingStrategy, fitMostMods, pickPlugPositions, } from 'app/loadout/mod-assignment-utils'; import { d2ManifestSelector, destiny2CoreSettingsSelector, manifestSelector, } from 'app/manifest/selectors'; import { showNotification } from 'app/notifications/notifications'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { DEFAULT_ORNAMENTS, DEFAULT_SHADER } from 'app/search/d2-known-values'; import { loadingTracker } from 'app/shell/loading-tracker'; import { ThunkResult } from 'app/store/types'; import { queueAction } from 'app/utils/action-queue'; import { CancelToken, CanceledError, withCancel } from 'app/utils/cancel'; import { count, filterMap, isEmpty, mapValues } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { DimError } from 'app/utils/dim-error'; import { emptyArray } from 'app/utils/empty'; import { convertToError, errorMessage } from 'app/utils/errors'; import { isClassCompatible, itemCanBeEquippedBy } from 'app/utils/item-utils'; import { errorLog, infoLog, timer, warnLog } from 'app/utils/log'; import { aspectSocketCategoryHashes, fragmentSocketCategoryHashes, getDefaultAbilityChoiceHash, getSocketByIndex, getSocketsByCategoryHashes, getSocketsByIndexes, plugFitsIntoSocket, subclassAbilitySocketCategoryHashes, } from 'app/utils/socket-utils'; import { HashLookup } from 'app/utils/util-types'; import { PlatformErrorCodes } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { maxBy, once, partition, throttle } from 'es-toolkit'; import { Draft, produce } from 'immer'; import { savePreviousLoadout } from '../loadout/actions'; import { Assignment, InGameLoadout, Loadout, LoadoutItem, PluggingAction, } from '../loadout/loadout-types'; import { LoadoutApplyPhase, LoadoutItemState, LoadoutModState, LoadoutSocketOverrideState, LoadoutStateGetter, LoadoutStateUpdater, anyActionFailed, makeLoadoutApplyState, setLoadoutApplyPhase, setModResult, setSocketOverrideResult, } from './loadout-apply-state'; import { backupLoadout, findItemForLoadout, getLoadoutSubclassFragmentCapacity, getModsFromLoadout, isFashionPlug, } from './loadout-utils'; const TAG = 'loadout'; // TODO: move this whole file to "loadouts" folder const outOfSpaceWarning = throttle((store: DimStore) => { showNotification({ type: 'info', title: t('FarmingMode.OutOfRoomTitle'), body: t('FarmingMode.OutOfRoom', { character: store.name }), }); }, 60000); const sortedBucketHashes: (BucketHashes | D1BucketHashes)[] = [ ...D2Categories.Weapons, ...D2Categories.Armor, ...D2Categories.General, ...D2Categories.Inventory, ...D1Categories.Weapons, ...D1Categories.Armor, ...D1Categories.General, ]; const bucketHashToIndex: HashLookup = {}; for (let i = 0; i < sortedBucketHashes.length; i++) { (bucketHashToIndex as Draft)[sortedBucketHashes[i]] = i; } /** * Apply a loadout - a collection of items to be moved and possibly equipped all at once. * @param allowUndo whether to include this loadout in the "undo loadout" menu stack. * @return a promise for the completion of the whole loadout operation. */ export function applyLoadout( store: DimStore, loadout: Loadout, { allowUndo = false, onlyMatchingClass = false, inGameLoadout, }: { /** Add this to the stack of loadouts that you can undo */ allowUndo?: boolean; /** Only apply items matching the class of the store we're applying to */ onlyMatchingClass?: boolean; /** Apply this ingame loadout at the end. This also replaces the name/icon of the notification. */ inGameLoadout?: InGameLoadout; } = {}, ): ThunkResult { return async (dispatch) => { if (!store) { throw new Error('You need a store!'); } if ($featureFlags.debugMoves) { infoLog(TAG, 'Apply loadout', loadout.name, 'to', store.name); } const stopTimer = timer(TAG, 'Loadout Application'); const [cancelToken, cancel] = withCancel(); const [getLoadoutState, setLoadoutState, stateObservable] = makeLoadoutApplyState(); // This will run after other moves/loadouts are done const loadoutPromise = queueAction(() => dispatch( doApplyLoadout( store, loadout, getLoadoutState, setLoadoutState, onlyMatchingClass, cancelToken, allowUndo, inGameLoadout, ), ), ); loadingTracker.addPromise(loadoutPromise); // Start a notification that will show as long as the loadout is equipping showNotification( loadoutNotification(inGameLoadout ?? loadout, stateObservable, loadoutPromise, cancel), ); try { await loadoutPromise; } catch (e) { errorLog(TAG, 'failed loadout', getLoadoutState(), e); } finally { stopTimer(); } }; } /** * This is the task in the action queue that actually performs the loadout application. It is responsible for * making all the various moves, equips, and item reconfiguration that the loadout requested. It does not * notify errors or progress - that is handled by LoadoutApplyState and the caller. */ function doApplyLoadout( store: DimStore, loadout: Loadout, getLoadoutState: LoadoutStateGetter, setLoadoutState: LoadoutStateUpdater, onlyMatchingClass: boolean, cancelToken: CancelToken, allowUndo = false, inGameLoadout?: InGameLoadout, ): ThunkResult { return async (dispatch, getState) => { const defs = manifestSelector(getState())!; // Stop farming mode while we're applying the loadout dispatch(interruptFarming()); // The store and its items may change as we move things - make sure we're always looking at the latest version const getStores = () => storesSelector(getState()); const getTargetStore = () => getStore(getStores(), store.id)!; // TODO: use a Map to cache results from getLoadoutItem, reset between asyncs? // TODO: use ResolvedLoadoutItem! /** Find a real item corresponding to this loadout item */ const getLoadoutItem = (loadoutItem: LoadoutItem) => findItemForLoadout(defs, allItemsSelector(getState()), store.id, loadoutItem); try { // Back up the current state as an "undo" loadout if (allowUndo && !store.isVault) { dispatch( savePreviousLoadout({ storeId: store.id, loadoutId: loadout.id, previousLoadout: backupLoadout(store, t('Loadouts.Before', { name: loadout.name })), }), ); } // TODO: would be great to avoid all these getLoadoutItems? let resolvedItems = filterMap(loadout.items, (loadoutItem) => { const item = getLoadoutItem(loadoutItem); if (item) { return { loadoutItem, item, }; } }); if (onlyMatchingClass && !store.isVault) { // Trim down the list of items to only those that could be equipped by the store we're sending to. resolvedItems = resolvedItems.filter( ({ item }) => !item.equipment || itemCanBeEquippedBy(item, store), ); } // Sort loadout items by their bucket so we move items in the order that DIM displays them const applicableLoadoutItems = resolvedItems .sort( compareBy(({ item }) => { const sortIndex = bucketHashToIndex[item.bucket.hash as BucketHashes]; return sortIndex === undefined ? Number.MAX_SAFE_INTEGER : sortIndex; }), ) .map(({ loadoutItem }) => loadoutItem); // Figure out which items have specific socket overrides that will need to be applied. // TODO: remove socket-overrides from the mods to apply list! const itemsWithOverrides = filterMap(loadout.items, (loadoutItem) => { const item = getLoadoutItem(loadoutItem); if ( !loadoutItem.socketOverrides || !item || // Don't apply perks/mods/subclass configs when moving items to the vault store.isVault || // Only apply perks/mods/subclass configs if the item is usable by the store we're applying to !isClassCompatible(item.classType, store.classType) ) { return undefined; } else if (item.bucket.hash === BucketHashes.Subclass) { // Subclass ability sockets can be missing from socketOverrides, but show and // should thus apply the result of `getDefaultAbilityChoiceHash`, so patch those in here. const abilityAndSuperSockets = getSocketsByCategoryHashes( item.sockets, subclassAbilitySocketCategoryHashes, ); const newOverrides = { ...loadoutItem.socketOverrides }; for (const socket of abilityAndSuperSockets) { if (newOverrides[socket.socketIndex] === undefined) { newOverrides[socket.socketIndex] = getDefaultAbilityChoiceHash(socket); } } return { ...loadoutItem, socketOverrides: newOverrides }; } else { return loadoutItem; } }); // Filter out mods that no longer exist or that aren't unlocked on this character const unlockedPlugSetItems = once(() => unlockedPlugSetItemsSelector(store.id)(getState())); const checkMod = (h: number) => { const mod = defs.InventoryItem.get(h); return ( Boolean(mod) && (unlockedPlugSetItems().has(h) || h === DEFAULT_SHADER || DEFAULT_ORNAMENTS.includes(h)) ); }; // Don't apply mods when moving to the vault const modsToApply = ( (defs.isDestiny2 && !store.isVault && getModsFromLoadout(defs, loadout, unlockedPlugSetItems()).map( (mod) => mod.resolvedMod.hash, )) || [] ).filter(checkMod); // Mods specific to a bucket but not an item - fashion mods (shader/ornament) const modsByBucketToApply: { [bucketHash: number]: number[]; } = {}; if (!store.isVault && loadout.parameters?.modsByBucket) { for (const [bucketHash, mods] of Object.entries(loadout.parameters.modsByBucket)) { const filteredMods = mods.filter(checkMod); if (filteredMods.length) { modsByBucketToApply[parseInt(bucketHash, 10)] = filteredMods; } } } // Initialize items/mods/etc in the LoadoutApplyState, for the notification setLoadoutState( produce((state) => { state.phase = LoadoutApplyPhase.Deequip; // Fill out pending state for all items for (const loadoutItem of applicableLoadoutItems) { const item = getLoadoutItem(loadoutItem)!; state.itemStates[item.index] = { item, equip: loadoutItem.equip, state: LoadoutItemState.Pending, }; } // Fill out pending state for all socket overrides for (const loadoutItem of itemsWithOverrides) { const item = getLoadoutItem(loadoutItem)!; if (item) { state.socketOverrideStates[item.index] = { item, results: mapValues(loadoutItem.socketOverrides ?? {}, (plugHash) => ({ plugHash, state: LoadoutSocketOverrideState.Pending, })), }; } } // Fill out pending state for all mods state.modStates = modsToApply .map((modHash) => ({ modHash, state: LoadoutModState.Pending, })) .concat( Object.values(modsByBucketToApply) .flat() .map((modHash) => ({ modHash, state: LoadoutModState.Pending, })), ); }), ); // Filter out items that don't need to move const loadoutItemsToMove: LoadoutItem[] = Array.from( applicableLoadoutItems.filter((loadoutItem) => { const item = getLoadoutItem(loadoutItem); // Ignore any items that are already in the correct state const requiresAction = item && // We need to move to another location - but exclude items that can't be transferred ((item.owner !== store.id && !item.notransfer) || // Items in the postmaster should be moved even if they're on the same character item.location.inPostmaster || // Needs to be equipped. Stuff not marked "equip" doesn't // necessarily mean to de-equip it. (loadoutItem.equip && !item.equipped) || // We always try to move consumable stacks because their logic is complicated (loadoutItem.amount && loadoutItem.amount > 1)); if (item && !requiresAction) { setLoadoutState( produce((state) => { state.itemStates[item.index].state = LoadoutItemState.AlreadyThere; }), ); } return requiresAction; }), // Shallow copy all LoadoutItems so we can mutate the equipped flag later (i) => ({ ...i }), ); // The vault can't equip items, so set equipped to false if (store.isVault) { for (const loadoutItem of loadoutItemsToMove) { loadoutItem.equip = false; } } let itemsToEquip = loadoutItemsToMove.filter((i) => i.equip); // If we need to equip many items at once, we'll use a single bulk-equip later if (itemsToEquip.length > 1) { // TODO: just set a bulkEquip flag for (const i of itemsToEquip) { i.equip = false; } } // Dequip items from the loadout off of other characters so they can be moved. // TODO: break out into its own action const itemsToDequip = loadoutItemsToMove.filter((loadoutItem) => { const item = getLoadoutItem(loadoutItem); return item?.equipped && item.owner !== store.id; }); const realItemsToDequip = filterMap(itemsToDequip, getLoadoutItem); const involvedItems = [...filterMap(itemsToEquip, getLoadoutItem), ...realItemsToDequip]; const moveSession = createMoveSession(cancelToken, involvedItems); // Group dequips per character const dequips = Object.entries(Object.groupBy(realItemsToDequip, (i) => i.owner)).map( async ([owner, dequipItems]) => { // If there's only one item to remove, we don't need to bulk dequip, it'll be handled // automatically when we try to move the item. if (dequipItems.length === 1) { return; } // You can't directly dequip things, you have to equip something // else - so choose an appropriate replacement for each item. const itemsToEquip = filterMap(dequipItems, (i) => getSimilarItem(getState, getStores(), i, { exclusions: applicableLoadoutItems, excludeExotic: i.isExotic, }), ); try { const result = await dispatch( equipItems( getStore(getStores(), owner)!, itemsToEquip, applicableLoadoutItems, moveSession, ), ); // Bulk equip can partially fail setLoadoutState( produce((state) => { for (const item of dequipItems) { const errorCode = result[item.id]; state.itemStates[item.index].state = errorCode === PlatformErrorCodes.Success ? LoadoutItemState.DequippedPendingMove : LoadoutItemState.FailedDequip; // TODO how to set the error code here? // state.itemStates[item.index].error = new DimError().withCause(BungieError(errorCode)) } }), ); } catch (err) { const e = convertToError(err); if (e instanceof CanceledError) { throw e; } errorLog(TAG, 'Failed to dequip items from', owner, e); setLoadoutState( produce((state) => { for (const item of dequipItems) { state.itemStates[item.index].state = LoadoutItemState.FailedDequip; state.itemStates[item.index].error = e; } }), ); } }, ); // Run each character's bulk dequip in parallel await Promise.all(dequips); // Move all items to the right location setLoadoutState(setLoadoutApplyPhase(LoadoutApplyPhase.MoveItems)); for (const loadoutItem of loadoutItemsToMove) { // TODO: try parallelizing these too? // TODO: respect flag for equip not allowed try { const initialItem = getLoadoutItem(loadoutItem)!; await dispatch( applyLoadoutItem( store.id, loadoutItem, getLoadoutItem, applicableLoadoutItems, moveSession, ), ); const updatedItem = getLoadoutItem(loadoutItem); if (updatedItem) { setLoadoutState( produce((state) => { // TODO: doing things based on item index is kind of tough for consumables! if (state.itemStates[initialItem.index]) { state.itemStates[initialItem.index].state = // If we're doing a bulk equip later, set to MovedPendingEquip itemsToEquip.length > 1 && itemsToEquip.some((li) => loadoutItem.id === li.id) ? LoadoutItemState.MovedPendingEquip : LoadoutItemState.Succeeded; } }), ); } } catch (err) { const e = convertToError(err); if (e instanceof CanceledError) { throw e; } const updatedItem = getLoadoutItem(loadoutItem); if (updatedItem) { errorLog(TAG, 'Failed to apply loadout item', updatedItem.name, e); setLoadoutState( produce((state) => { // If it made it to the right store, the failure was in equipping, not moving const isOnCorrectStore = updatedItem.owner === store.id; state.itemStates[updatedItem.index].state = isOnCorrectStore ? LoadoutItemState.FailedEquip : LoadoutItemState.FailedMove; state.itemStates[updatedItem.index].error = e; state.equipNotPossible ||= isOnCorrectStore && e instanceof DimError && checkEquipNotPossible(e.bungieErrorCode()); }), ); } } } // After moving all items into the right place, do a single bulk-equip to the selected store. // If only one item needed to be equipped we will have handled it as part of applyLoadoutItem. setLoadoutState(setLoadoutApplyPhase(LoadoutApplyPhase.EquipItems)); if (itemsToEquip.length > 1) { const store = getTargetStore(); const successfulItems = Object.values(getLoadoutState().itemStates).filter( (s) => s.equip && s.state === LoadoutItemState.MovedPendingEquip, ); // Use the bulk equipAll API to equip all at once. itemsToEquip = itemsToEquip.filter((i) => successfulItems.some((si) => si.item.id === getLoadoutItem(i)?.id), ); const realItemsToEquip = filterMap(itemsToEquip, getLoadoutItem); try { const result = await dispatch(equipItems(store, realItemsToEquip, [], moveSession)); // Bulk equip can partially fail setLoadoutState( produce((state) => { for (const item of realItemsToEquip) { const errorCode = result[item.id]; state.itemStates[item.index].state = errorCode === PlatformErrorCodes.Success ? LoadoutItemState.Succeeded : LoadoutItemState.FailedEquip; // TODO how to set the error code here? // state.itemStates[item.index].error = new DimError().withCause(BungieError(errorCode)) state.equipNotPossible ||= checkEquipNotPossible(errorCode); } }), ); } catch (err) { const e = convertToError(err); if (e instanceof CanceledError) { throw e; } errorLog(TAG, 'Failed to equip items', e); setLoadoutState( produce((state) => { for (const item of realItemsToEquip) { state.itemStates[item.index].state = LoadoutItemState.FailedEquip; state.itemStates[item.index].error = e; } }), ); } } // Apply socket overrides to items that have them, to set specific mods and perks if (itemsWithOverrides.length) { setLoadoutState(setLoadoutApplyPhase(LoadoutApplyPhase.SocketOverrides)); infoLog(TAG, 'Socket overrides to apply', itemsWithOverrides); await dispatch( applySocketOverrides(itemsWithOverrides, setLoadoutState, getLoadoutItem, cancelToken), ); const overrideResults = Object.values(getLoadoutState().socketOverrideStates).flatMap((r) => Object.values(r.results), ); const successfulItemOverrides = count( overrideResults, (r) => r.state === LoadoutSocketOverrideState.Applied, ); infoLog( 'loadout socket overrides', 'Socket overrides applied', successfulItemOverrides, overrideResults.length, ); } const clearMods = Boolean(loadout.parameters?.clearMods); // Apply any mods in the loadout. These apply to the current equipped items, not just loadout items! if (modsToApply.length || !isEmpty(modsByBucketToApply) || clearMods) { setLoadoutState(setLoadoutApplyPhase(LoadoutApplyPhase.ApplyMods)); infoLog(TAG, 'Mods to apply', modsToApply); await dispatch( applyLoadoutMods( applicableLoadoutItems, store.id, modsToApply, modsByBucketToApply, setLoadoutState, getLoadoutItem, cancelToken, Boolean(loadout.parameters?.clearMods), ), ); const { modStates } = getLoadoutState(); infoLog( 'loadout mods', 'Mods applied', count(modStates, (s) => s.state === LoadoutModState.Applied), modStates.length, ); } // If this is marked to clear space (and we're not applying it to the vault), move items not // in the loadout off the character if ((loadout.parameters?.clearWeapons || loadout.parameters?.clearArmor) && !store.isVault) { setLoadoutState(setLoadoutApplyPhase(LoadoutApplyPhase.ClearSpace)); await dispatch( clearSpaceAfterLoadout( getTargetStore(), applicableLoadoutItems.map((i) => getLoadoutItem(i)!), moveSession, loadout.parameters.clearWeapons ?? false, loadout.parameters.clearArmor ?? false, ), ); } if (inGameLoadout) { setLoadoutState(setLoadoutApplyPhase(LoadoutApplyPhase.InGameLoadout)); try { await equipInGameLoadout(currentAccountSelector(getState())!, inGameLoadout); await dispatch(updateAfterInGameLoadoutApply(inGameLoadout)); } catch (e) { if ( e instanceof DimError && e.bungieErrorCode() === PlatformErrorCodes.DestinyCannotPerformActionAtThisLocation ) { setLoadoutState( produce((state) => { state.inGameLoadoutInActivity = true; }), ); } else { throw e; } } } if (anyActionFailed(getLoadoutState())) { setLoadoutState(setLoadoutApplyPhase(LoadoutApplyPhase.Failed)); // This message isn't used, it just triggers the failure state in the notification throw new Error('loadout-failed'); } setLoadoutState(setLoadoutApplyPhase(LoadoutApplyPhase.Succeeded)); } finally { // Update the characters to get the latest stats dispatch(updateCharacters()); dispatch(resumeFarming()); } }; } /** * Move one loadout item to its destination. May also equip the item unless we're waiting to equip it later. */ function applyLoadoutItem( storeId: string, loadoutItem: LoadoutItem, getLoadoutItem: (loadoutItem: LoadoutItem) => DimItem | undefined, excludes: Exclusion[], moveSession: MoveSession, ): ThunkResult { return async (dispatch, getState) => { // The store and its items may change as we move things - make sure we're always looking at the latest version const stores = storesSelector(getState()); const store = getStore(stores, storeId)!; const item = getLoadoutItem(loadoutItem); if (!item) { return; } // We mark this *first*, because otherwise things observing state (like farming) may not see this // in time. updateManualMoveTimestamp(item); if (item.maxStackSize > 1) { // handle consumables! const amountAlreadyHave = amountOfItem(store, loadoutItem); let amountNeeded = loadoutItem.amount - amountAlreadyHave; if (amountNeeded > 0) { const otherStores = stores.filter((otherStore) => store.id !== otherStore.id); const storesWithAmount = otherStores.map((store) => ({ store, amount: amountOfItem(store, loadoutItem), })); let totalAmount = amountAlreadyHave; // Keep moving from stacks until we get enough while (amountNeeded > 0) { const source = maxBy(storesWithAmount, (s) => s.amount)!; const amountToMove = Math.min(source.amount, amountNeeded); const sourceItem = source.store.items.find((i) => i.hash === loadoutItem.hash); if (amountToMove === 0 || !sourceItem) { const error: DimError & { level?: string } = new DimError( 'Loadouts.TooManyRequested', t('Loadouts.TooManyRequested', { total: totalAmount, itemname: item.name, requested: loadoutItem.amount, }), ); error.level = 'warn'; throw error; } source.amount -= amountToMove; amountNeeded -= amountToMove; totalAmount += amountToMove; await dispatch( executeMoveItem( sourceItem, store, { equip: false, amount: amountToMove, excludes, }, moveSession, ), ); } } } else { // Normal items get a straightforward move await dispatch( executeMoveItem( item, store, { equip: loadoutItem.equip, amount: item.amount, excludes, }, moveSession, ), ); } }; } /** * Clear out non-loadout items from a character. "items" are the items from the loadout. */ function clearSpaceAfterLoadout( store: DimStore, items: DimItem[], moveSession: MoveSession, clearWeapons: boolean, clearArmor: boolean, ): ThunkResult { const itemsByType = Map.groupBy(items, (i) => i.bucket.hash); const reservations: MoveReservations = { // reserve one space in the active character [store.id]: {}, }; const itemsToRemove: DimItem[] = []; for (const [bucketHash, loadoutItems] of itemsByType.entries()) { // Only clear the buckets that were selected by the user if ( !(clearArmor && D2Categories.Armor.includes(bucketHash)) && !(clearWeapons && D2Categories.Weapons.includes(bucketHash)) ) { continue; } let numUnequippedLoadoutItems = 0; for (const existingItem of findItemsByBucket(store, bucketHash)) { if (existingItem.equipped) { // ignore equipped items continue; } if ( existingItem.notransfer || loadoutItems.some( (i) => i.id === existingItem.id && i.hash === existingItem.hash && i.amount <= existingItem.amount, ) ) { // This was one of our loadout items (or it can't be moved) numUnequippedLoadoutItems++; } else { // Otherwise we should move it to the vault itemsToRemove.push(existingItem); } } // Reserve enough space to only leave the loadout items reservations[store.id][loadoutItems[0].bucket.hash] = loadoutItems[0].bucket.capacity - numUnequippedLoadoutItems; } return clearItemsOffCharacter(store, itemsToRemove, moveSession, reservations); } /** * Move a list of items off of a character to the vault (or to other characters if the vault is full). * * Shows a warning if there isn't any space. */ export function clearItemsOffCharacter( store: DimStore, items: DimItem[], moveSession: MoveSession, reservations: MoveReservations, ): ThunkResult { return async (dispatch, getState) => { const getStores = () => storesSelector(getState()); for (const item of items) { try { const stores = getStores(); // Move a single item. We reevaluate each time in case something changed. const vault = getVault(stores)!; const vaultSpaceLeft = spaceLeftForItem(vault, item, stores); if (vaultSpaceLeft <= 1) { // If we're down to one space, try putting it on other characters const otherStores = stores.filter((s) => !s.isVault && s.id !== store.id); const otherStoresWithSpace = otherStores.filter((store) => spaceLeftForItem(store, item, stores), ); if (otherStoresWithSpace.length) { if ($featureFlags.debugMoves) { infoLog( 'loadout', 'clearItemsOffCharacter initiated move:', item.amount, item.name, item.typeName, 'to', otherStoresWithSpace[0].name, 'from', getStore(stores, item.owner)!.name, ); } await dispatch( executeMoveItem( item, otherStoresWithSpace[0], { equip: false, amount: item.amount, excludes: items, reservations, }, moveSession, ), ); continue; } else if (vaultSpaceLeft === 0) { outOfSpaceWarning(store); continue; } } if ($featureFlags.debugMoves) { infoLog( 'loadout', 'clearItemsOffCharacter initiated move:', item.amount, item.name, item.typeName, 'to', vault.name, 'from', getStore(stores, item.owner)!.name, ); } await dispatch( executeMoveItem( item, vault, { equip: false, amount: item.amount, excludes: items, reservations, }, moveSession, ), ); } catch (err) { const e = convertToError(err); if (e instanceof CanceledError) { throw e; } if (e instanceof DimError && e.code === 'no-space') { outOfSpaceWarning(store); } else { showNotification({ type: 'error', title: item.name, body: e.message }); } } } }; } /** * Applies the socket overrides for the passed in loadout items. * * This gets all the sockets for an item and either applies the override plug in the items * socket overrides, or applies the default item plug. If the plug is already in the socket * we don't actually make an API call, it is just counted as a success. */ function applySocketOverrides( itemsWithOverrides: LoadoutItem[], setLoadoutState: LoadoutStateUpdater, getLoadoutItem: (loadoutItem: LoadoutItem) => DimItem | undefined, cancelToken: CancelToken, ): ThunkResult { return async (dispatch, getState) => { const defs = d2ManifestSelector(getState())!; for (const loadoutItem of itemsWithOverrides) { const dimItem = getLoadoutItem(loadoutItem)!; if (!dimItem) { continue; } if (loadoutItem.socketOverrides) { // We build up an array of mods to socket in order const modsForItem: Assignment[] = []; const categories = dimItem.sockets?.categories || []; // Loadout progress reporting is unaware of our socket remapping, so // we need to translate from actual item socket index to socketOverride index. const itemSocketToLoadoutOverrideSocket: { [itemSocketIndex: number]: number } = {}; for (const category of categories) { // So this is a bit awkward but subclasses use socketOverrides (socketIndex => hash) // for aspects and fragments, but the actual order is not important and we don't want // to plug fragments and aspects to match some arbitrary order. So here we untangle the // aspects and fragments and assign them similar to armor mods where it doesn't really // matter where they are. // For fragments, socketIndices only specifies the active sockets, all sockets beyond that // are ignored even if they contain a needed fragment, so that we will plug it somewhere // in an earlier, active socket. const handleShuffledSockets = (socketIndices: number[]) => { const sockets = getSocketsByIndexes(dimItem.sockets!, socketIndices); const neededOverrides = filterMap(socketIndices, (socketIndex) => { const hash = loadoutItem.socketOverrides![socketIndex]; return hash ? { hash, loadoutSocketIndex: socketIndex } : undefined; }); const excessSockets = []; // If the loadout doesn't specify aspects/fragments, don't touch them because that's how it worked for a long time. if (neededOverrides.length) { for (const socket of sockets) { if (socket.plugged) { const idx = neededOverrides.findIndex( ({ hash }) => hash === socket.plugged!.plugDef.hash, ); if (idx !== -1) { const overrideIndex = neededOverrides[idx].loadoutSocketIndex; neededOverrides.splice(idx, 1); modsForItem.push({ socketIndex: socket.socketIndex, mod: socket.plugged.plugDef, requested: true, }); itemSocketToLoadoutOverrideSocket[socket.socketIndex] = overrideIndex; } else { excessSockets.push(socket); } } } for (const socket of excessSockets) { // For every socket we didn't find a corresponding requested socketOverride for, // we assign the remaining plugs, resetting all remaining sockets beyond that to empty let override = neededOverrides.pop(); let requested = true; if (!override) { override = { hash: socket.emptyPlugItemHash!, loadoutSocketIndex: socket.socketIndex, }; // These emptying actions are not marked as requested because we didn't create // the corresponding UI element to correctly report progress requested = false; } const mod = defs.InventoryItem.get( override.hash, ) as PluggableInventoryItemDefinition; modsForItem.push({ socketIndex: socket.socketIndex, mod, requested }); itemSocketToLoadoutOverrideSocket[socket.socketIndex] = override.loadoutSocketIndex; } } }; if (aspectSocketCategoryHashes.includes(category.category.hash)) { handleShuffledSockets(category.socketIndexes); } else if (fragmentSocketCategoryHashes.includes(category.category.hash)) { const resolved = { item: dimItem, loadoutItem }; const fragmentCapacity = getLoadoutSubclassFragmentCapacity(defs, resolved, true); handleShuffledSockets(category.socketIndexes.slice(0, fragmentCapacity)); } else { const sockets = getSocketsByIndexes(dimItem.sockets!, category.socketIndexes); for (const socket of sockets) { const socketIndex = socket.socketIndex; const modHash: number | undefined = loadoutItem.socketOverrides[socketIndex]; if (modHash) { const mod = defs.InventoryItem.get(modHash) as PluggableInventoryItemDefinition; // Supers and abilities simply go into their socket modsForItem.push({ socketIndex, mod, requested: true }); } } } } const handleSuccess = ({ socketIndex, requested }: Assignment) => { requested && setLoadoutState( setSocketOverrideResult( dimItem, itemSocketToLoadoutOverrideSocket[socketIndex] ?? socketIndex, LoadoutSocketOverrideState.Applied, ), ); }; const handleFailure = ( { socketIndex, requested }: Assignment, error?: Error, equipNotPossible?: boolean, ) => requested ? setLoadoutState( setSocketOverrideResult( dimItem, itemSocketToLoadoutOverrideSocket[socketIndex] ?? socketIndex, LoadoutSocketOverrideState.Failed, error, equipNotPossible, ), ) : setLoadoutState((state) => ({ ...state, equipNotPossible: state.equipNotPossible || Boolean(equipNotPossible), })); await dispatch( equipModsToItem(dimItem, modsForItem, handleSuccess, handleFailure, cancelToken), ); } } }; } /** * Apply all the mods in the loadout to the equipped armor. * * This uses our mod assignment algorithm to choose which armor gets which mod. It will socket * mods into any equipped armor, not just armor in the loadout - this allows for loadouts that * are *only* mods to be applied to current armor. */ function applyLoadoutMods( loadoutItems: LoadoutItem[], storeId: string, /** A list of inventory item hashes for plugs */ modHashes: number[], /** Extra mods to apply that are specifically per bucket */ modsByBucket: { [bucketHash: number]: number[]; }, setLoadoutState: LoadoutStateUpdater, getLoadoutItem: (loadoutItem: LoadoutItem) => DimItem | undefined, cancelToken: CancelToken, /** if an item has mods applied, this will "clear" all other sockets to empty/their default */ clearUnassignedSocketsPerItem = false, ): ThunkResult { return async (dispatch, getState) => { const defs = d2ManifestSelector(getState())!; const stores = storesSelector(getState()); const store = getStore(stores, storeId)!; // Apply mods to the armor items in the loadout that were marked "equipped" // even if they failed to equip. For each slot that doesn't have an equipped // item in the loadout, use the current equipped item (whatever it is) // instead. const currentEquippedArmor = store.items.filter((i) => i.bucket.inArmor && i.equipped); const loadoutDimItems: DimItem[] = []; for (const loadoutItem of loadoutItems) { const item = getLoadoutItem(loadoutItem); if (item?.bucket.inArmor && loadoutItem.equip) { loadoutDimItems.push(item); } } const armor = filterMap( ArmorBucketHashes, (bucketHash) => loadoutDimItems.find((item) => item.bucket.hash === bucketHash) || currentEquippedArmor.find((item) => item.bucket.hash === bucketHash), ); const mods = hashesToPluggableItems(defs, modHashes); // Early exit - if all the mods are already there, nothing to do if ( !clearUnassignedSocketsPerItem && allModsAreAlreadyApplied(armor, modHashes, modsByBucket) ) { infoLog(TAG, 'all mods are already there. loadout already applied'); setLoadoutState((state) => ({ ...state, modStates: modHashes .concat(Object.values(modsByBucket).flat()) .map((modHash) => ({ modHash, state: LoadoutModState.Applied })), })); return; } // TODO: prefer equipping to armor that *is* part of the loadout const { itemModAssignments, unassignedMods } = fitMostMods({ defs, items: armor, plannedMods: mods, armorEnergyRules: inGameArmorEnergyRules, }); for (const mod of unassignedMods) { setLoadoutState( setModResult({ modHash: mod.hash, state: LoadoutModState.Unassigned, error: new DimError('Loadouts.UnassignedModError'), }), ); } const handleSuccess = ({ mod, requested }: Assignment) => requested && setLoadoutState(setModResult({ modHash: mod.hash, state: LoadoutModState.Applied })); const handleFailure = ( { mod, requested }: Assignment, error?: Error, equipNotPossible?: boolean, ) => requested ? setLoadoutState( setModResult( { modHash: mod.hash, state: LoadoutModState.Failed, error }, equipNotPossible, ), ) : setLoadoutState((state) => ({ ...state, equipNotPossible: state.equipNotPossible || Boolean(equipNotPossible), })); const modAssigns: { item: DimItem; actions: PluggingAction[] }[] = []; const fashionAssigns: { item: DimItem; actions: PluggingAction[] }[] = []; for (const item of armor) { const assignments = pickPlugPositions( defs, item, itemModAssignments[item.id], clearUnassignedSocketsPerItem, ); // Patch in assignments for mods by bucket (shaders/ornaments) for (const modHash of modsByBucket[item.bucket.hash] ?? []) { const modDef = defs.InventoryItem.get(modHash); const socket = item.sockets?.allSockets.find((s) => plugFitsIntoSocket(s, modHash)); if (socket && isPluggableItem(modDef)) { assignments.push({ mod: modDef, socketIndex: socket.socketIndex, requested: true }); } else { // I guess technically these are unassigned setLoadoutState( setModResult({ modHash: modHash, state: LoadoutModState.Unassigned, error: new DimError('Loadouts.UnassignedModError'), }), ); } } const pluggingSteps = createPluggingStrategy(defs, item, assignments); const assignmentSequence = pluggingSteps.filter((assignment) => assignment.required); infoLog(TAG, 'Applying mods', assignmentSequence, 'to', item.name); if (assignmentSequence.length) { const [f, m] = partition(assignmentSequence, (a) => isFashionPlug(a.mod)); modAssigns.push({ item, actions: m }); fashionAssigns.push({ item, actions: f }); } } // Given limited time, slow API, impatient teammates, we'll plug all mods // into all armor pieces, *then* all fashion. We do each item in parallel // though, subject to the rate limit for the API. for (const assignGroup of [modAssigns, fashionAssigns]) { await Promise.all( assignGroup.map(({ item, actions }) => dispatch(equipModsToItem(item, actions, handleSuccess, handleFailure, cancelToken)), ), ); } }; } /** * Check whether all the mods in modHashes are already applied to the items in armor. */ function allModsAreAlreadyApplied( armor: DimItem[], modHashes: number[], modsByBucket: { [bucketHash: number]: number[]; }, ) { // Copy this - we'll be deleting from it modsByBucket = { ...modsByBucket }; // What mods are already on the equipped armor set? const existingMods: number[] = []; for (const item of armor) { if (item.sockets) { let modsForBucket: readonly number[] = modsByBucket[item.bucket.hash] ?? emptyArray(); for (const socket of item.sockets.allSockets) { if (socket.plugged) { const pluggedHash = socket.plugged.plugDef.hash; existingMods.push(pluggedHash); modsForBucket = modsForBucket.filter((h) => h !== pluggedHash); } } if (modsForBucket.length === 0) { delete modsByBucket[item.bucket.hash]; } } } if (!isEmpty(modsByBucket)) { return false; } // Early exit - if all the mods are already there, nothing to do return modHashes.every((h) => { const foundAt = existingMods.indexOf(h); if (foundAt === -1) { // a mod was missing return false; } else { // the mod was found, but we have consumed this copy of it existingMods.splice(foundAt, 1); return true; } }); } /** * Equip the specified mods on the item, in the order provided. This applies * each assignment, and does not account for item energy, which should be * pre-calculated. */ function equipModsToItem( item: DimItem, modsForItem: Assignment[], /** Callback for state reporting while applying. Mods are applied in parallel so we want to report ASAP. */ onSuccess: (assignment: Assignment) => void, /** Callback for state reporting while applying. Mods are applied in parallel so we want to report ASAP. */ onFailure: (assignment: Assignment, error?: Error, equipNotPossible?: boolean) => void, cancelToken: CancelToken, ): ThunkResult { return async (dispatch, getState) => { const defs = d2ManifestSelector(getState())!; const destiny2CoreSettings = destiny2CoreSettingsSelector(getState())!; if (!item.sockets) { return; } const modsToApply = [...modsForItem]; // TODO: we tried to do these applies in parallel, but you can get into trouble // if you need to remove a mod before applying another. for (const assignment of modsToApply) { const { socketIndex } = assignment; let { mod } = assignment; // Use this socket const socket = getSocketByIndex(item.sockets, socketIndex)!; // This is a special case for transmog ornaments - you can't apply a // transmog ornament to the same item it was created with. So instead we // swap at the last minute to applying the default ornament which should // match the appearance that the user wanted. We'll still report as if we // applied the ornament. if (mod.hash === item.hash) { const defaultPlugHash = socket.emptyPlugItemHash; if (defaultPlugHash) { mod = (defs.InventoryItem.get(defaultPlugHash) ?? mod) as PluggableInventoryItemDefinition; } } // If the plug is already inserted we can skip this if (socket.plugged?.plugDef.hash === mod.hash) { onSuccess(assignment); continue; } if (canInsertPlug(socket, mod.hash, destiny2CoreSettings, defs)) { infoLog( 'loadout mods', 'equipping mod', mod.displayProperties.name, 'into', item.name, 'socket', defs.SocketType.get(socket.socketDefinition.socketTypeHash)?.displayProperties.name || socket.socketIndex, ); // TODO: short circuit if equipping is not possible cancelToken.checkCanceled(); try { await dispatch(applyMod(item, socket, mod)); onSuccess(assignment); } catch (err) { const e = convertToError(err); const equipNotPossible = e instanceof DimError && checkEquipNotPossible(e.bungieErrorCode()); onFailure(assignment, e, equipNotPossible); } } else { warnLog( 'loadout mods', 'cannot equip mod', mod.displayProperties.name, 'into', item.name, 'socket', defs.SocketType.get(socket.socketDefinition.socketTypeHash)?.displayProperties.name || socket.socketIndex, ); // TODO: error here explaining why onFailure(assignment); } } }; } function applyMod( item: DimItem, socket: DimSocket, mod: PluggableInventoryItemDefinition, ): ThunkResult { return async (dispatch) => { try { await dispatch(insertPlug(item, socket, mod.hash)); } catch (e) { errorLog( 'loadout mods', 'failed to equip mod', mod.displayProperties.name, 'into', item.name, 'socket', socket.socketIndex, e, ); const plugName = mod.displayProperties.name ?? 'Unknown Plug'; throw new DimError( 'AWA.ErrorMessage', t('AWA.ErrorMessage', { error: errorMessage(e), item: item.name, plug: plugName, }), ).withError(e); } }; } /** * Check error code to see if it indicates one of the known conditions where no * equips or mod changes will succeed for the active character. */ function checkEquipNotPossible(errorCode?: PlatformErrorCodes) { return ( // Player is in an activity errorCode === PlatformErrorCodes.DestinyCannotPerformActionAtThisLocation || // This happens when you log out while still in a locked equipment activity errorCode === PlatformErrorCodes.DestinyItemUnequippable ); } ================================================ FILE: src/app/loadout-drawer/loadout-drawer-reducer.test.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { count } from 'app/utils/collections'; import { isClassCompatible, itemCanBeEquippedBy, itemCanBeInLoadout } from 'app/utils/item-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { getTestDefinitions, getTestStores } from 'testing/test-utils'; import { Loadout } from '../loadout/loadout-types'; import { addItem, applySocketOverrides, clearBucketCategory, clearSubclass, fillLoadoutFromEquipped, fillLoadoutFromUnequipped, removeItem, removeMod, setClassType, setLoadoutParameters, setLoadoutSubclassFromEquipped, toggleEquipped, updateMods, } from './loadout-drawer-reducer'; import { filterLoadoutToAllowedItems, newLoadout } from './loadout-utils'; let defs: D2ManifestDefinitions; let store: DimStore; // Items that could be in a Hunter loadout let items: DimItem[]; // All items, even ones that couldn't be in a Hunter loadout let allItems: DimItem[]; const emptyLoadout = newLoadout('Test', [], DestinyClass.Hunter); const artifactUnlocks = { unlockedItemHashes: [1, 2, 3], seasonNumber: 22, }; beforeAll(async () => { let stores: DimStore[]; [defs, stores] = await Promise.all([getTestDefinitions(), getTestStores()]); allItems = stores.flatMap((store) => store.items); const isValidItem = (store: DimStore, item: DimItem) => itemCanBeEquippedBy(item, store) && itemCanBeInLoadout(item) && isClassCompatible(item.classType, DestinyClass.Hunter); store = stores.find((s) => s.classType === DestinyClass.Hunter)!; items = allItems.filter((item) => isValidItem(store, item)); }); describe('addItem', () => { it('can add an item to an empty loadout', () => { const item = items[0]; const loadout = addItem(defs, item)(emptyLoadout); expect(loadout.items).toMatchObject([ { amount: 1, equip: true, hash: item.hash, id: item.id, }, ]); }); it('saves the crafted date for crafted items', () => { const item = items.find((i) => i.crafted)!; expect(item).toBeDefined(); const loadout = addItem(defs, item)(emptyLoadout); expect(loadout.items[0].craftedDate).toBeDefined(); }); it('can add an item to a loadout that is already there as a noop', () => { const item = items[0]; let loadout = addItem(defs, item)(emptyLoadout); loadout = addItem(defs, item)(loadout); expect(loadout.items).toStrictEqual(loadout.items); }); it('can add an unequipped item to a loadout', () => { const item = items[0]; const loadoutAdded = addItem(defs, item, false)(emptyLoadout); expect(loadoutAdded.items.length).toBe(1); expect(loadoutAdded.items[0].equip).toBe(false); }); it('defaults equip when unset', () => { const [item, item2] = items.filter((i) => i.bucket.hash === BucketHashes.KineticWeapons); // First kinetic weapon added should default to equipped let loadout = addItem(defs, item)(emptyLoadout); // Second one should default to unequipped loadout = addItem(defs, item2)(loadout); expect(loadout.items).toMatchObject([ { equip: true, id: item.id, }, { equip: false, id: item2.id, }, ]); }); it('can replace the currently equipped item', () => { const [item, item2] = items.filter((i) => i.bucket.hash === BucketHashes.KineticWeapons); let loadout = addItem(defs, item, true)(emptyLoadout); // Second kinetic weapon displaces the first loadout = addItem(defs, item2, true)(loadout); expect(loadout.items).toMatchObject([ { equip: false, id: item.id, }, { equip: true, id: item2.id, }, ]); }); it('can switch an equipped item to unequipped if added again with the equip flag set', () => { const item = items[0]; let loadout = addItem(defs, item, true)(emptyLoadout); loadout = addItem(defs, item, false)(loadout); expect(loadout.items).toMatchObject([ { equip: false, id: item.id, }, ]); }); it('can switch an unequipped item to equipped if added again with the equip flag set', () => { const item = items[0]; let loadout = addItem(defs, item, false)(emptyLoadout); loadout = addItem(defs, item, true)(loadout); expect(loadout.items).toMatchObject([ { equip: true, id: item.id, }, ]); }); it('handles duplicates even if the new version is a reshaped version of the one saved in the loadout', () => { const item = items.find((i) => i.crafted)!; let loadout = addItem(defs, item, true)(emptyLoadout); loadout = addItem(defs, { ...item, id: '1234' }, false)(loadout); expect(loadout.items).toMatchObject([ { equip: false, id: '1234', }, ]); }); it('fills in socket overrides when adding a subclass', () => { const subclass = items.find((i) => i.bucket.hash === BucketHashes.Subclass)!; const loadout = addItem(defs, subclass)(emptyLoadout); expect(loadout.items[0].socketOverrides).toBeDefined(); }); it('replaces the existing subclass when adding a new one', () => { const [subclass, subclass2] = items.filter((i) => i.bucket.hash === BucketHashes.Subclass); let loadout = addItem(defs, subclass)(emptyLoadout); loadout = addItem(defs, subclass2)(loadout); expect(loadout.items).toMatchObject([ { id: subclass2.id, }, ]); }); it('does nothing if the item cannot be in a loadout', () => { const invalidItem = store.items.find((i) => !itemCanBeInLoadout(i))!; expect(invalidItem).toBeDefined(); const loadout = addItem(defs, invalidItem)(emptyLoadout); expect(loadout.items).toEqual([]); }); it('does nothing if the item is for the wrong class', () => { const invalidItem = allItems.find((i) => !isClassCompatible(i.classType, DestinyClass.Hunter))!; expect(invalidItem).toBeDefined(); const loadout = addItem(defs, invalidItem)(emptyLoadout); expect(loadout.items).toEqual([]); }); it('does nothing when adding class-specific item to any-class loadout', () => { const invalidItem = allItems.find((i) => i.classType === DestinyClass.Hunter)!; expect(invalidItem).toBeDefined(); let loadout = setClassType(DestinyClass.Unknown)(emptyLoadout); loadout = addItem(defs, invalidItem)(loadout); expect(loadout.items).toEqual([]); }); it('removes class-specific items when saving as "any class"', () => { const hunterItem = allItems.find((i) => i.classType === DestinyClass.Hunter)!; expect(hunterItem).toBeDefined(); let loadout = addItem(defs, hunterItem)(emptyLoadout); loadout = setClassType(DestinyClass.Unknown)(loadout); loadout = filterLoadoutToAllowedItems(defs, loadout); expect(loadout.items).toEqual([]); }); it('does nothing if the bucket is already at capacity', () => { const weapons = items.filter((i) => i.bucket.hash === BucketHashes.KineticWeapons); expect(weapons.length).toBeGreaterThan(10); let loadout: Loadout | undefined; for (const item of weapons) { loadout = addItem(defs, item)(loadout ?? emptyLoadout); } expect(loadout!.items.length).toEqual(10); }); it('de-equips an exotic in another bucket when equipping a new exotic', () => { const exotics = items.filter((i) => i.isExotic); const exotic1 = exotics[0]; const exotic2 = exotics.find( (i) => i.bucket.hash !== exotic1.bucket.hash && i.equippingLabel === exotic1.equippingLabel, )!; let loadout = addItem(defs, exotic1, true)(emptyLoadout); loadout = addItem(defs, exotic2, true)(loadout); expect(loadout.items).toMatchObject([ { equip: false, id: exotic1.id, }, { equip: true, id: exotic2.id, }, ]); }); }); describe('removeItem', () => { it('promotes an unequipped item to equipped when the equipped item is removed', () => { const [item, item2] = items.filter((i) => i.bucket.hash === BucketHashes.KineticWeapons); let loadout = addItem(defs, item)(emptyLoadout); loadout = addItem(defs, item2)(loadout); // now the loadout has two items, one equipped, one unequipped loadout = removeItem(defs, { item, loadoutItem: loadout.items[0] })(loadout); expect(loadout.items).toMatchObject([ { id: item2.id, equip: true, }, ]); }); it('does nothing when asked to remove an item that is not in the loadout', () => { const item = items[0]; let loadout = addItem(defs, item)(emptyLoadout); loadout = removeItem(defs, { item, loadoutItem: { id: '1234', hash: item.hash, equip: true, amount: 1 }, })(loadout); expect(loadout.items.length).toBe(1); }); }); describe('toggleEquipped', () => { it('can toggle an equipped item to unequipped', () => { const item = items[0]; let loadout = addItem(defs, item, true)(emptyLoadout); loadout = toggleEquipped(defs, { item, loadoutItem: loadout.items[0] })(loadout); expect(loadout.items).toMatchObject([ { equip: false, id: item.id, }, ]); }); it('can toggle an unequipped item to equipped', () => { const item = items[0]; let loadout = addItem(defs, item, false)(emptyLoadout); loadout = toggleEquipped(defs, { item, loadoutItem: loadout.items[0] })(loadout); expect(loadout.items).toMatchObject([ { equip: true, id: item.id, }, ]); }); it('does nothing when applied to a subclass', () => { const item = items.find((i) => i.bucket.hash === BucketHashes.Subclass)!; let loadout = addItem(defs, item, true)(emptyLoadout); loadout = toggleEquipped(defs, { item, loadoutItem: loadout.items[0] })(loadout); expect(loadout.items).toMatchObject([ { equip: true, id: item.id, }, ]); }); it('does not lose socket overrides', () => { const item = items[0]; let loadout = addItem(defs, item, true)(emptyLoadout); loadout = applySocketOverrides({ item, loadoutItem: loadout.items[0] }, { 1: 42 })(loadout); loadout = toggleEquipped(defs, { item, loadoutItem: loadout.items[0] })(loadout); expect(loadout.items).toMatchObject([ { equip: false, id: item.id, socketOverrides: { 1: 42 }, }, ]); }); }); describe('removeMod', () => { it('removes a mod by inventory item hash', () => { let loadout = updateMods([193878019, 837201397])(emptyLoadout); expect(loadout.parameters!.mods).toStrictEqual([193878019, 837201397]); loadout = removeMod({ originalModHash: 193878019, resolvedMod: defs.InventoryItem.get(193878019) as PluggableInventoryItemDefinition, })(loadout); expect(loadout.parameters!.mods).toStrictEqual([837201397]); }); }); describe('clearSubclass', () => { it('removes the subclass', () => { const item = items.find((i) => i.bucket.hash === BucketHashes.Subclass)!; let loadout = addItem(defs, item, true)(emptyLoadout); loadout = clearSubclass(defs)(loadout); expect(loadout.items).toStrictEqual([]); }); }); describe('setLoadoutSubclassFromEquipped', () => { it('correctly populates the subclass and its overrides', () => { const loadout = setLoadoutSubclassFromEquipped(defs, store)(emptyLoadout); expect(loadout.items.length).toBe(1); expect(defs.InventoryItem.get(loadout.items[0].hash).inventory!.bucketTypeHash).toBe( BucketHashes.Subclass, ); // TODO: would be good to assert more about the socket overrides expect(loadout.items[0].socketOverrides).not.toBeUndefined(); }); }); describe('fillLoadoutFromEquipped', () => { it('can fill in weapons', () => { // Add a single item that's not equipped to the loadout const item = items.find((i) => i.bucket.hash === BucketHashes.KineticWeapons && !i.equipped)!; let loadout = addItem(defs, item)(emptyLoadout); loadout = fillLoadoutFromEquipped(defs, store, artifactUnlocks, 'Weapons')(loadout); // Three equipped items, and the original item was left in place expect(loadout.items).toMatchObject([ { equip: true, id: item.id, }, { equip: true }, { equip: true }, ]); expect(loadout.parameters?.mods).toBeUndefined(); expect(loadout.parameters?.artifactUnlocks).toBeUndefined(); expect(loadout.parameters?.modsByBucket).toBeUndefined(); }); it('can fill in armor', () => { // Add a single item that's not equipped to the loadout const item = items.find((i) => i.bucket.hash === BucketHashes.Helmet && !i.equipped)!; let loadout = addItem(defs, item)(emptyLoadout); loadout = fillLoadoutFromEquipped(defs, store, artifactUnlocks, 'Armor')(loadout); // Five equipped items, and the original item was left in place expect(loadout.items).toMatchObject([ { equip: true, id: item.id, }, { equip: true }, { equip: true }, { equip: true }, { equip: true }, ]); // Mods don't get saved when just filling in a category expect(loadout.parameters?.mods).toBeUndefined(); // Artifact unlocks don't get saved when just filling in a category expect(loadout.parameters?.artifactUnlocks).toBeUndefined(); // Adding armor saves its fashion expect(loadout.parameters?.modsByBucket).not.toBeUndefined(); }); it('can fill in everything', () => { // Add a single item that's not equipped to the loadout const item = items.find((i) => i.bucket.hash === BucketHashes.Helmet && !i.equipped)!; let loadout = addItem(defs, item)(emptyLoadout); loadout = fillLoadoutFromEquipped(defs, store, artifactUnlocks)(loadout); // Five equipped items, and the original item was left in place expect(loadout.items.length).toBe(13); // Subclass, weapons, armor, emblem, ship, ghost, sparrow expect(loadout.items[0]).toMatchObject({ equip: true, id: item.id }); // Mods get saved when everything is filled in, if they weren't defined before expect(loadout.parameters?.mods).not.toBeUndefined(); // Artifact unlocks are filled in too, if they weren't defined before expect(loadout.parameters?.artifactUnlocks).not.toBeUndefined(); // As is fashion, if it wasn't defined before expect(loadout.parameters?.modsByBucket).not.toBeUndefined(); }); it('will not overwrite mods if they are already there', () => { let loadout = updateMods([1, 2, 3])(emptyLoadout); loadout = fillLoadoutFromEquipped(defs, store, artifactUnlocks)(loadout); expect(loadout.parameters?.mods).toEqual([1, 2, 3]); }); it('will not overwrite artifact unlocks if they are already there', () => { let loadout = setLoadoutParameters({ artifactUnlocks: { unlockedItemHashes: [1], seasonNumber: 1 }, })(emptyLoadout); loadout = fillLoadoutFromEquipped(defs, store, artifactUnlocks)(loadout); expect(loadout.parameters?.artifactUnlocks).toEqual({ unlockedItemHashes: [1], seasonNumber: 1, }); }); }); describe('fillLoadoutFromUnequipped', () => { it('fills in unequipped items but does not change an existing item', () => { const bucketHash = BucketHashes.KineticWeapons; const numUnequipped = count( items, (i) => i.bucket.hash === bucketHash && !i.equipped && i.owner === store.id, ); // Add a single item that's not equipped to the loadout const item = items.find( (i) => i.bucket.hash === bucketHash && !i.equipped && i.owner === store.id, )!; let loadout = addItem(defs, item)(emptyLoadout); loadout = fillLoadoutFromUnequipped(defs, store)(loadout); const itemsInLoadout = loadout.items.filter( (i) => defs.InventoryItem.get(i.hash).inventory?.bucketTypeHash === bucketHash, ); // Make sure that previously equipped item is still equipped expect(itemsInLoadout[0]).toMatchObject({ equip: true, id: item.id, }); // Only numUnequipped items because one of them was already in the loadout expect(itemsInLoadout.length).toBe(numUnequipped); }); it('fills in unequipped items for a single category', () => { const bucketHash = BucketHashes.KineticWeapons; const numUnequipped = count( items, (i) => i.bucket.hash === bucketHash && !i.equipped && i.owner === store.id, ); // Add a single item that's not equipped to the loadout const item = items.find((i) => i.bucket.hash === bucketHash && !i.equipped)!; let loadout = addItem(defs, item)(emptyLoadout); loadout = fillLoadoutFromUnequipped(defs, store, 'Weapons')(loadout); const itemsInLoadout = loadout.items.filter( (i) => defs.InventoryItem.get(i.hash).inventory?.bucketTypeHash === bucketHash, ); // Make sure that previously equipped item is still equipped expect(itemsInLoadout[0]).toMatchObject({ equip: true, id: item.id, }); expect(itemsInLoadout.length).toBe(numUnequipped); }); it('fills in unequipped items for a single category without overflow', () => { // Add some items from the vault const vaultedItems = items .filter((i) => i.bucket.hash === BucketHashes.EnergyWeapons && i.owner === 'vault') .slice(0, 5); let loadout = emptyLoadout; for (const item of vaultedItems) { loadout = addItem(defs, item)(loadout); } loadout = fillLoadoutFromUnequipped(defs, store, 'Weapons')(loadout); for (const item of vaultedItems) { // Each of the items we added is still there expect(loadout.items.some((i) => i.id === item.id)).toBe(true); } const energyWeaponsInLoadout = loadout.items.filter( (i) => defs.InventoryItem.get(i.hash).inventory?.bucketTypeHash === BucketHashes.EnergyWeapons, ); expect(energyWeaponsInLoadout.length).toBe(10); expect(energyWeaponsInLoadout.some((i) => i.equip)).toBe(true); }); }); describe('clearBucketCategory', () => { it('clears the weapons category', () => { let loadout = fillLoadoutFromEquipped(defs, store, artifactUnlocks)(emptyLoadout); loadout = clearBucketCategory(defs, 'Weapons')(loadout); expect( loadout.items.some((i) => [ BucketHashes.KineticWeapons, BucketHashes.EnergyWeapons, BucketHashes.PowerWeapons, ].includes(defs.InventoryItem.get(i.hash).inventory?.bucketTypeHash ?? 0), ), ).toBe(false); }); it('clears the general category without clearing subclass', () => { let loadout = fillLoadoutFromEquipped(defs, store, artifactUnlocks)(emptyLoadout); loadout = clearBucketCategory(defs, 'General')(loadout); expect( loadout.items.some((i) => [ BucketHashes.Ghost, BucketHashes.Emblems, BucketHashes.Ships, BucketHashes.Vehicle, ].includes(defs.InventoryItem.get(i.hash).inventory?.bucketTypeHash ?? 0), ), ).toBe(false); expect( loadout.items.some((i) => [BucketHashes.Subclass].includes( defs.InventoryItem.get(i.hash).inventory?.bucketTypeHash ?? 0, ), ), ).toBe(true); }); }); ================================================ FILE: src/app/loadout-drawer/loadout-drawer-reducer.ts ================================================ import { InGameLoadoutIdentifiers, LoadoutParameters } from '@destinyitemmanager/dim-api-types'; import { D1Categories } from 'app/destiny1/d1-bucket-categories'; import { D1ManifestDefinitions } from 'app/destiny1/d1-definitions'; import { D2Categories } from 'app/destiny2/d2-bucket-categories'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { D1BucketCategory, D2BucketCategory } from 'app/inventory/inventory-buckets'; import { DimItem } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { SocketOverrides } from 'app/inventory/store/override-sockets'; import { getModExclusionGroup, mapToNonReducedModCostVariant } from 'app/loadout/mod-utils'; import { useD2Definitions } from 'app/manifest/selectors'; import { showNotification } from 'app/notifications/notifications'; import { ItemFilter } from 'app/search/filter-types'; import { filterMap, isEmpty } from 'app/utils/collections'; import { isItemLoadoutCompatible, itemCanBeInLoadout } from 'app/utils/item-utils'; import { errorLog } from 'app/utils/log'; import { getSocketsByCategoryHash } from 'app/utils/socket-utils'; import { DestinyClass, TierType } from 'bungie-api-ts/destiny2'; import { BucketHashes, SocketCategoryHashes } from 'data/d2/generated-enums'; import { keyBy, sample, shuffle } from 'es-toolkit'; import { Draft, produce } from 'immer'; import { useCallback } from 'react'; import { Loadout, LoadoutItem, ResolvedLoadoutItem, ResolvedLoadoutMod, } from '../loadout/loadout-types'; import { randomLoadout, randomSubclassConfiguration } from './auto-loadouts'; import { convertToLoadoutItem, createSocketOverridesFromEquipped, extractArmorModHashes, findItemForLoadout, findSameLoadoutItemIndex, fromEquippedTypes, getUnequippedItemsForLoadout, singularBucketHashes, } from './loadout-utils'; /* * This module contains functions for mutating loadouts. Each exported function * should return a LoadoutUpdateFunction so it can be used directly in useState * setters. */ /** * A function that takes a loadout and returns a modified loadout. The modified * loadout must be a new instance (immutable updates). These functions can be * used in reducers or passed directly to a `setLoadout` function. * * @example * * function addItem(defs, item): LoadoutUpdateFunction { * return (loadout) => { * ... * } * } * * setLoadout(addItem(defs, item)) */ export type LoadoutUpdateFunction = (loadout: Loadout) => Loadout; /** Some helpers that bind our updater functions to the current environment */ export function useLoadoutUpdaters( store: DimStore, setLoadout: (updater: LoadoutUpdateFunction) => void, ) { const defs = useD2Definitions()!; function useUpdater(fn: (...args: T) => LoadoutUpdateFunction) { return useCallback((...args: T) => setLoadout(fn(...args)), [fn]); } function useDefsUpdater( fn: (defs: D1ManifestDefinitions | D2ManifestDefinitions, ...args: T) => LoadoutUpdateFunction, ) { // exhaustive-deps wants us to remove the dependency on defs, but we really do need it // eslint-disable-next-line react-hooks/exhaustive-deps return useCallback((...args: T) => setLoadout(fn(defs, ...args)), [fn, defs]); } function useDefsStoreUpdater( fn: ( defs: D1ManifestDefinitions | D2ManifestDefinitions, store: DimStore, ...args: T ) => LoadoutUpdateFunction, ) { // exhaustive-deps wants us to remove the dependency on defs and store, but we really do need it // eslint-disable-next-line react-hooks/exhaustive-deps return useCallback((...args: T) => setLoadout(fn(defs, store, ...args)), [fn, defs, store]); } return { useUpdater, useDefsUpdater, useDefsStoreUpdater }; } /** * Produce a new loadout that adds a new item to the given loadout. */ export function addItem( defs: D2ManifestDefinitions | D1ManifestDefinitions, item: DimItem, equip?: boolean, socketOverrides?: SocketOverrides, ): LoadoutUpdateFunction { return produce((draftLoadout) => { const loadoutItem = convertToLoadoutItem(item, false); if (socketOverrides) { loadoutItem.socketOverrides = socketOverrides; } if (item.sockets && item.bucket.hash === BucketHashes.Subclass && !socketOverrides) { loadoutItem.socketOverrides = createSocketOverridesFromEquipped(item); } // We only allow one subclass, and it must be equipped. Same with a couple other things. const singular = singularBucketHashes.includes(item.bucket.hash); const maxSlots = singular ? 1 : item.bucket.capacity; if (!itemCanBeInLoadout(item)) { showNotification({ type: 'warning', title: t('Loadouts.OnlyItems') }); return; } if (!isItemLoadoutCompatible(item.classType, draftLoadout.classType)) { showNotification({ type: 'warning', title: t('Loadouts.ClassTypeMismatch', { className: item.classTypeNameLocalized }), }); return; } const dupeIndex = findSameLoadoutItemIndex(defs, draftLoadout.items, item); if (dupeIndex !== -1) { const dupe = draftLoadout.items[dupeIndex]; if (item.maxStackSize > 1) { // The item is already here but we'd like to add more of it (only D1 loadouts hold stackables) loadoutItem.amount += Math.min(dupe.amount + item.amount, item.maxStackSize); } // Remove the dupe, we'll replace it with the new item draftLoadout.items.splice(dupeIndex, 1); } const typeInventory = loadoutItemsInBucket(defs, draftLoadout, item.bucket.hash); if (dupeIndex === -1 && typeInventory.length >= maxSlots && !singular) { // We're already full errorLog('loadouts', "Can't add", item); showNotification({ type: 'warning', title: t('Loadouts.MaxSlots', { slots: maxSlots, bucketName: item.bucket.name }), }); return; } // Set equip based on either explicit argument, or if it's the first item of this type loadoutItem.equip = equip !== undefined ? equip : item.equipment && typeInventory.length === 0; // Reset all other items of this type to not be equipped if (loadoutItem.equip) { unequipOtherItems(defs, item, draftLoadout); } if (singular) { // Remove all others (there really should be at most one) and force equipped draftLoadout.items = draftLoadout.items.filter((li) => !typeInventory.includes(li)); loadoutItem.equip = true; } draftLoadout.items.push(loadoutItem); // If adding a new armor item, remove any fashion mods (shader/ornament) that couldn't be slotted if ( item.bucket.inArmor && loadoutItem.equip && draftLoadout.parameters?.modsByBucket?.[item.bucket.hash]?.length ) { const cosmeticSockets = getSocketsByCategoryHash( item.sockets, SocketCategoryHashes.ArmorCosmetics, ); draftLoadout.parameters.modsByBucket[item.bucket.hash] = draftLoadout.parameters.modsByBucket[ item.bucket.hash ].filter((plugHash) => cosmeticSockets.some((s) => s.plugSet?.plugs.some((p) => p.plugDef.hash === plugHash)), ); } }); } /** * Produce a new Loadout with the given item removed from the original loadout. */ export function removeItem( defs: D1ManifestDefinitions | D2ManifestDefinitions, { item, loadoutItem: searchLoadoutItem }: ResolvedLoadoutItem, ): LoadoutUpdateFunction { return produce((draftLoadout) => { // TODO: it might be nice if we just assigned a unique ID to every loadout item just for in-memory ops like deleting // We can't just look it up by identity since Immer wraps objects in a proxy and getItemsFromLoadoutItems // changes the socketOverrides, so simply search by unmodified ID and hash. const loadoutItemIndex = draftLoadout.items.findIndex( (i) => i.hash === searchLoadoutItem.hash && i.id === searchLoadoutItem.id, ); if (loadoutItemIndex === -1) { return; } const loadoutItem = draftLoadout.items[loadoutItemIndex]; loadoutItem.amount ||= 1; loadoutItem.amount--; if (loadoutItem.amount <= 0) { draftLoadout.items.splice(loadoutItemIndex, 1); } // If we removed an equipped item, equip the first unequipped item if (loadoutItem.equip) { const bucketHash = item.bucket.hash; const typeInventory = bucketHash ? loadoutItemsInBucket(defs, draftLoadout, bucketHash) : []; // Here we can use identity because typeInventory is all proxies const nextInLine = typeInventory.length > 0 && draftLoadout.items.find((i) => i === typeInventory[0]); if (nextInLine) { nextInLine.equip = true; } } }); } /** * Replace an existing item in the loadout (likely a missing item) with a new * item. It should inherit equipped-ness from the original item. */ export function replaceItem( { loadoutItem }: ResolvedLoadoutItem, newItem: DimItem, ): LoadoutUpdateFunction { return produce((draftLoadout) => { const newLoadoutItem = convertToLoadoutItem(newItem, loadoutItem.equip); const loadoutItemIndex = draftLoadout.items.findIndex( (i) => i.hash === loadoutItem.hash && i.id === loadoutItem.id, ); if (loadoutItemIndex === -1) { throw new Error('Original item to replace not found'); } draftLoadout.items[loadoutItemIndex] = newLoadoutItem; }); } /** * When setting an item to be equipped, this function resets other items to not * be equipped to prevent multiple equipped items in the same bucket, and * multiple equipped exotics. */ function unequipOtherItems( defs: D1ManifestDefinitions | D2ManifestDefinitions, item: DimItem, draftLoadout: Draft, ) { for (const li of draftLoadout.items) { const itemDef = defs.InventoryItem.get(li.hash); const bucketHash = getBucketHashFromItemHash(defs, li.hash); const equippingLabel = itemDef && 'tierType' in itemDef ? itemDef.tierType === TierType.Exotic ? itemDef.itemType.toString() : undefined : itemDef.equippingBlock?.uniqueLabel; // Others in this slot if ( bucketHash === item.bucket.hash || // Other exotics (item.equippingLabel && equippingLabel === item.equippingLabel) ) { li.equip = false; } } } /** * Produce a new loadout with the given item switched to being equipped (or unequipped if it's already equipped). */ export function toggleEquipped( defs: D1ManifestDefinitions | D2ManifestDefinitions, { item, loadoutItem: { equip, socketOverrides } }: ResolvedLoadoutItem, ): LoadoutUpdateFunction { return addItem(defs, item, !equip, socketOverrides); } export function applySocketOverrides( { loadoutItem: searchLoadoutItem }: ResolvedLoadoutItem, socketOverrides: SocketOverrides | undefined, ): LoadoutUpdateFunction { return produce((draftLoadout) => { // TODO: it might be nice if we just assigned a unique ID to every loadout item just for in-memory ops like deleting // We can't just look it up by identity since Immer wraps objects in a proxy and getItemsFromLoadoutItems // changes the socketOverrides, so simply search by unmodified ID and hash. const loadoutItem = draftLoadout.items.find( (li) => li.id === searchLoadoutItem.id && li.hash === searchLoadoutItem.hash, ); if (loadoutItem) { loadoutItem.socketOverrides = socketOverrides; } }); } function loadoutItemsInBucket( defs: D1ManifestDefinitions | D2ManifestDefinitions, loadout: Loadout, searchBucketHash: number, ) { return loadout.items.filter((li) => { const bucketHash = getBucketHashFromItemHash(defs, li.hash); return bucketHash && bucketHash === searchBucketHash; }); } function getBucketHashFromItemHash( defs: D1ManifestDefinitions | D2ManifestDefinitions, itemHash: number, ) { const def = defs.InventoryItem.get(itemHash); return def && ('bucketTypeHash' in def ? def.bucketTypeHash : def.inventory?.bucketTypeHash); } /** * Remove all Loadout Optimizer parameters from a loadout. This leaves things like mods and fashion in place. */ export function clearLoadoutOptimizerParameters(): LoadoutUpdateFunction { return produce((draft) => { if (draft.parameters) { delete draft.parameters.assumeArmorMasterwork; delete draft.parameters.exoticArmorHash; delete draft.parameters.query; delete draft.parameters.statConstraints; delete draft.parameters.autoStatMods; } }); } /** Remove the current subclass from the loadout. */ export function clearSubclass( defs: D1ManifestDefinitions | D2ManifestDefinitions, ): LoadoutUpdateFunction { return (loadout) => { if (!defs.isDestiny2) { return loadout; } const isSubclass = (i: LoadoutItem) => defs.InventoryItem.get(i.hash)?.inventory?.bucketTypeHash === BucketHashes.Subclass; return { ...loadout, items: loadout.items.filter((i) => !isSubclass(i)), }; }; } /** * Remove a specific mod by its inventory item hash. */ export function removeMod(mod: ResolvedLoadoutMod): LoadoutUpdateFunction { return (loadout) => { if (loadout.parameters?.mods) { const index = loadout.parameters?.mods.indexOf(mod.originalModHash); if (index !== -1) { const mods = loadout.parameters.mods.toSpliced(index, 1); return setLoadoutParameters({ mods })(loadout); } } return loadout; }; } /** Replace the loadout's subclass with the store's currently equipped subclass */ export function setLoadoutSubclassFromEquipped( defs: D1ManifestDefinitions | D2ManifestDefinitions, store: DimStore, ): LoadoutUpdateFunction { return (loadout) => { const newSubclass = store.items.find( (item) => item.equipped && item.bucket.hash === BucketHashes.Subclass && itemCanBeInLoadout(item), ); if (!newSubclass || !defs.isDestiny2) { return loadout; } return addItem(defs, newSubclass, true)(loadout); }; } /** * Fill in items from the store's equipped items, keeping any equipped items already in the loadout in place. */ export function fillLoadoutFromEquipped( defs: D1ManifestDefinitions | D2ManifestDefinitions, store: DimStore, artifactUnlocks: LoadoutParameters['artifactUnlocks'] | undefined, /** Fill in from only this specific category */ category?: D2BucketCategory, ): LoadoutUpdateFunction { return (loadout) => { const equippedItemsByBucket = keyBy( loadout.items.filter((li) => li.equip), (li) => getBucketHashFromItemHash(defs, li.hash)!, ); const newEquippedItems = store.items.filter( (item) => item.equipped && itemCanBeInLoadout(item) && itemMatchesCategory(item, category), ); const modsByBucket: { [bucketHash: number]: number[] } = {}; for (const item of newEquippedItems) { if (!(item.bucket.hash in equippedItemsByBucket)) { loadout = addItem(defs, item, true)(loadout); // Only save fashion for the items we added const plugs = item.sockets ? filterMap( getSocketsByCategoryHash(item.sockets, SocketCategoryHashes.ArmorCosmetics), (s) => s.plugged?.plugDef.hash, ) : []; if (plugs.length) { modsByBucket[item.bucket.hash] = plugs; } } } // Populate mods if they aren't already there if (!category && !loadout.parameters?.mods?.length) { loadout = syncModsFromEquipped(store)(loadout); } // Populate artifactUnlocks if they aren't already there if (!category && isEmpty(loadout.parameters?.artifactUnlocks)) { loadout = syncArtifactUnlocksFromEquipped(artifactUnlocks)(loadout); } // Save "fashion" mods for newly equipped items, but don't overwrite existing fashion if (!isEmpty(modsByBucket)) { loadout = updateModsByBucket({ ...modsByBucket, ...loadout.parameters?.modsByBucket })( loadout, ); } return loadout; }; } /** * Replace all equipped items from the store's equipped items. */ export function syncLoadoutCategoryFromEquipped( defs: D2ManifestDefinitions | D1ManifestDefinitions, store: DimStore, category: D2BucketCategory, ): LoadoutUpdateFunction { return (loadout) => { const bucketHashes = getLoadoutBucketHashesFromCategory(defs, category); // Remove equipped items from this bucket loadout = { ...loadout, items: loadout.items.filter( (li) => !(li.equip && bucketHashes.includes(getBucketHashFromItemHash(defs, li.hash) ?? 0)), ), }; const newEquippedItems = store.items.filter( (item) => item.equipped && itemCanBeInLoadout(item) && bucketHashes.includes(item.bucket.hash), ); for (const item of newEquippedItems) { loadout = addItem(defs, item, true)(loadout); } // Save "fashion" mods for equipped items const modsByBucket: { [bucketHash: number]: number[] } = {}; for (const item of newEquippedItems.filter((i) => i.bucket.inArmor)) { const plugs = item.sockets ? filterMap( getSocketsByCategoryHash(item.sockets, SocketCategoryHashes.ArmorCosmetics), (s) => s.plugged?.plugDef.hash, ) : []; if (plugs.length) { modsByBucket[item.bucket.hash] = plugs; } } if (!isEmpty(modsByBucket)) { loadout = setLoadoutParameters({ modsByBucket })(loadout); } return loadout; }; } /** * Add all the unequipped items on the given character to the loadout. */ export function fillLoadoutFromUnequipped( defs: D1ManifestDefinitions | D2ManifestDefinitions, store: DimStore, /** Fill in from only this specific category */ category?: D2BucketCategory, ): LoadoutUpdateFunction { return (loadout) => { const items = getUnequippedItemsForLoadout(store, category); for (const item of items) { // Don't mess with something that's already there const dupeIndex = findSameLoadoutItemIndex(defs, loadout.items, item); if (dupeIndex === -1) { // Add as an unequipped item loadout = addItem(defs, item, false)(loadout); } } return loadout; }; } export function setName(name: string): LoadoutUpdateFunction { return (loadout) => ({ ...loadout, name, }); } export function setNotes(notes: string | undefined): LoadoutUpdateFunction { return (loadout) => ({ ...loadout, notes, }); } export function setClassType(classType: DestinyClass): LoadoutUpdateFunction { return (loadout) => ({ ...loadout, classType, }); } export function setClearSpace( clearSpace: boolean, category: 'Weapons' | 'Armor', ): LoadoutUpdateFunction { return (loadout) => ({ ...loadout, parameters: { ...loadout.parameters, [category === 'Weapons' ? 'clearWeapons' : 'clearArmor']: clearSpace, }, }); } export function setLoadoutParameters(params: Partial): LoadoutUpdateFunction { return (loadout) => ({ ...loadout, parameters: { ...loadout.parameters, ...params }, }); } /** * Replace the mods in this loadout with all the mods currently on this character's equipped armor. */ export function syncModsFromEquipped(store: DimStore): LoadoutUpdateFunction { const mods: number[] = []; const equippedArmor = store.items.filter( (item) => item.equipped && itemCanBeInLoadout(item) && item.bucket.inArmor, ); for (const item of equippedArmor) { mods.push(...extractArmorModHashes(item)); } return updateMods(mods); } export function getLoadoutBucketHashesFromCategory( defs: D1ManifestDefinitions | D2ManifestDefinitions, category: D2BucketCategory | D1BucketCategory, ) { return defs.isDestiny2 ? category === 'General' ? [BucketHashes.Ghost, BucketHashes.Emblems, BucketHashes.Ships, BucketHashes.Vehicle] : D2Categories[category as D2BucketCategory] : D1Categories[category as D1BucketCategory]; } export function clearBucketCategory( defs: D1ManifestDefinitions | D2ManifestDefinitions, category: D2BucketCategory | D1BucketCategory, ) { return clearBuckets(defs, getLoadoutBucketHashesFromCategory(defs, category)); } /** * Remove all items that are in one or more buckets. */ function clearBuckets( defs: D1ManifestDefinitions | D2ManifestDefinitions, bucketHashes: number[], ): LoadoutUpdateFunction { return (loadout) => ({ ...loadout, items: loadout.items.filter((i) => { const bucketHash = getBucketHashFromItemHash(defs, i.hash); return !( bucketHash && // Subclasses are in "general" but shouldn't be cleared when we // clear general -- there's an explicit clearSubclass bucketHash !== BucketHashes.Subclass && bucketHashes.includes(bucketHash) ); }), }); } export function clearMods(): LoadoutUpdateFunction { return produce((loadout) => { delete loadout.parameters?.mods; }); } export function changeClearMods(enabled: boolean): LoadoutUpdateFunction { return setLoadoutParameters({ clearMods: enabled, }); } export function changeIncludeRuntimeStats(enabled: boolean): LoadoutUpdateFunction { return setLoadoutParameters({ includeRuntimeStatBenefits: enabled, }); } export function updateMods(mods: number[]): LoadoutUpdateFunction { return setLoadoutParameters({ mods: mods.map(mapToNonReducedModCostVariant), }); } export function updateModsByBucket( modsByBucket: { [bucketHash: number]: number[] } | undefined, ): LoadoutUpdateFunction { return setLoadoutParameters({ modsByBucket: isEmpty(modsByBucket) ? undefined : modsByBucket, }); } /** * Replace the artifact unlocks with the currently equipped ones. */ export function syncArtifactUnlocksFromEquipped( artifactUnlocks: | { unlockedItemHashes: number[]; seasonNumber: number; } | undefined, ): LoadoutUpdateFunction { if (artifactUnlocks?.unlockedItemHashes.length) { return setLoadoutParameters({ artifactUnlocks, }); } else { return (loadout) => loadout; } } /** * Clear the artifact unlocks. */ export function clearArtifactUnlocks(): LoadoutUpdateFunction { return setLoadoutParameters({ artifactUnlocks: undefined, }); } /** * Remove one artifact mod. */ export function removeArtifactUnlock(mod: number): LoadoutUpdateFunction { return produce((loadout) => { if (loadout.parameters?.artifactUnlocks) { const index = loadout.parameters?.artifactUnlocks.unlockedItemHashes.indexOf(mod); if (index !== -1) { loadout.parameters.artifactUnlocks.unlockedItemHashes.splice(index, 1); } } }); } /** Randomize the subclass and subclass configuration */ export function randomizeLoadoutSubclass( defs: D1ManifestDefinitions | D2ManifestDefinitions, store: DimStore, ): LoadoutUpdateFunction { return (loadout) => { const newSubclass = sample( store.items.filter( (item) => item.bucket.hash === BucketHashes.Subclass && itemCanBeInLoadout(item), ), ); if (!newSubclass) { return loadout; } return addItem( defs, newSubclass, true, defs.isDestiny2 ? randomSubclassConfiguration(defs, newSubclass) : undefined, )(loadout); }; } function itemMatchesCategory(item: DimItem, category: D2BucketCategory | undefined) { return category ? category === 'General' ? item.bucket.hash !== BucketHashes.Subclass && item.bucket.sort === category : item.bucket.sort === category : fromEquippedTypes.includes(item.bucket.hash); } /** * Randomize the subclass (+ configuration), items, and mods of the loadout. */ export function randomizeFullLoadout( defs: D1ManifestDefinitions | D2ManifestDefinitions, store: DimStore, allItems: DimItem[], itemFilter: ItemFilter | undefined, unlockedPlugs: Set, ) { return (loadout: Loadout) => { loadout = randomizeLoadoutItems(defs, store, allItems, undefined, itemFilter)(loadout); return randomizeLoadoutMods(defs, store, allItems, unlockedPlugs)(loadout); }; } /** * Randomize the equipped items, filling empty buckets and replacing existing equipped items. */ export function randomizeLoadoutItems( defs: D1ManifestDefinitions | D2ManifestDefinitions, store: DimStore, allItems: DimItem[], /** Randomize only this specific category */ category: D2BucketCategory | undefined, itemFilter: ItemFilter | undefined, ): LoadoutUpdateFunction { return produce((loadout) => { const randomizedLoadout = randomLoadout( store, allItems, (item) => itemMatchesCategory(item, category) && (!(item.bucket.inArmor || item.bucket.inWeapons) || !itemFilter || itemFilter(item)), ); const randomizedLoadoutBuckets = randomizedLoadout.items.map((li) => getBucketHashFromItemHash(defs, li.hash), ); loadout.items = loadout.items.filter( (i) => !i.equip || !randomizedLoadoutBuckets.includes(getBucketHashFromItemHash(defs, i.hash)), ); for (const item of randomizedLoadout.items) { let loadoutItem = item; if (defs.isDestiny2 && getBucketHashFromItemHash(defs, item.hash) === BucketHashes.Subclass) { loadoutItem = { ...loadoutItem, socketOverrides: randomSubclassConfiguration( defs, allItems.find((dimItem) => dimItem.hash === item.hash)!, ), }; } loadout.items.push(loadoutItem); } }); } /** * Replace the loadout's mods with randomly chosen mods that will fit on the * loadout's equipped armor (falling back to character-equipped items). */ export function randomizeLoadoutMods( defs: D2ManifestDefinitions | D1ManifestDefinitions, store: DimStore, allItems: DimItem[], unlockedPlugs: Set, ): LoadoutUpdateFunction { return produce((loadout) => { const equippedArmor = store.items.filter( (item) => item.equipped && itemCanBeInLoadout(item) && item.bucket.inArmor, ); for (const li of loadout.items) { const existingItem = findItemForLoadout(defs, allItems, store.id, li); if (existingItem?.bucket.inArmor) { const idx = equippedArmor.findIndex( (item) => item.bucket.hash === existingItem.bucket.hash, ); equippedArmor[idx] = existingItem; } } const mods = []; for (const item of equippedArmor) { if (item.sockets) { let energy = item.energy?.energyCapacity ?? 0; const exclusionGroups: string[] = []; const sockets = shuffle( getSocketsByCategoryHash(item.sockets, SocketCategoryHashes.ArmorMods), ); for (const socket of sockets) { const chosenMod = sample( socket.plugSet?.plugs.filter((plug) => { if ( plug.plugDef.hash === socket.emptyPlugItemHash || !unlockedPlugs.has(plug.plugDef.hash) ) { return false; } const cost = plug.plugDef.plug.energyCost?.energyCost; const exclusionGroup = getModExclusionGroup(plug.plugDef); return ( (cost === undefined || cost <= energy) && (exclusionGroup === undefined || !exclusionGroups.includes(exclusionGroup)) ); }) ?? [], ); if (chosenMod) { mods.push(mapToNonReducedModCostVariant(chosenMod.plugDef.hash)); energy -= chosenMod.plugDef.plug.energyCost?.energyCost ?? 0; const exclusionGroup = getModExclusionGroup(chosenMod.plugDef); if (exclusionGroup !== undefined) { exclusionGroups.push(exclusionGroup); } } } } } return setLoadoutParameters({ mods, })(loadout); }); } /** * Set the name/icon/color of this loadout. */ export function setInGameLoadoutIdentifiers(identifiers: InGameLoadoutIdentifiers | undefined) { return setLoadoutParameters({ inGameIdentifiers: identifiers }); } ================================================ FILE: src/app/loadout-drawer/loadout-events.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import { EventBus } from 'app/utils/observable'; import { Loadout } from '../loadout/loadout-types'; export interface EditLoadoutState { loadout: Loadout; showClass: boolean; storeId: string; fromExternal: boolean; } export const editLoadout$ = new EventBus(); export const addItem$ = new EventBus(); /** * Start editing a loadout. */ export function editLoadout( loadout: Loadout, storeId: string, { showClass = true, /** Is this from an external source (e.g. a loadout share)? */ fromExternal = false, }: { showClass?: boolean; fromExternal?: boolean } = {}, ) { editLoadout$.next({ storeId, loadout, showClass, fromExternal, }); } /** * Add an item to the loadout we're currently editing. This is driven by clicks in Inventory. */ export function addItemToLoadout(item: DimItem) { addItem$.next(item); } /** * Copy and Edit Loadout */ export function copyAndEditLoadout( loadout: Loadout, storeId: string, { showClass = true }: { showClass?: boolean } = {}, ) { const copiedLoadout = { ...loadout, name: `${loadout.name} - Copy`, id: globalThis.crypto.randomUUID(), // Give it a new ID so it's a new loadout }; editLoadout(copiedLoadout, storeId, { showClass }); } ================================================ FILE: src/app/loadout-drawer/loadout-item-conversion.ts ================================================ import { D1ManifestDefinitions } from 'app/destiny1/d1-definitions'; import { makeFakeItem as makeFakeD1Item } from 'app/inventory/store/d1-item-factory'; import { ItemCreationContext, makeFakeItem } from 'app/inventory/store/d2-item-factory'; import { applySocketOverrides } from 'app/inventory/store/override-sockets'; import { emptyArray } from 'app/utils/empty'; import { warnLog } from 'app/utils/log'; import { plugFitsIntoSocket } from 'app/utils/socket-utils'; import { DimItem } from '../inventory/item-types'; import { LoadoutItem, ResolvedLoadoutItem } from '../loadout/loadout-types'; import { findItemForLoadout } from './loadout-utils'; let missingLoadoutItemId = 1; /* * We don't save consumables in D2 loadouts, but we may omit ids in shared * loadouts (because they'll never match someone else's inventory). So instead, * pick an ID. The ID ought to be numeric, or it will fail when sent to the DIM * API. */ export function generateMissingLoadoutItemId() { return `${missingLoadoutItemId++}`; } /** * Turn the loadout's items into real DIM items. Any that don't exist in inventory anymore * are returned as warnitems. */ export function getItemsFromLoadoutItems( itemCreationContext: ItemCreationContext, loadoutItems: LoadoutItem[] | undefined, storeId: string | undefined, allItems: DimItem[], modsByBucket?: { [bucketHash: number]: number[] | undefined; }, /** needs passing in if this is d1 mode */ d1Defs?: D1ManifestDefinitions, ): [items: ResolvedLoadoutItem[], warnitems: ResolvedLoadoutItem[]] { if (!loadoutItems) { return [emptyArray(), emptyArray()]; } const { defs, buckets } = itemCreationContext; const useTheseDefs = d1Defs ?? defs; const items: ResolvedLoadoutItem[] = []; const warnitems: ResolvedLoadoutItem[] = []; for (const loadoutItem of loadoutItems) { // TODO: filter down to the class type of the loadout const item = findItemForLoadout(useTheseDefs, allItems, storeId, loadoutItem); if (item) { // If there are any mods for this item's bucket, and the item is equipped, add them to socket overrides const modsForBucket = loadoutItem.equip && modsByBucket ? (modsByBucket[item.bucket.hash] ?? []) : []; let overrides = loadoutItem.socketOverrides; for (const modHash of modsForBucket) { const socket = item.sockets?.allSockets.find((s) => plugFitsIntoSocket(s, modHash)); if (socket) { overrides = { ...overrides, [socket?.socketIndex]: modHash }; } } // Apply socket overrides so the item appears as it should be configured in the loadout const overriddenItem = useTheseDefs.isDestiny2 ? applySocketOverrides(itemCreationContext, item, overrides) : item; items.push({ item: overriddenItem, // TODO: Should we keep the original socket overrides here, somewhere? There's a difference between "effective socket overrides" and "socket overrides to save" loadoutItem: overrides === loadoutItem.socketOverrides ? loadoutItem : { ...loadoutItem, socketOverrides: overrides }, }); } else { const fakeItem = useTheseDefs.isDestiny2 ? makeFakeItem(itemCreationContext, loadoutItem.hash) : makeFakeD1Item(useTheseDefs, buckets, loadoutItem.hash); if (fakeItem) { fakeItem.id = generateMissingLoadoutItemId(); warnitems.push({ item: fakeItem, loadoutItem, missing: true }); } else { warnLog('loadout', "Couldn't create fake warn item for", loadoutItem); } } } return [items, warnitems]; } ================================================ FILE: src/app/loadout-drawer/loadout-utils.ts ================================================ import { LoadoutParameters } from '@destinyitemmanager/dim-api-types'; import { D1ManifestDefinitions } from 'app/destiny1/d1-definitions'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { BucketSortType } from 'app/inventory/inventory-buckets'; import { DimCharacterStat, DimStore } from 'app/inventory/store-types'; import { SocketOverrides } from 'app/inventory/store/override-sockets'; import { isPluggableItem } from 'app/inventory/store/sockets'; import { findItemsByBucket, getCurrentStore, getStore } from 'app/inventory/stores-helpers'; import { ArmorBucketHashes, ArmorEnergyRules, inGameArmorEnergyRules, } from 'app/loadout-builder/types'; import { calculateAssumedItemEnergy, isAssumedMasterworked } from 'app/loadout/armor-upgrade-utils'; import { UNSET_PLUG_HASH } from 'app/loadout/known-values'; import { isLoadoutBuilderItem } from 'app/loadout/loadout-item-utils'; import { fitMostMods } from 'app/loadout/mod-assignment-utils'; import { isInsertableArmor2Mod, mapToAvailableModCostVariant, sortMods, } from 'app/loadout/mod-utils'; import { getTotalModStatChanges } from 'app/loadout/stats'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { MASTERWORK_ARMOR_STAT_BONUS, MAX_ARMOR_ENERGY_CAPACITY, armorStats, deprecatedPlaceholderArmorModHash, } from 'app/search/d2-known-values'; import { filterMap, isEmpty, mapValues, sumBy } from 'app/utils/collections'; import { compareByIndex } from 'app/utils/comparators'; import { emptyObject } from 'app/utils/empty'; import { isArmor3, isArmor3MasterworkSocket, isClassCompatible, isItemLoadoutCompatible, itemCanBeEquippedBy, itemCanBeInLoadout, } from 'app/utils/item-utils'; import { weakMemoize } from 'app/utils/memoize'; import { aspectSocketCategoryHashes, fragmentSocketCategoryHashes, getSocketsByCategoryHash, getSocketsByCategoryHashes, getSocketsByIndexes, plugFitsIntoSocket, } from 'app/utils/socket-utils'; import { HashLookup, LookupTable } from 'app/utils/util-types'; import { DestinyClass, DestinyInventoryItemDefinition, DestinyItemSubType, DestinyItemType, DestinyLoadoutItemComponent, DestinySeasonDefinition, } from 'bungie-api-ts/destiny2'; import { BucketHashes, SocketCategoryHashes } from 'data/d2/generated-enums'; import { keyBy, maxBy } from 'es-toolkit'; import { produce } from 'immer'; import { D2Categories } from '../destiny2/d2-bucket-categories'; import { DimItem, DimSocket, PluggableInventoryItemDefinition } from '../inventory/item-types'; import { Loadout, LoadoutItem, ResolvedLoadoutItem, ResolvedLoadoutMod, } from '../loadout/loadout-types'; // We don't want to prepopulate the loadout with D1 cosmetics export const fromEquippedTypes: (BucketHashes | D1BucketHashes)[] = [ BucketHashes.Subclass, BucketHashes.KineticWeapons, BucketHashes.EnergyWeapons, BucketHashes.PowerWeapons, BucketHashes.Helmet, BucketHashes.Gauntlets, BucketHashes.ChestArmor, BucketHashes.LegArmor, BucketHashes.ClassArmor, D1BucketHashes.Artifact, BucketHashes.Ghost, BucketHashes.Ships, BucketHashes.Vehicle, BucketHashes.Emblems, ]; // Bucket hashes, in order, that are contained within ingame loadouts const inGameLoadoutBuckets: BucketHashes[] = [ BucketHashes.Subclass, BucketHashes.KineticWeapons, BucketHashes.EnergyWeapons, BucketHashes.PowerWeapons, BucketHashes.Helmet, BucketHashes.Gauntlets, BucketHashes.ChestArmor, BucketHashes.LegArmor, BucketHashes.ClassArmor, ]; /** * Buckets where the item should be treated as "singular" in a loadout - where * it can only have a single item and that item must be equipped. */ export const singularBucketHashes = [ BucketHashes.Subclass, BucketHashes.Emblems, BucketHashes.Emotes, ]; // order to display a list of all 8 gear slots const gearSlotOrder: BucketHashes[] = [...D2Categories.Weapons, ...D2Categories.Armor]; /** * Creates a new loadout, with all of the items equipped and the items inserted mods saved. */ export function newLoadout(name: string, items: LoadoutItem[], classType?: DestinyClass): Loadout { return { id: globalThis.crypto.randomUUID(), classType: classType !== undefined && classType !== DestinyClass.Classified ? classType : DestinyClass.Unknown, name, items, clearSpace: false, }; } /** * Create a socket overrides structure from the item's currently plugged sockets. * This will ignore all default plugs except for abilities where the default values * will be included. */ export function createSocketOverridesFromEquipped(item: DimItem) { if (item.sockets) { const socketOverrides: SocketOverrides = {}; let fragmentCapacity = getSubclassFragmentCapacity(item); nextCategory: for (const category of item.sockets.categories) { const sockets = getSocketsByIndexes(item.sockets, category.socketIndexes); for (const socket of sockets) { // For subclass fragments, only active fragments should be saved. // This check has to happen early because a fragment is inactive if it's // in a fragment socket index >= capacity // (so with three fragment slots and fragments [1, 2, empty, 4] the last // fragment will be inactive) if (fragmentSocketCategoryHashes.includes(category.category.hash)) { if (fragmentCapacity > 0) { fragmentCapacity--; } else { continue nextCategory; } } // Add currently plugged, unless it's the empty option. Abilities and Supers // explicitly don't have an emptyPlugItemHash. if ( socket.plugged && // Only save them if they're valid plug options though, otherwise // we'd save the empty stasis sockets that Void 3.0 spawns with plugFitsIntoSocket(socket, socket.plugged.plugDef.hash) && socket.plugged.plugDef.hash !== socket.emptyPlugItemHash ) { socketOverrides[socket.socketIndex] = socket.plugged.plugDef.hash; } } } return socketOverrides; } } /** * Create a new loadout that includes all the equipped items and mods on the character. */ export function newLoadoutFromEquipped( name: string, dimStore: DimStore, artifactUnlocks: LoadoutParameters['artifactUnlocks'], ) { const items = dimStore.items.filter( (item) => item.equipped && itemCanBeInLoadout(item) && fromEquippedTypes.includes(item.bucket.hash), ); const loadoutItems = items.map((i) => { const item = convertToLoadoutItem(i, true); if (i.bucket.hash === BucketHashes.Subclass) { item.socketOverrides = createSocketOverridesFromEquipped(i); } return item; }); const loadout = newLoadout(name, loadoutItems, dimStore.classType); // Choose a stable ID loadout.id = 'equipped'; const mods = items.flatMap((i) => extractArmorModHashes(i)); if (mods.length) { loadout.parameters = { mods, }; } if (artifactUnlocks?.unlockedItemHashes.length) { loadout.parameters = { ...loadout.parameters, artifactUnlocks, }; } // Save "fashion" mods for equipped items const modsByBucket: { [bucketHash: number]: number[] } = {}; for (const item of items.filter((i) => i.bucket.inArmor)) { const plugs = item.sockets ? filterMap( getSocketsByCategoryHash(item.sockets, SocketCategoryHashes.ArmorCosmetics), (s) => s.plugged?.plugDef.hash, ) : []; if (plugs.length) { modsByBucket[item.bucket.hash] = plugs; } } if (!isEmpty(modsByBucket)) { loadout.parameters = { ...loadout.parameters, modsByBucket, }; } return loadout; } /** * Extract the equipped items into a list of ingame loadout item components. Basically newLoadoutFromEquipped * but for creating ingame loadout state. */ export function inGameLoadoutItemsFromEquipped(store: DimStore): DestinyLoadoutItemComponent[] { return inGameLoadoutBuckets.map((bucketHash) => { const item = findItemsByBucket(store, bucketHash).find((i) => i.equipped); const overrides = item && createSocketOverridesFromEquipped(item); return { itemInstanceId: item?.id ?? '0', // Ingame loadouts always specify plug hashes for 16 socket indexes plugItemHashes: Array.from(new Array(16), (_v, i) => overrides?.[i] ?? UNSET_PLUG_HASH), }; }); } /* * Calculates the light level for a list of items, one per type of weapon and armor * or it won't be accurate. function properly supports guardians w/o artifacts * returns to tenth decimal place. */ export function getLight(store: DimStore, items: DimItem[]): number { // https://www.reddit.com/r/DestinyTheGame/comments/6yg4tw/how_overall_power_level_is_calculated/ if (store.destinyVersion === 2) { const exactLight = sumBy(items, (i) => i.power) / items.length; return Math.floor(exactLight * 1000) / 1000; } else { const itemWeight: LookupTable = { Weapons: 6, Armor: 5, General: 4, }; const itemWeightDenominator = items.reduce( (memo, item) => memo + (itemWeight[item.bucket.hash === BucketHashes.ClassArmor ? 'General' : item.bucket.sort!] || 0), 0, ); const exactLight = items.reduce( (memo, item) => memo + item.power * (itemWeight[ item.bucket.hash === BucketHashes.ClassArmor ? 'General' : item.bucket.sort! ] || 1), 0, ) / itemWeightDenominator; return Math.floor(exactLight * 10) / 10; } } /** * This gets the loadout stats for all the equipped items and mods. * * It will add all stats from the mods whether they are equipped or not. If * you want to ensure it will be the same as the game stats, make sure to check * if all mods will fit on the items. */ export function getLoadoutStats( defs: D2ManifestDefinitions, classType: DestinyClass, subclass: ResolvedLoadoutItem | undefined, armor: DimItem[], mods: PluggableInventoryItemDefinition[], includeRuntimeStatBenefits: boolean, /** Assume armor is masterworked according to these rules when calculating stats */ armorEnergyRules?: ArmorEnergyRules, ) { const statDefs = armorStats.map((hash) => defs.Stat.get(hash)); // Construct map of stat hash to DimCharacterStat const stats: { [hash: number | string]: DimCharacterStat } = {}; for (const { hash, displayProperties } of statDefs) { stats[hash] = { hash, displayProperties, value: 0, breakdown: [] }; } // Sum the items stats into the stats const armorPiecesStats = mapValues(stats, () => 0); for (const item of armor) { for (const [statHash, value] of Object.entries( calculateAssumedMasterworkStats(item, armorEnergyRules), )) { armorPiecesStats[statHash] += value; } } for (const hash of armorStats) { stats[hash].value += armorPiecesStats[hash]; stats[hash].breakdown!.push({ hash: -1, count: undefined, name: t('Loadouts.ArmorStats'), icon: undefined, source: 'armorStats', value: armorPiecesStats[hash], }); } // Assign the chosen mods to items so we can display them as if they were slotted const { itemModAssignments, unassignedMods } = fitMostMods({ defs, items: armor, plannedMods: mods, armorEnergyRules: armorEnergyRules ?? inGameArmorEnergyRules, }); const modStats = getTotalModStatChanges( defs, unassignedMods, subclass, classType, includeRuntimeStatBenefits, itemModAssignments, armor, ); for (const [statHash, value] of Object.entries(modStats)) { stats[statHash].value += value.value; if (value.breakdown) { stats[statHash].breakdown?.push(...value.breakdown); } } return stats; } /** * Calculate an item's stats, assuming masterwork bonuses either if they * actually are masterworked or if we assume they are. */ export function calculateAssumedMasterworkStats( dimItem: DimItem, armorEnergyRules: ArmorEnergyRules | undefined, ): { [statHash: number]: number } { if (!dimItem.stats) { return emptyObject(); } const statMap: { [statHash: number]: number } = {}; const capacity = armorEnergyRules ? calculateAssumedItemEnergy(dimItem, armorEnergyRules) : (dimItem.energy?.energyCapacity ?? 0); // TODO: Rather than patch this directly, figure out what mod we'd insert // for the given assume-masterwork level, and apply its stats // 1. Find the correct masterwork socket on the item // 2. Look through the plugset attached to that socket for the correct masterwork level // 3. Evaluate conditional stats // 4. Apply the stats to the item // Alternatively, once we pick the right masterwork mod, we could use socketOverrides to get a resolved item with stats const assumeMasterworked = armorEnergyRules ? isAssumedMasterworked(dimItem, armorEnergyRules) : false; const newMasterworkType = isArmor3(dimItem); const mwPlug = newMasterworkType && dimItem.sockets?.allSockets.find(isArmor3MasterworkSocket)?.plugged; for (const { statHash, base } of dimItem.stats) { let value = base; // For now, manually apply the masterwork stats: if (!newMasterworkType && capacity >= MAX_ARMOR_ENERGY_CAPACITY) { // 1. If this is an armor 2.0 item, and it has max energy (whether // assumed or just normally), apply the legacy masterwork bonus (+2 // to each stat) value += MASTERWORK_ARMOR_STAT_BONUS; } else if (newMasterworkType && assumeMasterworked && value === 0) { // 2. If this is an armor 3.0 item, AND we're assuming it should be // masterworked, apply +5 to the three lowest stats (they will have a // base value of 0) value = 5; } else if (newMasterworkType && !assumeMasterworked && value === 0 && mwPlug) { // 3. If this is an armor 3.0 item, and we're NOT assuming it should be // masterworked, apply the current bonus from the item's masterwork // plug. value += mwPlug.stats?.[statHash]?.value ?? 0; } statMap[statHash] = value; } return statMap; } // Generate an optimized item set (loadout items) based on a filtered set of items and a value function export function optimalItemSet( applicableItems: DimItem[], store: DimStore, bestItemFn: (item: DimItem) => number, ): Record<'equippable' | 'equipUnrestricted' | 'classUnrestricted', DimItem[]> { const anyClassItemsByBucket = Object.groupBy(applicableItems, (i) => i.bucket.hash); const anyClassBestItemByBucket = mapValues( anyClassItemsByBucket, (thisSlotItems) => maxBy(thisSlotItems, bestItemFn)!, ); const classUnrestricted = Object.values(anyClassBestItemByBucket).sort( compareByIndex(gearSlotOrder, (i) => i.bucket.hash), ); const thisClassItemsByBucket = Object.groupBy( applicableItems.filter((i) => itemCanBeEquippedBy(i, store, true)), (i) => i.bucket.hash, ); const thisClassBestItemByBucket = mapValues( thisClassItemsByBucket, (thisSlotItems) => maxBy(thisSlotItems, bestItemFn)!, ); const equipUnrestricted = Object.values(thisClassBestItemByBucket).sort( compareByIndex(gearSlotOrder, (i) => i.bucket.hash), ); let equippableBestItemByBucket = { ...thisClassBestItemByBucket }; // Solve for the case where our optimizer decided to equip two exotics const getLabel = (i: DimItem) => i.equippingLabel; // All items that share an equipping label, grouped by label const overlaps = Map.groupBy(equipUnrestricted.filter(getLabel), (i) => getLabel(i)!); for (const overlappingItems of overlaps.values()) { if (overlappingItems.length <= 1) { continue; } const options: { [x: string]: DimItem }[] = []; // For each item, replace all the others overlapping it with the next best thing for (const item of overlappingItems) { const option = { ...equippableBestItemByBucket }; const otherItems = overlappingItems.filter((i) => i !== item); let optionValid = true; for (const otherItem of otherItems) { // Note: we could look for items that just don't have the *same* equippingLabel but // that may fail if there are ever mutual-exclusion items beyond exotics. const nonExotics = thisClassItemsByBucket[otherItem.bucket.hash].filter( (i) => !i.equippingLabel, ); if (nonExotics.length) { option[otherItem.bucket.hash] = maxBy(nonExotics, bestItemFn)!; } else { // this option isn't usable because we couldn't swap this exotic for any non-exotic optionValid = false; } } if (optionValid) { options.push(option); } } // Pick the option where the optimizer function adds up to the biggest number, again favoring equipped stuff if (options.length > 0) { const bestOption = maxBy(options, (opt) => sumBy(Object.values(opt), bestItemFn))!; equippableBestItemByBucket = bestOption; } } const equippable = Object.values(equippableBestItemByBucket).sort( compareByIndex(gearSlotOrder, (i) => i.bucket.hash), ); return { equippable, equipUnrestricted, classUnrestricted }; } export function optimalLoadout( applicableItems: DimItem[], store: DimStore, bestItemFn: (item: DimItem) => number, name: string, ): Loadout { const { equippable } = optimalItemSet(applicableItems, store, bestItemFn); return newLoadout( name, equippable.map((i) => convertToLoadoutItem(i, true)), ); } /** * Create a loadout from all of this character's items that can be in loadouts, * as a backup. */ export function backupLoadout(store: DimStore, name: string): Loadout { const allItems = store.items.filter( (item) => itemCanBeInLoadout(item) && !item.location.inPostmaster, ); const loadout = newLoadout( name, allItems.map((i) => { const item = convertToLoadoutItem(i, i.equipped); if (i.bucket.hash === BucketHashes.Subclass) { item.socketOverrides = createSocketOverridesFromEquipped(i); } return item; }), ); // Save mods too, so we put them back if you undo loadout.parameters = { mods: allItems.filter((i) => i.equipped).flatMap(extractArmorModHashes), }; return loadout; } /** * Converts DimItem to a LoadoutItem. */ export function convertToLoadoutItem(item: DimItem, equip: boolean): LoadoutItem { return { id: item.id, hash: item.hash, amount: item.amount, equip, craftedDate: item.craftedInfo?.craftedDate, }; } /** Extracts the equipped armour 2.0 mod hashes from the item */ export function extractArmorModHashes(item: DimItem) { if (!isLoadoutBuilderItem(item) || !item.sockets) { return []; } return filterMap(item.sockets.allSockets, (socket) => socket.plugged && isInsertableArmor2Mod(socket.plugged.plugDef) ? socket.plugged.plugDef.hash : undefined, ); } /** * Some items have been replaced with equivalent new items. So far that's been * true of the "Light 2.0" subclasses which are an entirely different item from * the old one. When loading loadouts we'd like to just use the new version. */ const oldToNewItems: HashLookup = { // Arcstrider 1334959255: 2328211300, // Striker 2958378809: 2932390016, // Stormcaller 1751782730: 3168997075, // Gunslinger 3635991036: 2240888816, // Sunbreaker 3105935002: 2550323932, // Dawnblade 3481861797: 3941205951, // Nightstalker 3225959819: 2453351420, // Sentinel 3382391785: 2842471112, // Voidwalker 3887892656: 2849050827, }; /** * Items that are technically instanced but should always * be matched by hash. */ const matchByHash = [ BucketHashes.Subclass, BucketHashes.Emblems, BucketHashes.Emotes, D1BucketHashes.Horn, D1BucketHashes.Shader, ]; /** * Figure out how a LoadoutItem with a given hash should be resolved: * By hash or by id, and by which hash. */ export function getResolutionInfo( defs: D1ManifestDefinitions | D2ManifestDefinitions, loadoutItemHash: number, ): | { hash: number; instanced: boolean; bucketHash: number; } | undefined { const hash = oldToNewItems[loadoutItemHash] ?? loadoutItemHash; const def = defs.InventoryItem.get(hash) as | undefined | (DestinyInventoryItemDefinition & { // D1 definitions use this toplevel "instanced" field instanced: boolean; bucketTypeHash: number; }); // in this world, there are no guarantees if (!def) { return; } // Instanced items match by ID, uninstanced match by hash. It'd actually be // nice to use "is random rolled or configurable" here instead but that's hard // to determine. const bucketHash = def.bucketTypeHash || def.inventory?.bucketTypeHash || 0; const instanced = Boolean( (def.instanced || def.inventory?.isInstanceItem) && // Subclasses and some other types are technically instanced but should be matched by hash !matchByHash.includes(bucketHash), ); return { hash, instanced, bucketHash, }; } /** * Returns the index of the LoadoutItem in the list of loadoutItems that would * resolve to the provided item, or -1 if not found. This is meant for finding * existing items that match some incoming item. */ export function findSameLoadoutItemIndex( defs: D1ManifestDefinitions | D2ManifestDefinitions, loadoutItems: LoadoutItem[], item: DimItem, ) { const info = getResolutionInfo(defs, item.hash)!; return loadoutItems.findIndex((i) => { const newHash = oldToNewItems[i.hash] ?? i.hash; return ( info.hash === newHash && (!info.instanced || item.id === i.id || // Crafted items may change ID but keep their date (item.craftedInfo?.craftedDate && item.craftedInfo.craftedDate === i.craftedDate)) ); }); } /** * Given a loadout item specification, find the corresponding inventory item we should use. */ export function findItemForLoadout( defs: D1ManifestDefinitions | D2ManifestDefinitions, allItems: DimItem[], storeId: string | undefined, loadoutItem: LoadoutItem, ): DimItem | undefined { const info = getResolutionInfo(defs, loadoutItem.hash); if (!info) { return; } if (info.instanced) { return getInstancedLoadoutItem(allItems, loadoutItem); } return getUninstancedLoadoutItem(allItems, info.hash, storeId); } /** * Get a mapping from item id to item. Used for looking up items from loadouts. * This used to be restricted to only items that could be in loadouts, but we * need it to be all items to make search-based loadout transfers work. */ export const itemsByItemId = weakMemoize((allItems: DimItem[]) => keyBy( allItems.filter((i) => i.id !== '0' && itemCanBeInLoadout(i)), (i) => i.id, ), ); /** * Get a mapping from crafted date to item, for items that could be in loadouts. Used for * looking up items from loadouts. */ const itemsByCraftedDate = weakMemoize((allItems: DimItem[]) => keyBy( allItems.filter((i) => i.instanced && i.craftedInfo?.craftedDate), (i) => i.craftedInfo!.craftedDate, ), ); export function getInstancedLoadoutItem(allItems: DimItem[], loadoutItem: LoadoutItem) { const result = itemsByItemId(allItems)[loadoutItem.id]; if (result) { return result; } // Crafted items get new IDs, but keep their crafted date, so we can match on that if (loadoutItem.craftedDate) { return itemsByCraftedDate(allItems)[loadoutItem.craftedDate]; } } /** * Get a mapping from item hash to item. Used for looking up items from * loadouts. This used to be restricted to only items that could be in loadouts, * but we need it to be all items to make search-based loadout transfers work. */ const itemsByHash = weakMemoize((allItems: DimItem[]) => Map.groupBy(allItems, (i) => i.hash)); export function getUninstancedLoadoutItem( allItems: DimItem[], hash: number, storeId: string | undefined, ) { // This is for subclasses and emblems - it finds all matching items by hash and then picks the one that's on the desired character // It's also used for moving consumables in search loadouts const candidates = itemsByHash(allItems).get(hash) ?? []; // the copy of this item being held by the specified store const heldItem = storeId !== undefined ? candidates.find((item) => item.owner === storeId) : undefined; return heldItem ?? (candidates[0]?.notransfer ? undefined : candidates[0]); } /** * Given a loadout, see how many Fragments can be plugged * * If the loadout supplies Aspects, we use them to calculate Fragment capacity. * * When *applying* a Loadout subclass configuration with no Aspects, we make no Aspect changes. * Thus, `fallbackToCurrent` controls whether to use the subclass's current Aspect config. */ export function getLoadoutSubclassFragmentCapacity( defs: D2ManifestDefinitions, item: ResolvedLoadoutItem, fallbackToCurrent: boolean, ): number { if (item.item.sockets) { const aspectSocketIndices = item.item.sockets.categories.find((c) => aspectSocketCategoryHashes.includes(c.category.hash)) ?.socketIndexes ?? []; const aspectDefs = item.loadoutItem.socketOverrides && filterMap(aspectSocketIndices, (aspectSocketIndex) => { const aspectHash = item.loadoutItem.socketOverrides![aspectSocketIndex]; return aspectHash ? defs.InventoryItem.get(aspectHash) : undefined; }); if (aspectDefs?.length) { // the loadout provided some aspects. use those. return sumAspectCapacity(aspectDefs); } else if (fallbackToCurrent) { // the loadout provided no aspects. assume the currently applied aspects. return getSubclassFragmentCapacity(item.item); } else { return 0; } } return 0; } export function isMissingItems( defs: D1ManifestDefinitions | D2ManifestDefinitions, allItems: DimItem[], storeId: string, loadout: Loadout, ): boolean { for (const loadoutItem of loadout.items) { const info = getResolutionInfo(defs, loadoutItem.hash); if (!info) { // If an item hash is entirely missing from the database, we show that // there is a missing item but can't offer a replacement (or even show // which item went missing), but we can maybe add a migration in `oldToNewItems`? return true; } if (info.instanced) { if (!getInstancedLoadoutItem(allItems, loadoutItem)) { return true; } } else if (storeId !== 'vault' && !getUninstancedLoadoutItem(allItems, info.hash, storeId)) { // The vault can't really have uninstanced items like subclasses or emblems, so no point // in reporting a missing item in that case. return true; } } return false; } export function isFashionOnly(defs: D2ManifestDefinitions, loadout: Loadout): boolean { if (loadout.items.length) { return false; } if (!loadout.parameters?.modsByBucket) { return false; } for (const bucketHash in loadout.parameters.modsByBucket) { // if this is mods for a non-armor bucket if (!ArmorBucketHashes.includes(Number(bucketHash))) { return false; } const modsForThisArmorSlot = loadout.parameters.modsByBucket[bucketHash]; if ( modsForThisArmorSlot.some( (modHash) => !isFashionPlug(defs.InventoryItem.getOptional(modHash)), ) ) { return false; } } return true; } /** not fashion mods, just useful ones */ export function isArmorModsOnly(defs: D2ManifestDefinitions, loadout: Loadout): boolean { // if it contains armor, it's not a mods-only loadout if (loadout.items.length) { return false; } // if there's no mods at all, this isn't a mods-only loadout if (!loadout.parameters?.mods?.length && !loadout.parameters?.modsByBucket) { return false; } // if there's specific mods, make sure none are fashion if (loadout.parameters?.modsByBucket) { for (const bucketHash in loadout.parameters.modsByBucket) { // if this is mods for a non-armor bucket if (!ArmorBucketHashes.includes(Number(bucketHash))) { return false; } const modsForThisArmorSlot = loadout.parameters.modsByBucket[bucketHash]; if ( modsForThisArmorSlot.some((modHash) => isFashionPlug(defs.InventoryItem.getOptional(modHash)), ) ) { return false; } } } return true; } /** given a hash we know is a plug, is this a fashion plug? */ export function isFashionPlug(modDef: DestinyInventoryItemDefinition | undefined): boolean { return Boolean( modDef && (modDef.itemSubType === DestinyItemSubType.Shader || modDef.itemSubType === DestinyItemSubType.Ornament || modDef.itemType === DestinyItemType.Armor), ); } /** * Returns a flat list of mods as PluggableInventoryItemDefinitions in the Loadout, by default including auto stat mods. * This INCLUDES both locked and unlocked mods; `unlockedPlugs` is used to identify if the expensive or cheap copy of an * armor mod should be used. */ export function getModsFromLoadout( defs: D2ManifestDefinitions | undefined, loadout: Loadout, unlockedPlugs = new Set(), ) { const internalModHashes = loadout.parameters?.mods ?? []; return resolveLoadoutModHashes(defs, internalModHashes, unlockedPlugs); } const oldToNewMod: HashLookup = { 204137529: 1703647492, // InventoryItem "Minor Mobility Mod" 3961599962: 4183296050, // InventoryItem "Mobility Mod" 3682186345: 2532323436, // InventoryItem "Minor Resilience Mod" 2850583378: 1180408010, // InventoryItem "Resilience Mod" 555005975: 1237786518, // InventoryItem "Minor Recovery Mod" 2645858828: 4204488676, // InventoryItem "Recovery Mod" 2623485440: 4021790309, // InventoryItem "Minor Discipline Mod" 4048838440: 1435557120, // InventoryItem "Discipline Mod" 1227870362: 350061697, // InventoryItem "Minor Intellect Mod" 3355995799: 2724608735, // InventoryItem "Intellect Mod" 3699676109: 2639422088, // InventoryItem "Minor Strength Mod" 3253038666: 4287799666, // InventoryItem "Strength Mod" }; /** * Convert a list of plug item hashes into ResolvedLoadoutMods, which may not be * the same as the original hashes as we try to be smart about what the user meant. * e.g. we replace some mods with lower-cost variants depending on the artifact state. * @param unlockedPlugs all unlocked mod hashes. See unlockedPlugSetItemsSelector. */ export function resolveLoadoutModHashes( defs: D2ManifestDefinitions | undefined, modHashes: number[] | undefined, unlockedPlugs: Set, ) { const mods: ResolvedLoadoutMod[] = []; if (defs && modHashes) { for (const originalModHash of modHashes) { const migratedModHash = oldToNewMod[originalModHash] ?? originalModHash; const resolvedModHash = mapToAvailableModCostVariant(migratedModHash, unlockedPlugs); const item = defs.InventoryItem.getOptional(resolvedModHash); if (isPluggableItem(item)) { mods.push({ originalModHash, resolvedMod: item }); } else { const deprecatedPlaceholderMod = defs.InventoryItem.get(deprecatedPlaceholderArmorModHash); if (isPluggableItem(deprecatedPlaceholderMod)) { mods.push({ originalModHash, resolvedMod: deprecatedPlaceholderMod }); } else { throw new Error( `Could not find deprecated placeholder mod definition, hash: ${ deprecatedPlaceholderArmorModHash }`, ); } } } } return mods.sort((a, b) => sortMods(a.resolvedMod, b.resolvedMod)); } /** * given a real (or overridden) subclass item, * determine how many Fragment slots are provided by its current Aspects */ function getSubclassFragmentCapacity(subclassItem: DimItem): number { const aspects = getSocketsByCategoryHashes(subclassItem.sockets, aspectSocketCategoryHashes); return sumAspectCapacity(aspects.map((a) => a.plugged?.plugDef)); } /** given some Aspects or Aspect sockets, see how many Fragment slots they'll provide */ function sumAspectCapacity( aspects: (DestinyInventoryItemDefinition | DimSocket | undefined)[] | undefined, ) { if (!aspects?.length) { return 0; } return sumBy(aspects, (aspect) => { const aspectDef = aspect && 'plugged' in aspect ? aspect.plugged?.plugDef : aspect; return aspectDef?.plug?.energyCapacity?.capacityValue ?? 0; }); } /** * filter for items that are in a character's "pockets" but not equipped, * and can be added to a loadout */ export function getUnequippedItemsForLoadout(dimStore: DimStore, category?: string) { return dimStore.items.filter( (item) => !item.equipped && !item.location.inPostmaster && !singularBucketHashes.includes(item.bucket.hash) && itemCanBeInLoadout(item) && isClassCompatible(item.classType, dimStore.classType) && (category ? item.bucket.sort === category : fromEquippedTypes.includes(item.bucket.hash)), ); } /** * Pick a (non-vault) store that backs the loadout drawer, using the * preferredStoreId if it matches the classType and using the first * matching store otherwise. */ export function pickBackingStore( stores: DimStore[], preferredStoreId: string | undefined, classType: DestinyClass, ) { classType = classType === DestinyClass.Classified ? DestinyClass.Unknown : classType; const requestedStore = !preferredStoreId || preferredStoreId === 'vault' ? getCurrentStore(stores) : getStore(stores, preferredStoreId); return requestedStore && isClassCompatible(classType, requestedStore.classType) ? requestedStore : stores.find((s) => !s.isVault && isClassCompatible(classType, s.classType)); } /** * Remove items and settings that don't match the loadout's class type. */ export function filterLoadoutToAllowedItems( defs: D2ManifestDefinitions | D1ManifestDefinitions, loadoutToSave: Readonly, ): Readonly { return produce(loadoutToSave, (loadout) => { // Filter out items that don't fit the class type loadout.items = loadout.items.filter((loadoutItem) => { const classType = defs.InventoryItem.get(loadoutItem.hash)?.classType; return classType !== undefined && isItemLoadoutCompatible(classType, loadout.classType); }); if (loadout.classType === DestinyClass.Unknown && loadout.parameters) { // Remove fashion and non-mod loadout parameters from Any Class loadouts // FIXME It's really easy to forget to consider properties of LoadoutParameters here, // maybe some type voodoo can force us to make a decision for every property? if ( loadout.parameters.mods?.length || loadout.parameters.clearMods || loadout.parameters.artifactUnlocks || // weapons but not armor since AnyClass loadouts can't have armor loadout.parameters.clearWeapons ) { loadout.parameters = { mods: loadout.parameters.mods, clearMods: loadout.parameters.clearMods, artifactUnlocks: loadout.parameters.artifactUnlocks, clearWeapons: loadout.parameters.clearWeapons, }; } else { delete loadout.parameters; } } }); } export function getLoadoutSeason(loadout: Loadout, seasons: DestinySeasonDefinition[]) { return seasons.find( (s) => new Date(s.startDate!).getTime() <= (loadout.lastUpdatedAt ?? Date.now()), ); } ================================================ FILE: src/app/loadout-drawer/postmaster.ts ================================================ import { t } from 'app/i18next-t'; import { postmasterNotification } from 'app/inventory/MoveNotifications'; import { storesSelector } from 'app/inventory/selectors'; import { capacityForItem, findItemsByBucket, getVault, potentialSpaceLeftForItem, spaceLeftForItem, } from 'app/inventory/stores-helpers'; import type { ItemRarityName } from 'app/search/d2-known-values'; import { ThunkResult } from 'app/store/types'; import { CancelToken, CanceledError, withCancel } from 'app/utils/cancel'; import { compareBy } from 'app/utils/comparators'; import { DimError } from 'app/utils/dim-error'; import { convertToError, errorMessage } from 'app/utils/errors'; import { errorLog } from 'app/utils/log'; import { BucketHashes } from 'data/d2/generated-enums'; import { countBy, memoize, throttle } from 'es-toolkit'; import { InventoryBuckets } from '../inventory/inventory-buckets'; import { MoveReservations, createMoveSession, executeMoveItem, } from '../inventory/item-move-service'; import { DimItem } from '../inventory/item-types'; import { DimStore } from '../inventory/store-types'; import { showNotification } from '../notifications/notifications'; // weight "move an item aside" options, according to their rarity const moveAsideWeighting: Record = { Legendary: 4, Rare: 3, Uncommon: 2, Common: 1, Exotic: 0, Currency: 0, Unknown: 0, }; export function makeRoomForPostmaster(store: DimStore, buckets: InventoryBuckets): ThunkResult { return async (dispatch) => { const postmasterItems: DimItem[] = buckets.byCategory.Postmaster.flatMap((bucket) => findItemsByBucket(store, bucket.hash), ); const postmasterItemCountsByType = countBy(postmasterItems, (i) => i.bucket.hash); const [cancelToken, cancel] = withCancel(); // If any category is full, we'll move enough aside const itemsToMove: DimItem[] = []; for (const [bucket, count] of Object.entries(postmasterItemCountsByType)) { const bucketHash = parseInt(bucket, 10); if (count > 0 && findItemsByBucket(store, bucketHash).length > 0) { const items: DimItem[] = findItemsByBucket(store, bucketHash); const capacity = capacityForItem(store, items[0]); const numNeededToMove = Math.max(0, count + items.length - capacity); if (numNeededToMove > 0) { // We'll move the lowest-value item to the vault. const candidates = items .filter((i) => !i.equipped && !i.notransfer) .sort( compareBy((i) => { let value = moveAsideWeighting[i.rarity]; // And low-stat value += i.power / 1000; return value; }), ); itemsToMove.push(...candidates.slice(0, numNeededToMove)); } } } try { await dispatch(moveItemsToVault(store, itemsToMove, cancelToken)); showNotification({ type: 'success', title: t('Loadouts.MakeRoom'), body: t('Loadouts.MakeRoomDone', { count: postmasterItems.length, movedNum: itemsToMove.length, store: store.name, context: store.genderName, }), onCancel: cancel, }); } catch (e) { if (!(e instanceof CanceledError)) { showNotification({ type: 'error', title: t('Loadouts.MakeRoom'), body: t('Loadouts.MakeRoomError', { error: errorMessage(e) }), }); throw e; } } }; } // D2 only export function pullablePostmasterItems(store: DimStore, stores: DimStore[]) { return (findItemsByBucket(store, BucketHashes.LostItems) || []).filter( (i) => pullFromPostmasterAmount(i, store, stores) > 0, ); } /** * How many of the given item's stack can be pulled from postmaster into a store? */ export function pullFromPostmasterAmount(i: DimItem, store: DimStore, stores: DimStore[]) { if (!i.canPullFromPostmaster) { // can't be pulled return 0; } const potentialSpace = potentialSpaceLeftForItem(store, i, stores); if (potentialSpace.guaranteed) { // We have space for this many items, but can only pull as many as we have in this stack return Math.min(potentialSpace.guaranteed, i.amount); } else if (potentialSpace.couldMakeSpace) { // We could make space, so assume the whole stack can be transferred return i.amount; } else { // No space return 0; } } // We should load this from the manifest but it's hard to get it in here export const POSTMASTER_SIZE = 21; export function postmasterAlmostFull(store: DimStore) { return postmasterSpaceLeft(store) < 6; // I think you can get 6 drops at once in some activities } function postmasterSpaceLeft(store: DimStore) { return Math.max(0, POSTMASTER_SIZE - totalPostmasterItems(store)); } export function postmasterSpaceUsed(store: DimStore) { return POSTMASTER_SIZE - postmasterSpaceLeft(store); } export function totalPostmasterItems(store: DimStore) { return findItemsByBucket(store, BucketHashes.LostItems).length; } const showNoSpaceError = throttle( (e: Error) => showNotification({ type: 'error', title: t('Loadouts.PullFromPostmasterPopupTitle'), body: t('Loadouts.NoSpace', { error: e.message }), }), 1000, { edges: ['leading'] }, ); // D2 only export function pullFromPostmaster(store: DimStore): ThunkResult { return async (dispatch, getState) => { const stores = storesSelector(getState()); const items = pullablePostmasterItems(store, stores); // Only show one popup per message const errorNotification = memoize((message: string) => { showNotification({ type: 'error', title: t('Loadouts.PullFromPostmasterPopupTitle'), body: t('Loadouts.PullFromPostmasterError', { error: message }), }); }); const [cancelToken, cancel] = withCancel(); const promise = (async () => { let succeeded = 0; const moveSession = createMoveSession(cancelToken, items); for (const item of items) { let amount = item.amount; if (item.uniqueStack) { const spaceLeft = spaceLeftForItem(store, item, storesSelector(getState())); if (spaceLeft > 0) { // Only transfer enough to top off the stack amount = Math.min(item.amount || 1, spaceLeft); } // otherwise try the move anyway - it may be that you don't have any but your bucket // is full, so it'll move aside something else (or the stack itself can be moved into // the vault). Otherwise it'll fail in moveTo. } try { await dispatch(executeMoveItem(item, store, { equip: false, amount }, moveSession)); succeeded++; } catch (err) { const e = convertToError(err); if (e instanceof CanceledError) { return false; } // TODO: collect and summarize errors? errorLog('postmaster', `Error pulling ${item.name} from postmaster`, e); if (e instanceof DimError && e.code === 'no-space') { if (items.length === 1) { // Transform the notification into an error throw new DimError('Loadouts.NoSpace', t('Loadouts.NoSpace', { error: e.message })); } else { // Show the error separately and continue showNoSpaceError(e); } } else if (items.length === 1) { // Transform the notification into an error throw new DimError( 'Loadouts.PullFromPostmasterError', t('Loadouts.PullFromPostmasterError', { error: e.message }), ); } else { // Show the error separately and continue errorNotification(e.message); } } } if (!succeeded) { throw new DimError('Loadouts.PullFromPostmasterGeneralError'); } })(); showNotification(postmasterNotification(items.length, store, promise, cancel)); await promise; }; } // cribbed from D1FarmingService, but modified function moveItemsToVault( store: DimStore, items: DimItem[], cancelToken: CancelToken, ): ThunkResult { return async (dispatch, getState) => { const reservations: MoveReservations = { // reserve space for all move-asides [store.id]: countBy(items, (i) => i.bucket.hash), }; const moveSession = createMoveSession(cancelToken, items); for (const item of items) { const stores = storesSelector(getState()); // Move a single item. We reevaluate the vault each time in case things have changed. const vault = getVault(stores)!; const vaultSpaceLeft = spaceLeftForItem(vault, item, stores); if (vaultSpaceLeft <= 1) { // If we're down to one space, try putting it on other characters const otherStores = stores.filter((s) => !s.isVault && s.id !== store.id); const otherStoresWithSpace = otherStores.filter((store) => spaceLeftForItem(store, item, stores), ); if (otherStoresWithSpace.length) { await dispatch( executeMoveItem( item, otherStoresWithSpace[0], { equip: false, amount: item.amount, excludes: items, reservations, }, moveSession, ), ); continue; } } await dispatch( executeMoveItem( item, vault, { equip: false, amount: item.amount, excludes: items, reservations, }, moveSession, ), ); } }; } ================================================ FILE: src/app/login/Login.m.scss ================================================ @use 'sass:color'; @use '../variables.scss' as *; .billboard { display: flex; flex-direction: column; align-items: stretch; background-color: rgb(0, 0, 0, 0.6); color: var(--theme-text); max-width: 800px; border-top: 5px solid #888; box-shadow: 0 0 2px rgb(0, 0, 0, 0.5); padding: 1rem 3rem; text-align: center; z-index: 99999; white-space: pre-wrap; box-sizing: border-box; margin: auto; h1 { margin: 0; text-align: center; @include destiny-header; } } .explanation { font-size: 16px; margin: 8px 0 16px 0; } .section { border-top: 1px solid #444; text-align: center; padding-top: 2em; margin-top: 2em; a { text-decoration: underline; color: #ccc; cursor: pointer; } } .dimSync { composes: section; display: flex; flex-direction: column; align-items: center; } .dimSyncCheckbox { width: fit-content; margin: 0 0 0.5em 0; } .fineprint { color: var(--theme-text-secondary); } .warning { margin: 16px 0 0 0; padding: 0.85em; background: color.scale($red, $lightness: -90%); border-top: 4px solid $red; h2 { margin: 0 0 8px 0 !important; letter-spacing: normal !important; text-transform: none !important; } a { color: var(--theme-text); font-size: 12px; } button { display: block; margin: 0.5em auto 0 auto; } } .auth { composes: dim-button from global; font-size: 1rem; font-weight: bold; text-align: center; padding: 1em 3em; background-color: var(--theme-accent-primary); color: var(--theme-text-invert); text-shadow: none; @include interactive($hover: true) { transform: scale(1.05); } } ================================================ FILE: src/app/login/Login.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'auth': string; 'billboard': string; 'dimSync': string; 'dimSyncCheckbox': string; 'explanation': string; 'fineprint': string; 'section': string; 'warning': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/login/Login.tsx ================================================ import CheckButton from 'app/dim-ui/CheckButton'; import ExternalLink from 'app/dim-ui/ExternalLink'; import { t } from 'app/i18next-t'; import { userGuideUrl } from 'app/shell/links'; import { exportBackupData, exportLocalData } from 'app/storage/export-data'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { isAppStoreVersion } from 'app/utils/browsers'; import { useEffect, useMemo, useState } from 'react'; import { Link, useLocation } from 'react-router'; import { oauthClientId } from '../bungie-api/bungie-api-utils'; import * as styles from './Login.m.scss'; export const dimApiHelpLink = userGuideUrl('DIM-Sync'); const loginHelpLink = userGuideUrl('Accounts-and-Login'); export default function Login() { const dispatch = useThunkDispatch(); const authorizationState = useMemo( () => (isAppStoreVersion() ? 'dimauth-' : '') + globalThis.crypto.randomUUID(), [], ); const clientId = oauthClientId(); const location = useLocation(); const state = location.state as { path?: string } | undefined; const previousPath = state?.path; useEffect(() => { localStorage.setItem('authorizationState', authorizationState); }, [authorizationState]); // Save the path we were originally on, so we can restore it after login in the DefaultAccount component. useEffect(() => { if (previousPath) { localStorage.setItem('returnPath', $PUBLIC_PATH.replace(/\/$/, '') + previousPath); } }, [previousPath]); const authorizationURL = (reauth?: string) => { const queryParams = new URLSearchParams({ client_id: clientId, response_type: 'code', state: authorizationState, ...(reauth && { reauth }), }); return `https://www.bungie.net/en/OAuth/Authorize?${queryParams.toString()}`; }; // If API permissions had been explicitly disabled before, don't even show the option to enable DIM Sync const apiPermissionPreviouslyDisabled = localStorage.getItem('dim-api-enabled') === 'false'; const [apiPermissionGranted, setApiPermissionGranted] = useState(() => { const enabled = localStorage.getItem('dim-api-enabled') !== 'false'; localStorage.setItem('dim-api-enabled', JSON.stringify(enabled)); return enabled; }); const onApiPermissionChange = (checked: boolean) => { localStorage.setItem('dim-api-enabled', JSON.stringify(checked)); setApiPermissionGranted(checked); }; const onExportData = async () => { // Export from local data const data = await dispatch(exportLocalData()); exportBackupData(data); }; return (

{t('Views.Login.Permission')}

{t('Views.Login.Explanation')}

{t('Views.Login.Auth')}

{t('Storage.EnableDimApi')}
{t('Storage.DimApiFinePrint')}{' '} {t('Storage.LearnMore')}
{apiPermissionPreviouslyDisabled && apiPermissionGranted && (
{t('Views.Login.EnableDimSyncWarning')}
)} {!apiPermissionPreviouslyDisabled && !apiPermissionGranted && (
{t('Storage.DimSyncNotEnabled')}
)}
{t('Views.Login.LearnMore')} |{' '} Privacy Policy
); } ================================================ FILE: src/app/main.scss ================================================ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // DIM Main stylesheet // // The main stylesheet for all of the styles! // // Prefer CSS modules, or at least stylesheets that are imported directly by the components that need them, to putting things here. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @use 'variables.scss' as *; @use 'themes/theme'; @use 'themes/theme-dimdark'; @use 'themes/theme-pyramid'; @use 'themes/theme-classic'; @use 'themes/theme-neomuna'; @use 'themes/theme-throneworld'; @use 'themes/theme-vexnet'; @use 'themes/theme-europa'; @use 'app/dim-ui/dim-button.scss'; @layer base { @font-face { font-family: 'Destiny Symbols'; src: url('../data/font/DestinySymbols.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: fallback; } @font-face { font-family: 'DIM Symbols'; src: url('../data/font/DIMSymbols.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: fallback; } @media (max-width: 1025px) { :root { --item-size: 48px; } } @keyframes browser-warning { to { opacity: 0; visibility: hidden; } } :root { // Item width including border --item-size: 50px; // Margin on the bottom and right of an item --item-margin: 6px; // Padding at the ends of inventory column --inventory-column-padding: 12px; // ultimately user-determined. default values here --num-characters: 3; --tiles-per-char-column: 3; // equipped item plus character's inventory --character-column-width: calc( (var(--tiles-per-char-column) + 1) * (var(--item-size) + var(--item-margin)) ); // used for phone portrait mode --column-padding: calc(2 * var(--inventory-column-padding) - var(--item-margin)); // The height of the visible viewport, taking into account virtual keyboards on iOS. Set by JS. This is more accurate than 100vh! --viewport-height: 100vh; // The bottom offset of the visual viewport from the layout viewport. Set by JS. This can help attach things to virtual keyboards. --viewport-bottom-offset: 0; // The height of the header including padding that may be added on rounded-corner phones. Set by JS. --header-height: 44px; // The height of the inventory store header section. Set by JS. --store-header-height: 62px; // How many desktop mode sidebar trays are open --expanded-sidebars: 0; --sidebar-size: 277px; // How big to render each engram in the row of engrams in the Postmaster (on the Inventory screen). --engram-size: calc(var(--character-column-width) / 10); // Rumors have it that the Windows scrollbar size is 17px. I always thought // it was 16px. --scrollbar-size: 17px; // Whether to display ornament or base icons --ornament-display-opacity: 1; --ornament-display-visibility: auto; --ornament-display-visibility-inverse: hidden; // prevents content shift with persistent scroll-y bar overflow-y: scroll; } /* stylelint-disable-next-line order/order */ @include phone-portrait { :root { --item-margin: 10px; // Padding at the ends of inventory column --inventory-column-padding: 12px; /* prettier-ignore */ --item-size: calc( ( 100vw - var(--column-padding) - #{$equipped-item-total-outset} - var(--combined-item-margins) ) / (var(--tiles-per-char-column) + 1) ) !important; --column-padding: calc(2 * var(--inventory-column-padding) - var(--item-margin)); --combined-item-margins: calc(var(--item-margin) * (var(--tiles-per-char-column) + 1)); --engram-size: calc((100vw - (2 * var(--inventory-column-padding))) / 10); // sets scroll bar back to auto as the content shift an issue at this size. overflow-y: auto; } .char-cols-3 { --item-margin: 15px; // Padding at the ends of inventory column --inventory-column-padding: 34px; // this is duplicated for .char-cols-3 to recalculate using local var adjustments /* prettier-ignore */ --item-size: calc( ( 100vw - var(--column-padding) - #{$equipped-item-total-outset} - var(--combined-item-margins) ) / (var(--tiles-per-char-column) + 1) ) !important; --column-padding: calc(2 * var(--inventory-column-padding) - var(--item-margin)); --combined-item-margins: calc(var(--item-margin) * (var(--tiles-per-char-column) + 1)); } } h1, h2, h3, h4, h5 { font-weight: normal; } label { cursor: inherit; } // Styles for the warning saying DIM isn't compatible with your browser. Once this CSS loads the warning will fade out after some time. #browser-warning { position: fixed; bottom: 0; left: 0; right: 0; background: #900; padding: 8px; text-align: center; animation: 1s linear 10s forwards browser-warning; &.hidden { display: none; } } a { color: var(--theme-text); cursor: pointer; text-decoration: underline; } h2, h3 { margin-top: 20px; margin-bottom: 15px; } html, body { // Disable pull to refresh in Android overscroll-behavior: none; // Adjust so the anchor we scroll to doesn't end up behind the header scroll-padding-top: var(--header-height); @include phone-portrait { user-select: none; } } body { margin: 0 auto; background-color: var(--theme-pwa-background); color: var(--theme-text); font-family: 'Open Sans', sans-serif, 'Destiny Symbols', 'DIM Symbols'; font-size: 12px; line-height: calc(16 / 12); accent-color: var(--theme-accent-primary); // Disable drag and drop so we can use our polyfill -webkit-user-drag: none; // Don't let iOS Safari mess with font sizes text-size-adjust: none; overflow-wrap: break-word; } h2, h3, h4 { font-weight: 400; } *[role='button'] { cursor: pointer; } /* Forms */ input, select, option { font-family: 'Open Sans', sans-serif, 'Destiny Symbols', 'DIM Symbols'; } input[type='text'], input[type='search'] { color: var(--theme-text); caret-color: var(--theme-accent-primary); background-color: var(--theme-input-bg); } input[type='search'] { appearance: textfield; } textarea { width: 100%; height: 30px; background-color: var(--theme-input-bg); padding: 4px 8px; border: none; outline: none; color: var(--theme-text); box-sizing: border-box; font-family: monospace, 'Destiny Symbols', 'DIM Symbols'; font-size: inherit; // Setting this makes yuku/textcomplete faster because it doesn't need to make a fake element to measure line height (incorrectly, too) line-height: calc(16 / 12); caret-color: var(--theme-accent-primary); @include interactive($hover: true, $focus: true) { box-shadow: inset 0 0 0 1px var(--theme-search-dropdown-border); } } select { appearance: none; background: url('data:image/svg+xml;utf8,') no-repeat; background-size: 10px; background-position: calc(100% - 10px) center; background-repeat: no-repeat; border-radius: 0; background-color: var(--theme-button-bg); padding: 2px 28px 2px 10px; height: 27px; font-size: 12px; line-height: calc(16 / 12); color: var(--theme-text); text-shadow: 1px 1px 3px rgb(0, 0, 0, 0.25); border: 1px solid transparent; @include phone-portrait { font-size: 14px; line-height: calc(18 / 14); padding: 6px 32px 6px 16px; height: 35px; } @include interactive($hover: true, $active: true) { background-color: var(--theme-accent-primary); color: var(--theme-text-invert); background-image: url('data:image/svg+xml;utf8,'); } // Set focus styles &:focus { border-color: var(--theme-accent-primary); outline: none; } // For browsers that support :focus-visible, remove focus styles when focus-visible would be unset &:focus:not(:focus-visible) { border-color: transparent; } option { background: black; color: var(--theme-text); } } code { position: relative; background: rgb(255, 255, 255, 0.05); border: 1px solid rgb(255, 255, 255, 0.1); border-radius: 3px; padding: 0 4px; bottom: 1px; } /** Misc DIM page **/ .dim-page { // non-inventory page settings max-width: 900px; margin: 0 auto; } // Use this to make images not tappable (Android Chrome will show a download menu) .no-pointer-events, .app-icon { @include phone-portrait { pointer-events: none; } } .horizontal-swipable { touch-action: pan-y; } // bungie's stat icons are semitransparent... boooo. // when applied to an armor stat icon, this bolsters their visibility with a same-size dropshadow .stat-icon { filter: drop-shadow(0 0 0 #fff); } } ================================================ FILE: src/app/manifest/actions.ts ================================================ import { getBungieNetSettings } from 'app/bungie-api/bungie-core-api'; import { ThunkResult } from 'app/store/types'; import { Destiny2CoreSettings } from 'bungie-api-ts/core'; import { createAction } from 'typesafe-actions'; import { D1ManifestDefinitions } from '../destiny1/d1-definitions'; import { D2ManifestDefinitions } from '../destiny2/d2-definitions'; export const setD2Manifest = createAction('manifest/D2')(); export const setD1Manifest = createAction('manifest/D1')(); export const coreSettingsLoaded = createAction('manifest/CORE_SETTINGS')(); export function loadCoreSettings(): ThunkResult { return async (dispatch, getState) => { if (getState().manifest.destiny2CoreSettings) { return; } const settings = await getBungieNetSettings(); if (getState().manifest.destiny2CoreSettings) { return; } dispatch(coreSettingsLoaded(settings.destiny2CoreSettings)); }; } ================================================ FILE: src/app/manifest/d1-manifest-service.ts ================================================ import { handleErrors } from 'app/bungie-api/bungie-service-helper'; import { HttpStatusError, toHttpStatusError } from 'app/bungie-api/http-client'; import { AllD1DestinyManifestComponents } from 'app/destiny1/d1-manifest-types'; import { settingsSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { loadingEnd, loadingStart } from 'app/shell/actions'; import { del, get, set } from 'app/storage/idb-keyval'; import { ThunkResult } from 'app/store/types'; import { DimError } from 'app/utils/dim-error'; import { convertToError } from 'app/utils/errors'; import { errorLog, infoLog } from 'app/utils/log'; import { dedupePromise } from 'app/utils/promises'; import { reportException } from 'app/utils/sentry'; import { showNotification } from '../notifications/notifications'; import { settingsReady } from '../settings/settings'; const TAG = 'manifest'; // This file exports D1ManifestService at the bottom of the // file (TS wants us to declare classes before using them)! // Testing flags const alwaysLoadRemote = false; const manifestLangs = new Set(['en', 'fr', 'es', 'de', 'it', 'ja', 'pt-br']); const localStorageKey = 'd1-manifest-version'; const idbKey = 'd1-manifest'; let version: string | null = null; const getManifestAction: ThunkResult = dedupePromise((dispatch) => dispatch(doGetManifest()), ); export function getManifest(): ThunkResult { return getManifestAction; } function doGetManifest(): ThunkResult { return async (dispatch) => { dispatch(loadingStart(t('Manifest.Load'))); try { const manifest = await dispatch(loadManifest()); if (!manifest.DestinyVendorDefinition) { throw new Error('Manifest corrupted, please reload'); } return manifest; } catch (err) { let e = convertToError(err); if (e instanceof DimError && e.cause) { e = e.cause; } if (e.cause instanceof TypeError || e.cause instanceof HttpStatusError) { } else { // Something may be wrong with the manifest deleteManifestFile(); } errorLog(TAG, 'Manifest loading error', e); reportException('manifest load', e); throw new DimError('Manifest.Error', t('Manifest.Error', { error: e.message })).withError(e); } finally { dispatch(loadingEnd(t('Manifest.Load'))); } }; } function loadManifest(): ThunkResult { return async (dispatch, getState) => { await settingsReady; // wait for settings to be ready const language = settingsSelector(getState()).language; const manifestLang = manifestLangs.has(language) ? language : 'en'; const path = `/data/d1/manifests/d1-manifest-${manifestLang}.json?v=2021-12-05`; // Use the path as the version version = path; try { return await loadManifestFromCache(version); } catch { return dispatch(loadManifestRemote(version, path)); } }; } /** * Returns a promise for the manifest data as a Uint8Array. Will cache it on success. */ function loadManifestRemote( version: string, path: string, ): ThunkResult { return async (dispatch) => { dispatch(loadingStart(t('Manifest.Download'))); try { const response = await fetch(path); const manifest = await (response.ok ? (response.json() as Promise) : Promise.reject(await toHttpStatusError(response))); // We intentionally don't wait on this promise saveManifestToIndexedDB(manifest, version); return manifest; } catch (e) { handleErrors(e); // throws } finally { dispatch(loadingEnd(t('Manifest.Download'))); } }; } async function saveManifestToIndexedDB(typedArray: unknown, version: string) { try { await set(idbKey, typedArray); infoLog(TAG, `Successfully stored manifest file.`); localStorage.setItem(localStorageKey, version); } catch (e) { errorLog(TAG, 'Error saving manifest file', e); showNotification({ title: t('Help.NoStorage'), body: t('Help.NoStorageMessage'), type: 'error', }); } } function deleteManifestFile() { localStorage.removeItem(localStorageKey); del(idbKey); } /** * Returns a promise for the cached manifest of the specified * version as a Uint8Array, or rejects. */ async function loadManifestFromCache(version: string): Promise { if (alwaysLoadRemote) { throw new Error('Testing - always load remote'); } const currentManifestVersion = localStorage.getItem(localStorageKey); if (currentManifestVersion === version) { const manifest = await get(idbKey); if (!manifest) { throw new Error('Empty cached manifest file'); } return manifest; } else { throw new Error(`version mismatch: ${version} ${currentManifestVersion}`); } } ================================================ FILE: src/app/manifest/manifest-service-json.ts ================================================ import { handleErrors } from 'app/bungie-api/bungie-service-helper'; import { HttpStatusError, toHttpStatusError } from 'app/bungie-api/http-client'; import { settingsSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { loadingEnd, loadingStart } from 'app/shell/actions'; import { del, get, keys, set } from 'app/storage/idb-keyval'; import { ThunkResult } from 'app/store/types'; import { DimError } from 'app/utils/dim-error'; import { emptyArray, emptyObject } from 'app/utils/empty'; import { convertToError } from 'app/utils/errors'; import { errorLog, infoLog, timer } from 'app/utils/log'; import { dedupePromise } from 'app/utils/promises'; import { LookupTable } from 'app/utils/util-types'; import { AllDestinyManifestComponents, DestinyCollectibleDefinition, DestinyInventoryItemDefinition, DestinyItemActionBlockDefinition, DestinyItemTalentGridBlockDefinition, DestinyItemTranslationBlockDefinition, DestinyManifestComponentName, DestinyObjectiveDefinition, DestinyRecordDefinition, } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { once } from 'es-toolkit'; import { deepEqual } from 'fast-equals'; import { Draft } from 'immer'; import { getManifest as d2GetManifest } from '../bungie-api/destiny2-api'; import { showNotification } from '../notifications/notifications'; import { settingsReady } from '../settings/settings'; import { reportException } from '../utils/sentry'; const TAG = 'manifest'; // This file exports D2ManifestService at the bottom of the // file (TS wants us to declare classes before using them)! // TODO: replace this with a redux action! // Testing flags const alwaysLoadRemote = false; /** Functions that can reduce the size of a table after it's downloaded but before it's saved to cache. */ const tableTrimmers: LookupTable any> = { DestinyInventoryItemDefinition: (table: { [hash: number]: DestinyInventoryItemDefinition }) => { for (const key in table) { const def = table[key] as Draft; // Deleting properties can actually make memory usage go up as V8 replaces some efficient // structures from JSON parsing. Only replace objects with empties, and always test with the // memory profiler. Don't assume that deleting something makes this smaller. def.action = emptyObject>(); def.backgroundColor = emptyObject(); def.translationBlock = emptyObject>(); if (def.equippingBlock?.displayStrings?.length) { def.equippingBlock.displayStrings = emptyArray(); } if (def.preview) { if (def.preview.derivedItemCategories?.length) { def.preview.derivedItemCategories = emptyArray(); } def.preview.screenStyle = ''; } if (def.inventory) { if (def.inventory.bucketTypeHash !== BucketHashes.Subclass) { // The only useful bit about talent grids is for subclass damage types def.talentGrid = emptyObject>(); } def.inventory.tierTypeName = ''; } if (def.sockets) { def.sockets.intrinsicSockets = emptyArray(); for (const socket of def.sockets.socketEntries) { if (socket.reusablePlugSetHash && socket.reusablePlugItems.length > 0) { socket.reusablePlugItems = emptyArray(); } } } // We never figured out anything to do with icon sequences on items if (def.displayProperties.iconSequences) { def.displayProperties.iconSequences = emptyArray(); } // We don't use these def.tooltipStyle = ''; def.itemTypeAndTierDisplayName = ''; } return table; }, DestinyObjectiveDefinition: (table: { [hash: number]: DestinyObjectiveDefinition }) => { for (const key in table) { const def = table[key] as Draft; def.stats = emptyObject(); def.perks = emptyObject(); // Believe it or not we don't use these def.displayProperties.description = ''; def.displayProperties.name = ''; } return table; }, DestinyCollectibleDefinition: (table: { [hash: number]: DestinyCollectibleDefinition }) => { for (const key in table) { const def = table[key] as Draft; def.acquisitionInfo = emptyObject(); def.stateInfo = emptyObject(); } return table; }, DestinyRecordDefinition: (table: { [hash: number]: DestinyRecordDefinition }) => { for (const key in table) { const def = table[key] as Draft; def.requirements = emptyObject(); def.expirationInfo = emptyObject(); } return table; }, }; // Module-local state const localStorageKey = 'd2-manifest-version'; const idbKey = 'd2-manifest'; let version: string | null = null; export async function checkForNewManifest() { const data = await d2GetManifest(); // If none of the paths (for any language) matches what we downloaded... return version && !Object.values(data.jsonWorldContentPaths).includes(version); } type TrimTableName = T extends `Destiny${infer U}Definition` ? U : never; type TableShortName = TrimTableName; const getManifestAction = once( (tableAllowList: TableShortName[]): ThunkResult => dedupePromise((dispatch) => dispatch(doGetManifest(tableAllowList))), ); export function getManifest( tableAllowList: TableShortName[], ): ThunkResult { return getManifestAction(tableAllowList); } function doGetManifest( tableAllowList: TableShortName[], ): ThunkResult { return async (dispatch) => { dispatch(loadingStart(t('Manifest.Load'))); const stopTimer = timer(TAG, 'Load manifest'); try { const manifest = await dispatch(loadManifest(tableAllowList)); if (!manifest.DestinyVendorDefinition) { throw new Error('Manifest corrupted, please reload'); } return manifest; } catch (err) { let e = convertToError(err); if (e instanceof DimError && e.cause) { e = e.cause; } if (e.cause instanceof TypeError || e.cause instanceof HttpStatusError) { } else { // Something may be wrong with the manifest deleteManifestFile(); } errorLog(TAG, 'Manifest loading error', e); reportException('manifest load', e); throw new DimError('Manifest.Error', t('Manifest.Error', { error: e.message })).withError(e); } finally { dispatch(loadingEnd(t('Manifest.Load'))); stopTimer(); } }; } function loadManifest(tableAllowList: TableShortName[]): ThunkResult { return async (dispatch, getState) => { let components: { [key: string]: string; }; try { const data = await d2GetManifest(); await settingsReady; // wait for settings to be ready const language = settingsSelector(getState()).language; const path = data.jsonWorldContentPaths[language] || data.jsonWorldContentPaths.en; components = data.jsonWorldComponentContentPaths[language] || data.jsonWorldComponentContentPaths.en; // Use the path as the version, rather than the "version" field, because // Bungie can update the manifest file without changing that version. version = `v2-${path}`; // the prefix is used to bust the cache if we change the table trimmers } catch (e) { // If we can't get info about the current manifest, try to just use whatever's already saved. version = localStorage.getItem(localStorageKey); if (version) { return loadManifestFromCache(version, tableAllowList); } else { throw e; } } try { return await loadManifestFromCache(version, tableAllowList); } catch (e) { infoLog(TAG, 'Unable to use cached manifest, loading fresh manifest from Bungie.net', e); return dispatch(loadManifestRemote(version, components, tableAllowList)); } }; } /** * Returns a promise for the manifest data as a Uint8Array. Will cache it on success. */ function loadManifestRemote( version: string, components: { [key: string]: string; }, tableAllowList: TableShortName[], ): ThunkResult { return async (dispatch) => { dispatch(loadingStart(t('Manifest.Download'))); try { const manifest = await downloadManifestComponents(components, tableAllowList); // We intentionally don't wait on this promise saveManifestToIndexedDB(manifest, version, tableAllowList); return manifest; } finally { dispatch(loadingEnd(t('Manifest.Download'))); } }; } export async function downloadManifestComponents( components: { [key: string]: string; }, tableAllowList: TableShortName[], ) { // Adding a cache buster to work around bad cached CloudFlare data: https://github.com/DestinyItemManager/DIM/issues/5101 // try canonical component URL which should likely be already cached, // then fall back to appending "?dim" then "?dim-[random numbers]", // in case cloudflare has inappropriately cached another domain's CORS headers or a 404 that's no longer a 404 const cacheBusterStrings = [ '', '?dim', `?dim-${Math.random().toString().split('.')[1] ?? 'dimCacheBust'}`, ]; const manifest: Partial = {}; // Load the manifest tables we want table-by-table, in parallel. This is // faster and downloads less data than the single huge file. const futures = tableAllowList .map((t) => `Destiny${t}Definition` as DestinyManifestComponentName) .map(async (table) => { let response: Response; let error: Error | undefined; let body = null; for (const query of cacheBusterStrings) { try { response = await fetch(`https://www.bungie.net${components[table]}${query}`); if (response.ok) { // Sometimes the file is found, but isn't parseable as JSON body = (await response.json()) as AllDestinyManifestComponents[DestinyManifestComponentName]; break; } error ??= await toHttpStatusError(response); } catch (e) { error ??= convertToError(e); } } if (!body) { handleErrors(error); // throws } // I couldn't figure out how to make these types work... // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment manifest[table] = table in tableTrimmers ? tableTrimmers[table]!(body) : body; }); await Promise.all(futures); return manifest as AllDestinyManifestComponents; } async function saveManifestToIndexedDB( manifest: AllDestinyManifestComponents, version: string, tableAllowList: TableShortName[], ) { try { await Promise.all([ ...tableAllowList.map(async (t) => { const records = manifest[`Destiny${t}Definition`]; if (records) { await set(`${idbKey}-${t}`, records); } }), del(idbKey), // the old storage location before per-table ]); infoLog(TAG, `Successfully stored manifest file.`); localStorage.setItem(localStorageKey, version); localStorage.setItem(`${localStorageKey}-whitelist`, JSON.stringify(tableAllowList)); } catch (e) { errorLog(TAG, 'Error saving manifest file', e); showNotification({ title: t('Help.NoStorage'), body: t('Help.NoStorageMessage'), type: 'error', }); } } async function deleteManifestFile() { localStorage.removeItem(localStorageKey); await Promise.all( (await keys()).map(async (key) => { if (typeof key === 'string' && key.startsWith(idbKey)) { await del(key); } }), ); } /** * Returns a promise for the cached manifest of the specified * version as a Uint8Array, or rejects. */ async function loadManifestFromCache( version: string, tableAllowList: TableShortName[], ): Promise { if (alwaysLoadRemote) { throw new Error('Testing - always load remote'); } const currentManifestVersion = localStorage.getItem(localStorageKey); const currentAllowList = JSON.parse( localStorage.getItem(`${localStorageKey}-whitelist`) || '[]', ) as string[]; if (currentManifestVersion === version && deepEqual(currentAllowList, tableAllowList)) { const manifest = {} as AllDestinyManifestComponents; await Promise.all( tableAllowList.map(async (t) => { const records = await get>(`${idbKey}-${t}`); const tableName = `Destiny${t}Definition` as DestinyManifestComponentName; if (!records) { throw new Error(`No cached contents for table ${tableName}`); } manifest[tableName] = records; }), ); return manifest; } else { // Delete the existing manifest first, to make space await deleteManifestFile(); throw new Error(`version mismatch: ${version} ${currentManifestVersion}`); } } ================================================ FILE: src/app/manifest/reducer.ts ================================================ import { unadvertisedResettableVendors } from 'app/search/d2-known-values'; import { Destiny2CoreSettings } from 'bungie-api-ts/core'; import { Reducer } from 'redux'; import { ActionType, getType } from 'typesafe-actions'; import type { AccountsAction } from '../accounts/reducer'; import { D1ManifestDefinitions } from '../destiny1/d1-definitions'; import { D2ManifestDefinitions } from '../destiny2/d2-definitions'; import * as actions from './actions'; export interface ManifestState { d1Manifest?: D1ManifestDefinitions; d2Manifest?: D2ManifestDefinitions; /** * Bungie.net core Destiny settings. * We load these remotely, and they're in the "manifest" state because I mostly think they * should have been included in the manifest. */ destiny2CoreSettings?: Destiny2CoreSettings; } export type ManifestAction = ActionType; const initialState: ManifestState = {}; export const manifest: Reducer = ( state: ManifestState = initialState, action: ManifestAction | AccountsAction, ): ManifestState => { switch (action.type) { case getType(actions.setD1Manifest): { return { ...state, d1Manifest: action.payload, }; } case getType(actions.setD2Manifest): { return { ...state, d2Manifest: action.payload, }; } case getType(actions.coreSettingsLoaded): { // This issue isn't wrong. https://github.com/Bungie-net/api/issues/1917 // Unless it's ever addressed, we can add-in 'missing' non-seasonal, resettable vendors. // This mutates a settings array, but it's a single-purpose piece of data and refreshed by a new settings poll. if (action.payload.currentRankProgressionHashes?.length) { for (const h of unadvertisedResettableVendors) { if (!action.payload.currentRankProgressionHashes.includes(h)) { action.payload.currentRankProgressionHashes.push(h); } } } return { ...state, destiny2CoreSettings: action.payload, }; } default: return state; } }; ================================================ FILE: src/app/manifest/selectors.ts ================================================ import { destinyVersionSelector } from 'app/accounts/selectors'; import { RootState } from 'app/store/types'; import { emptyArray } from 'app/utils/empty'; import { useSelector } from 'react-redux'; export const destiny2CoreSettingsSelector = (state: RootState) => state.manifest.destiny2CoreSettings; export const rankProgressionHashesSelector = (state: RootState): number[] => state.manifest.destiny2CoreSettings?.currentRankProgressionHashes ?? emptyArray(); export const currentSeasonPassHashSelector = (state: RootState): number | undefined => state.manifest.destiny2CoreSettings?.currentSeasonPassHash; const d1ManifestSelector = (state: RootState) => state.manifest.d1Manifest; export const d2ManifestSelector = (state: RootState) => state.manifest.d2Manifest; export const manifestSelector = (state: RootState) => destinyVersionSelector(state) === 2 ? d2ManifestSelector(state) : d1ManifestSelector(state); export function useD1Definitions() { return useSelector(d1ManifestSelector); } export function useD2Definitions() { return useSelector(d2ManifestSelector); } export function useDefinitions() { return useSelector(manifestSelector); } ================================================ FILE: src/app/material-counts/MaterialCounts.m.scss ================================================ .materialCounts { display: grid; margin: auto; width: fit-content; grid-template-columns: repeat(3, max-content); gap: 2px 6px; align-items: center; &.wide { grid-template-columns: repeat(6, max-content); } img { height: 20px; width: 20px; } } .material { display: contents; & > :first-child { margin-left: 10px; } & > :last-child { margin-right: 10px; } } .amount { text-align: right; font-weight: bold; font-variant-numeric: tabular-nums; } .spanGrid { grid-column: 1/-1; hr { margin: 3px; } } ================================================ FILE: src/app/material-counts/MaterialCounts.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'amount': string; 'material': string; 'materialCounts': string; 'spanGrid': string; 'wide': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/material-counts/MaterialCounts.tsx ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import BungieImage from 'app/dim-ui/BungieImage'; import { currenciesSelector, materialsSelector, transmogCurrenciesSelector, upgradeCurrenciesSelector, } from 'app/inventory/selectors'; import { AccountCurrency } from 'app/inventory/store-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { compact, filterMap } from 'app/utils/collections'; import { chainComparator, compareBy } from 'app/utils/comparators'; import { addDividers } from 'app/utils/react'; import clsx from 'clsx'; import glimmerMats from 'data/d2/spider-mats.json'; import { useSelector } from 'react-redux'; import * as styles from './MaterialCounts.m.scss'; const upgradeMats = [ 4257549984, // Enhancement Prism 3853748946, // Enhancement Core 2718300701, // Unstable Cores 4257549985, // Ascendant Shard 353704689, // Ascendant Alloy 3467984096, // Exotic Cipher 2228452164, // Deepsight Harmonizer ]; // Deprecated or otherwise uninteresting materials // TODO: Generate this in d2ai based on items that say "This item serves no purpose and can be safely dismantled." const hiddenMats = [ 529424730, // Upgrade Points 1624697519, // Engram Tracker 592227263, // Baryon Bough 950899352, // Dusklight Shard 1485756901, // Glacial Starwort 3592324052, // Helium Filaments 4046539562, // Mod Components 4114204995, // Ghost Fragments 1289622079, // Strand Meditations 2512446424, // Nonary Manifold 443031983, // Phantasmal Core ]; // Synthcord is a material, Synthweave is a currency const transmogMats = [ 3855200273, // InventoryItem "Rigid Synthcord" 3552107018, // InventoryItem "Plush Synthcord" 3107195131, // InventoryItem "Sleek Synthcord" ]; export function MaterialCounts({ wide, includeCurrencies, }: { wide?: boolean; includeCurrencies?: boolean; }) { const defs = useD2Definitions()!; const allMats = useSelector(materialsSelector); const materials = Map.groupBy(allMats, (m) => m.hash); for (const h of hiddenMats) { materials.delete(h); } const currencies = useSelector(currenciesSelector); let transmogCurrencies = useSelector(transmogCurrenciesSelector); const upgradeCurrencies = useSelector(upgradeCurrenciesSelector); // TODO: This bucket hash doesn't have a name in the manifest, so I'm not sure if it's "Seasonal" or "Kepler". const seasonalMats = allMats.filter((m) => m.bucket.hash === 2207872501).map((m) => m.hash); // Track materials which have already appeared, in case these categories overlap const shownMats = new Set(); const matsToCurrencies = (matgroup: number[]) => filterMap(matgroup, (h): AccountCurrency | undefined => { const items = materials.get(h); if (!items || shownMats.has(h)) { return undefined; } shownMats.add(h); const amount = items.reduce((total, i) => total + i.amount, 0); if (amount === undefined) { return undefined; } const item = items[0]; return { itemHash: item.hash, displayProperties: { icon: item.icon, name: item.name, description: item.description, hasIcon: Boolean(item.icon), iconSequences: [], highResIcon: '', iconHash: 0, }, quantity: amount, }; }); const [ seasonalMatsAsCurrencies, upgradeMatsAsCurrencies, glimmerMatsAsCurrencies, transmogMatsAsCurrencies, remainingMatsAsCurrencies, ]: AccountCurrency[][] = [ seasonalMats, upgradeMats, [ ...glimmerMats, 2979281381, // Upgrade Module (deprecated in edge of fate and turned into a source of glimmer/enhancement cores) ], transmogMats, [...materials.keys()], ].map(matsToCurrencies); upgradeMatsAsCurrencies.push(...upgradeCurrencies); transmogCurrencies = [...transmogCurrencies, ...transmogMatsAsCurrencies]; const content = [ ...[ includeCurrencies ? currencies : [], seasonalMatsAsCurrencies, upgradeMatsAsCurrencies, glimmerMatsAsCurrencies, remainingMatsAsCurrencies, transmogCurrencies, ].map( (currencies) => currencies.length > 0 && ( ), ), ]; return (
{addDividers( compact(content),
, )}
); } function CurrencyGroup({ currencies, defs, }: { currencies: AccountCurrency[]; defs: D2ManifestDefinitions; }) { return currencies .toSorted( chainComparator( compareBy(({ itemHash }) => defs.InventoryItem.get(itemHash)?.inventory?.tierType ?? 0), compareBy(({ displayProperties }) => displayProperties.name), ), ) .map((currency) => (
{currency.quantity.toLocaleString()} {currency.displayProperties.name}
)); } ================================================ FILE: src/app/material-counts/MaterialCountsWrappers.m.scss ================================================ .container { margin: 16px 10px; } ================================================ FILE: src/app/material-counts/MaterialCountsWrappers.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'container': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/material-counts/MaterialCountsWrappers.tsx ================================================ import { Tooltip } from 'app/dim-ui/PressTip'; import { t } from 'app/i18next-t'; import { Observable } from 'app/utils/observable'; import { useSubscription } from 'use-subscription'; import Sheet from '../dim-ui/Sheet'; import { MaterialCounts } from './MaterialCounts'; import * as styles from './MaterialCountsWrappers.m.scss'; /** * The currently selected store for showing gear power. */ const doShowMaterialCounts$ = new Observable(false); /** * Show the gear power sheet */ export function showMaterialCount() { doShowMaterialCounts$.next(true); } export function MaterialCountsSheet() { const isShown = useSubscription(doShowMaterialCounts$); if (!isShown) { return null; } const close = () => { doShowMaterialCounts$.next(false); }; return ( {t('Header.MaterialCounts')}}>
); } export function MaterialCountsTooltip() { return ( <> ); } ================================================ FILE: src/app/notifications/Notification.m.scss ================================================ @use 'sass:color'; @use '../variables.scss' as *; .notification { margin: 0 0 8px; width: 350px; box-shadow: 0 -1px 24px 4px #222; @include phone-portrait { width: 100%; } } .inner { color: var(--theme-text); box-sizing: border-box; border-top: 4px solid black; pointer-events: all; user-select: none; transition: transform 150ms ease-in-out; @include interactive($hover: true) { transform: scale(1.02); } } .contents { display: flex; flex-direction: row; padding: 8px; } .icon { flex-shrink: 0; margin-right: 8px; } .trailer { margin-left: 8px; flex-shrink: 0; display: flex; } .timer { height: 1px; background-color: rgb(255, 255, 255, 0.5); } .details { flex: 1; display: flex; flex-direction: column; > div { white-space: pre-wrap; } } .title { font-weight: bold; margin-bottom: 4px; } .info, .progress { border-top-color: #2f96b4; background-color: color.scale(#2f96b4, $lightness: -90%); } .error { border-top-color: $red; background-color: color.scale($red, $lightness: -90%); } .warning { border-top-color: #f89406; background-color: color.scale(#f89406, $lightness: -90%); } .success { border-top-color: $green; background-color: color.scale($green, $lightness: -90%); } ================================================ FILE: src/app/notifications/Notification.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'contents': string; 'details': string; 'error': string; 'icon': string; 'info': string; 'inner': string; 'notification': string; 'progress': string; 'success': string; 'timer': string; 'title': string; 'trailer': string; 'warning': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/notifications/Notification.tsx ================================================ import { t } from 'app/i18next-t'; import { CanceledError } from 'app/utils/cancel'; import { convertToError } from 'app/utils/errors'; import clsx from 'clsx'; import { motion, MotionProps, Transition } from 'motion/react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import * as styles from './Notification.m.scss'; import NotificationButton from './NotificationButton'; import { NotificationError, NotificationType, Notify } from './notifications'; const typeStyles: { [type in NotificationType]: string } = { success: styles.success, error: styles.error, progress: styles.progress, warning: styles.warning, info: styles.info, }; const showErrorDuration = 5000; interface Props extends MotionProps { notification: Notify; onClose: (notification: Notify) => void; } export default function Notification({ notification, onClose, ...animation }: Props) { const [hovering, setHovering] = useState(false); const [success, setSuccess] = useState(); const [error, setError] = useState(); const timer = useRef(0); const setupTimer = useCallback(() => { if (timer.current) { window.clearTimeout(timer.current); timer.current = 0; } if (!error && !success && notification.promise) { notification.promise .then(() => setSuccess(true)) .catch((e) => e instanceof CanceledError ? setSuccess(true) : setError(convertToError(e)), ); } else if (notification.duration || error) { timer.current = window.setTimeout( () => { if (!hovering) { onClose(notification); } }, error ? Math.max(notification.duration, showErrorDuration) : notification.duration, ); } else { window.setTimeout(() => onClose(notification), 0); } }, [error, success, notification, hovering, onClose]); const clearTimer = () => { if (timer.current) { window.clearTimeout(timer.current); timer.current = 0; } }; useEffect(() => { setupTimer(); return clearTimer; }, [setupTimer]); const onClick = (event: React.MouseEvent) => { if (notification.onClick?.(event) !== false) { onClose(notification); } }; const hover = () => { clearTimer(); setHovering(true); }; const stopHover = () => { setHovering(false); setupTimer(); }; const progressTarget = hovering || Boolean(!error && !success && notification.promise) ? '0%' : '100%'; const transition: Transition = hovering ? { type: 'tween', ease: 'easeOut', duration: 0.3, } : { type: 'tween', ease: 'linear', duration: (error ? showErrorDuration : notification.duration) / 1000 - 0.3, }; // A NotificationError can override a lot of properties const title = error?.title || notification.title; const body = error?.body || error?.message || notification.body; const icon = error?.icon || notification.icon; const trailer = error?.trailer || notification.trailer; return (
{Boolean(icon) &&
{icon}
}
{title}
{Boolean(body) &&
{body}
} {!error && notification.onCancel && ( {success || error ? t('Notification.OK') : t('Notification.Cancel')} )}
{Boolean(trailer) &&
{trailer}
}
{(success || error || !notification.promise) && typeof notification.duration === 'number' && ( )}
); } ================================================ FILE: src/app/notifications/NotificationButton.m.scss ================================================ @use '../variables.scss' as *; .button { margin: 10px auto 0; padding: 6px; border-radius: 4px; text-align: center; color: var(--theme-text); background-color: #222; display: block; width: 30%; cursor: pointer; @include interactive($hover: true) { background-color: var(--theme-accent-secondary); color: #222; } & > svg { margin-right: 0.5em; } } ================================================ FILE: src/app/notifications/NotificationButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'button': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/notifications/NotificationButton.tsx ================================================ import React from 'react'; import * as styles from './NotificationButton.m.scss'; /** * an independent element fed into showNotification({body: * attach your own functionality to its onClick when creating it. * jsx children are the button's label */ export default function NotificationButton({ children, onClick, }: { children: React.ReactNode; onClick: (e: React.MouseEvent) => void; }) { return ( {children} ); } ================================================ FILE: src/app/notifications/NotificationsContainer.m.scss ================================================ @use '../variables.scss' as *; .container { position: fixed; right: 0; z-index: $tempContainerZindex + 1; padding: 8px; box-sizing: border-box; width: 100%; backface-visibility: hidden; pointer-events: none; display: flex; flex-direction: column; align-items: flex-end; @include below-header; @include phone-portrait { bottom: 0; flex-direction: column-reverse; padding-bottom: calc(44px + env(safe-area-inset-bottom)); @media all and (display-mode: standalone) { padding-bottom: env(safe-area-inset-bottom); } } } ================================================ FILE: src/app/notifications/NotificationsContainer.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'container': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/notifications/NotificationsContainer.tsx ================================================ import { useEventBusListener } from 'app/utils/hooks'; import { AnimatePresence, Transition, Variants } from 'motion/react'; import { useCallback, useState } from 'react'; import Notification from './Notification'; import * as styles from './NotificationsContainer.m.scss'; import { Notify, notifications$ } from './notifications'; const spring: Transition = { type: 'spring', bounce: 0, duration: 0.3 }; const animateVariants: Variants = { hidden: { opacity: 0, height: 0 }, shown: { height: 'auto', opacity: 1 }, }; /** This is the root element that displays popup notifications. */ export default function NotificationsContainer() { const [notifications, setNotifications] = useState([]); useEventBusListener( notifications$, useCallback((notification: Notify) => { setNotifications((notifications) => [...notifications, notification]); }, []), ); const onNotificationClosed = (notification: Notify) => setNotifications((notifications) => notifications.filter((n) => n !== notification)); return (
{notifications.map((item) => ( ))}
); } ================================================ FILE: src/app/notifications/notifications.ts ================================================ import { EventBus } from 'app/utils/observable'; import React from 'react'; export type NotificationType = 'success' | 'info' | 'warning' | 'error' | 'progress'; export interface NotifyInput { title: string; body?: React.ReactNode; type?: NotificationType; /** Some content to show to the left of the notification */ icon?: React.ReactNode; /** Some content to show to the right of the notification */ trailer?: React.ReactNode; /** The notification will stay up while the promise is not complete, and for a duration afterwards. Throw NotificationError to customize the error screen. */ promise?: Promise; /** The notification will show for the given number of milliseconds. */ duration?: number; /** Return false to not close the notification on click. */ onClick?: (event: React.MouseEvent) => boolean | void; onCancel?: () => void; } export interface Notify { id: number; type: NotificationType; title: string; body?: React.ReactNode; icon?: React.ReactNode; trailer?: React.ReactNode; promise?: Promise; /** The notification will show for either the given number of milliseconds, or when the provided promise completes. */ duration: number; onClick?: (event: React.MouseEvent) => boolean | void; onCancel?: () => void; } /** * An error that allows setting the properties of the notification. Throw this from your promise * to transform the notification into an error. */ export class NotificationError extends Error { title?: string; body?: React.ReactNode; type?: NotificationType; icon?: React.ReactNode; trailer?: React.ReactNode; constructor( message: string, { title, body, type, icon, trailer, }: { title?: string; body?: React.ReactNode; type?: NotificationType; icon?: React.ReactNode; trailer?: React.ReactNode; }, ) { super(message); this.name = 'NotificationError'; this.title = title; this.body = body || message; this.type = type || 'error'; this.icon = icon; this.trailer = trailer; } } export const notifications$ = new EventBus(); let notificationId = 0; export function showNotification(notification: NotifyInput) { notifications$.next({ id: notificationId++, duration: 5000, type: 'info', ...notification, }); } ================================================ FILE: src/app/organizer/Columns.m.scss ================================================ @use '../variables' as *; @use './ItemTable.m.scss' as table; .noWrap { white-space: nowrap; } .name { composes: noWrap; font-weight: bold; } .new { composes: flexColumn from '../dim-ui/common.m.scss'; align-items: center; > div { position: static !important; } } .stats { text-align: right; > div { justify-content: center; } // Recoil background semi-circle svg { circle { fill: black; } } } .statsHeader { text-align: center; > div { display: flex; flex-direction: row; align-items: center; > div { display: flex; flex-direction: column; align-items: center; } } } .centered { text-align: center; justify-content: center; } .dmg { composes: centered; padding-top: calc(var(--item-size) * 0.75 * 0.5 - 2px); } .dmgHeader { composes: centered; padding-top: 4px; } .icon { --icon-item-size: calc(var(--item-size) * 0.75); composes: centered; padding-top: 4px; min-width: calc(var(--item-size) * 0.75); left: calc(30px + env(safe-area-inset-left)); position: sticky; top: calc(var(--header-height) + var(--table-header-height) + var(--item-table-toolbar-height)); z-index: table.$header-cells; background: var(--org-row-bg); > :global(.item) { --item-size: var(--icon-item-size) !important; height: var(--item-size); @include interactive($hover: true) { outline: 1px solid var(--theme-item-polaroid-hover-border); cursor: pointer; } } } .iconHeader { composes: centered; left: calc(30px + env(safe-area-inset-left)); position: sticky; top: calc(var(--header-height) + var(--item-table-toolbar-height)); } .inlineIcon { height: 14px; width: 14px; margin-right: 0; } .positive { color: $xp; } .negative { color: #d14334; } .modPerks { composes: flexColumn from '../dim-ui/common.m.scss'; break-inside: avoid; gap: 4px; padding-left: 3px; } .perkLike { padding-top: calc(var(--item-size) * 0.5 * 0.5 - 5px); > *:nth-last-child(n + 2) { margin-bottom: 4px; } .modPerks { flex: 0; min-width: 160px; flex-basis: 160px; padding-left: 3px; } } .perks { composes: perkLike; columns: 2; height: 100%; box-sizing: border-box; column-gap: 8px; } .isPerk { margin-left: 0; padding-left: 1px; box-shadow: -5px 0 0 -3px var(--org-row-bg), -5px 0 0 -1px #888; } // Perks in the perk/mod columns .modPerk { composes: flexRow from '../dim-ui/common.m.scss'; composes: noWrap; align-items: flex-start; gap: 2px; } .miniPerkContainer { --item-size: 18px; position: relative; contain: layout paint style; box-sizing: border-box; height: 18px; width: 18px; margin-left: -1px; flex-shrink: 0; > img { height: var(--item-size); width: var(--item-size); } } .perkSelected { font-weight: bold; } .perkSelectable { cursor: pointer; } // copy pasted from src/app/item-popup/PlugTooltip.m.scss with spacing tweaks .enhancedArrow { width: 25px; display: flex; &::after { content: ''; display: inline-block; width: 6px; height: 11px; vertical-align: text-bottom; mask-image: url('images/enhancedArrow.svg'); background-color: $enhancedYellow; margin-left: 1px; margin-top: 3px; } } .perksGrid { --mod-size: calc(#{dim-item-px(28)}); --perk-size: calc(#{dim-item-px(28)}); padding-top: 4px; padding-bottom: 4px; @include phone-portrait { --mod-size: 26px; } } // D1 talent grids are like sockets .talentGrid { padding: 4px 2px 0 2px; max-width: 150px; height: auto; } .locationCell { composes: flexRow from '../dim-ui/common.m.scss'; composes: noWrap; align-items: flex-start; img { margin-right: 4px; height: 16px; width: 16px; } } $modslotSize: 24px; .modslotIcon { height: $modslotSize; width: $modslotSize; } .modslot { composes: centered; padding-top: calc(4px + (var(--item-size) * 0.75 - #{$modslotSize}) / 2); } .loadouts { flex-direction: column; gap: 4px; } .loadout { composes: noWrap; img { height: 16px; width: 16px; margin-right: 4px; vertical-align: middle; } } .shapedIconOverlay { position: absolute; bottom: 1px; left: 1px; width: calc(var(--item-size) - ($item-border-width * 2)); } ================================================ FILE: src/app/organizer/Columns.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'centered': string; 'dmg': string; 'dmgHeader': string; 'enhancedArrow': string; 'hasFilter': string; 'header': string; 'headerRow': string; 'icon': string; 'iconHeader': string; 'importButton': string; 'inlineIcon': string; 'isPerk': string; 'loadout': string; 'loadouts': string; 'locationCell': string; 'miniPerkContainer': string; 'modPerk': string; 'modPerks': string; 'modslot': string; 'modslotIcon': string; 'name': string; 'negative': string; 'new': string; 'noItems': string; 'noWrap': string; 'perkLike': string; 'perkSelectable': string; 'perkSelected': string; 'perks': string; 'perksGrid': string; 'positive': string; 'row': string; 'selection': string; 'shapedIconOverlay': string; 'shiftHeld': string; 'sorter': string; 'stats': string; 'statsHeader': string; 'table': string; 'talentGrid': string; 'toolbar': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/organizer/Columns.tsx ================================================ import { CustomStatDef, DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { StoreIcon } from 'app/character-tile/StoreIcon'; import BungieImage from 'app/dim-ui/BungieImage'; import ElementIcon from 'app/dim-ui/ElementIcon'; import { PressTip, Tooltip } from 'app/dim-ui/PressTip'; import { SpecialtyModSlotIcon } from 'app/dim-ui/SpecialtyModSlotIcon'; import { I18nKey, t, tl } from 'app/i18next-t'; import ItemIcon, { DefItemIcon } from 'app/inventory/ItemIcon'; import ItemPopupTrigger from 'app/inventory/ItemPopupTrigger'; import NewItemIndicator from 'app/inventory/NewItemIndicator'; import TagIcon from 'app/inventory/TagIcon'; import { TagValue, tagConfig } from 'app/inventory/dim-item-info'; import { D1Item, DimItem, DimSocket, DimStat } from 'app/inventory/item-types'; import { storesSelector } from 'app/inventory/selectors'; import { isHarmonizable } from 'app/inventory/store/deepsight'; import { getEvent, getSeason } from 'app/inventory/store/season'; import { getStatSortOrder } from 'app/inventory/store/stats'; import { getStore } from 'app/inventory/stores-helpers'; import { KillTrackerInfo } from 'app/item-popup/KillTracker'; import NotesArea from 'app/item-popup/NotesArea'; import { DimPlugTooltip } from 'app/item-popup/PlugTooltip'; import { recoilValue } from 'app/item-popup/RecoilStat'; import { editLoadout } from 'app/loadout-drawer/loadout-events'; import InGameLoadoutIcon from 'app/loadout/ingame/InGameLoadoutIcon'; import { InGameLoadout, Loadout, isInGameLoadout } from 'app/loadout/loadout-types'; import { LoadoutsByItem } from 'app/loadout/selectors'; import { TOTAL_STAT_HASH, breakerTypeNames, realD2ArmorStatSearchByHash, } from 'app/search/d2-known-values'; import D2Sources from 'app/search/items/search-filters/d2-sources'; import { quoteFilterString } from 'app/search/query-parser'; import { statHashByName } from 'app/search/search-filter-values'; import { getD1QualityColor, percent } from 'app/shell/formatters'; import { AppIcon, faCheck, lockIcon, powerIndicatorIcon, thumbsDownIcon, thumbsUpIcon, } from 'app/shell/icons'; import { RootState } from 'app/store/types'; import { compact, filterMap, invert } from 'app/utils/collections'; import { Comparator, compareBy, primitiveComparator } from 'app/utils/comparators'; import { getArmor3StatFocus, getArmor3TuningStat, getItemDamageShortName, getItemKillTrackerInfo, getItemYear, getMasterworkStatNames, getSpecialtySocketMetadata, isArmor3, isArtifice, isArtificeSocket, isD1Item, } from 'app/utils/item-utils'; import { getArmorArchetype, getArmorArchetypeSocket, getExtraIntrinsicPerkSockets, getIntrinsicArmorPerkSocket, getSocketsByType, getWeaponArchetype, getWeaponArchetypeSocket, isEnhancedPerk, } from 'app/utils/socket-utils'; import { LookupTable } from 'app/utils/util-types'; import { InventoryWishListRoll } from 'app/wishlists/wishlists'; import clsx from 'clsx'; import { D2EventInfo } from 'data/d2/d2-event-info-v2'; import { BreakerTypeHashes, BucketHashes, StatHashes } from 'data/d2/generated-enums'; import shapedOverlay from 'images/shapedOverlay.png'; import React from 'react'; import { useSelector } from 'react-redux'; import { createCustomStatColumns } from './CustomStatColumns'; import CompareStat from 'app/compare/CompareStat'; import { buildNodeNames, buildSocketNames, csvStatNamesForDestinyVersion, } from 'app/inventory/spreadsheets'; import { PlugClickedHandler } from 'app/inventory/store/override-sockets'; import { AmmoIcon } from 'app/item-popup/AmmoIcon'; import { DeepsightHarmonizerIcon, HarmonizerIcon } from 'app/item-popup/DeepsightHarmonizerIcon'; import ItemSockets from 'app/item-popup/ItemSockets'; import { ItemModSockets } from 'app/item-popup/ItemSocketsWeapons'; import ItemTalentGrid from 'app/item-popup/ItemTalentGrid'; import { ammoTypeFilter } from 'app/search/items/search-filters/known-values'; import { emptyArray } from 'app/utils/empty'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import * as styles from './Columns.m.scss'; import { ColumnDefinition, ColumnGroup, ColumnWithStat, SortDirection, Value } from './table-types'; /** * Get the ID used to select whether this column is shown or not. */ export function getColumnSelectionId(column: ColumnDefinition) { return column.columnGroup ? column.columnGroup.id : column.id; } // Some stat labels are long. This lets us replace them with i18n export const statLabels: LookupTable = { [StatHashes.RoundsPerMinute]: tl('Organizer.Stats.RPM'), [StatHashes.ReloadSpeed]: tl('Organizer.Stats.Reload'), [StatHashes.AimAssistance]: tl('Organizer.Stats.Aim'), [StatHashes.RecoilDirection]: tl('Organizer.Stats.Recoil'), [StatHashes.Attack]: tl('Organizer.Stats.Power'), [StatHashes.Defense]: tl('Organizer.Stats.Power'), [StatHashes.AirborneEffectiveness]: tl('Organizer.Stats.Airborne'), [StatHashes.AmmoGeneration]: tl('Organizer.Stats.AmmoGeneration'), }; export const perkStringSort: Comparator = (a, b) => { const aParts = (a ?? '').split(','); const bParts = (b ?? '').split(','); let ai = 0; let bi = 0; while (ai < aParts.length && bi < bParts.length) { const aPart = aParts[ai]; const bPart = bParts[bi]; if (aPart === bPart) { ai++; bi++; continue; } // Disrupt normal alphabetization by making empty string (no interesting information) sort to last. // (the "both are blank" condition is already ruled out above) return !aPart ? 1 : !bPart ? -1 : (aPart.localeCompare(bPart) as 1 | 0 | -1); } return 0; }; const perkStringFilter = (value: string | undefined) => { if (!value) { return undefined; } return value .split(',') .map((perk) => `exactperk:"${perk}"`) .join(' '); }; /** * This helper allows TypeScript to perform type inference to determine the * type of V based on its arguments. This allows us to automatically type the * various column methods like `cell` and `filter` automatically based on the * return type of `value`. */ /*@__INLINE__*/ function c(columnDef: ColumnDefinition): ColumnDefinition { return columnDef; } export const d1QualityColumn = c({ id: 'quality', header: t('Organizer.Columns.Quality'), csv: '% Quality', value: (item) => (isD1Item(item) && item.quality ? item.quality.min : 0), cell: (value) => {value}%, filter: (value) => `quality:>=${value}`, }); export function modsColumn( className: string, headerClassName: string, isWeapon: boolean, onPlugClicked?: PlugClickedHandler, ) { return c({ id: 'mods', className, headerClassName, header: t('Organizer.Columns.Mods'), // TODO: for ghosts this should return ghost mods, not cosmetics value: (item) => perkString(getSocketsByType(item, 'mods')), cell: (_val, item) => ( <> {isD1Item(item) && item.talentGrid && ( )} {item.sockets && (isWeapon ? ( ) : ( ))} ), sort: perkStringSort, }); } export function perksGridColumn( className: string, headerClassName: string, onPlugClicked: PlugClickedHandler | undefined, initialItemId?: string, headerKey: I18nKey = 'Organizer.Columns.Perks', ) { return c({ id: 'perksGrid', className, headerClassName, header: t(headerKey), value: (item) => perkString(getSocketsByType(item, 'perks')), cell: (_val, item) => ( <> {isD1Item(item) && item.talentGrid && ( )} {item.missingSockets && item.id === initialItemId && (
{item.missingSockets === 'missing' ? t('MovePopup.MissingSockets') : t('MovePopup.LoadingSockets')}
)} {item.sockets && } ), sort: perkStringSort, }); } /** * This function generates the columns. */ export function getColumns( useCase: 'organizer' | 'spreadsheet', itemsType: 'weapon' | 'armor' | 'ghost', /** A single example stat per stat hash among items */ stats: DimStat[], getTag: (item: DimItem) => TagValue | undefined, getNotes: (item: DimItem) => string | undefined, wishList: (item: DimItem) => InventoryWishListRoll | undefined, hasWishList: boolean, customStatDefs: CustomStatDef[], loadoutsByItem: LoadoutsByItem, newItems: Set, destinyVersion: DestinyVersion, onPlugClicked?: PlugClickedHandler, ): ColumnDefinition[] { const isGhost = itemsType === 'ghost'; const isArmor = itemsType === 'armor'; const isWeapon = itemsType === 'weapon'; const isSpreadsheet = useCase === 'spreadsheet'; const { statColumns, baseStatColumns, d1ArmorQualityByStat } = getStatColumns( stats, customStatDefs, destinyVersion, { isArmor, isSpreadsheet, className: styles.stats, headerClassName: styles.statsHeader, }, ); const customStats = isSpreadsheet ? [] : createCustomStatColumns(customStatDefs, { className: styles.stats, headerClassName: styles.statsHeader, }); const columns: ColumnDefinition[] = compact([ !isSpreadsheet && c({ id: 'icon', header: t('Organizer.Columns.Icon'), className: styles.icon, headerClassName: styles.iconHeader, value: (i) => i.icon, cell: (_val, item) => ( {(ref, onClick) => (
{item.crafted && }
)}
), noSort: true, noHide: true, }), c({ id: 'name', header: t('Organizer.Columns.Name'), csv: 'Name', className: styles.name, headerClassName: styles.noWrap, value: (i) => i.name, filter: (name) => `name:${quoteFilterString(name)}`, }), isSpreadsheet && c({ id: 'hash', header: 'Hash', csv: 'Hash', value: (i) => i.hash, }), isSpreadsheet && c({ id: 'id', header: 'Id', csv: 'Id', value: (i) => `"${i.id}"`, }), !isGhost && c({ id: 'power', csv: destinyVersion === 2 ? 'Power' : 'Light', className: styles.centered, headerClassName: styles.centered, header: , dropdownLabel: t('Organizer.Columns.Power'), value: (item) => item.power, defaultSort: SortDirection.DESC, filter: (value) => `power:>=${value}`, }), isWeapon && c({ id: 'dmg', header: t('Organizer.Columns.Damage'), className: styles.dmg, headerClassName: styles.dmgHeader, csv: 'Element', value: (item) => item.element?.displayProperties.name, cell: (_val, item) => , filter: (_val, item) => `is:${getItemDamageShortName(item)}`, }), isWeapon && c({ id: 'ammo', header: t('Organizer.Columns.Ammo'), className: styles.dmg, headerClassName: styles.dmgHeader, value: (item) => item.ammoType, cell: (_val, item) => , filter: (_val, item) => ammoTypeFilter.fromItem(item), csv: (_val, item) => ['Ammo', ammoTypeFilter.fromItem(item).replace('is:', '')], }), isArmor && isSpreadsheet && c({ id: 'Equippable', header: 'Equippable', csv: 'Equippable', value: (item) => item.classType === DestinyClass.Unknown ? 'Any' : item.classTypeNameLocalized, }), (isArmor || isGhost) && destinyVersion === 2 && c({ id: 'energy', header: t('Organizer.Columns.Energy'), className: styles.centered, headerClassName: styles.centered, csv: 'Energy Capacity', value: (item) => item.energy?.energyCapacity, defaultSort: SortDirection.DESC, filter: (value) => `energycapacity:>=${value}`, }), c({ id: 'locked', header: , csv: 'Locked', className: styles.centered, headerClassName: styles.centered, dropdownLabel: t('Organizer.Columns.Locked'), value: (i) => i.locked, cell: (value) => (value ? : undefined), defaultSort: SortDirection.DESC, filter: (value) => `${value ? '' : '-'}is:locked`, }), c({ id: 'tag', header: t('Organizer.Columns.Tag'), className: styles.centered, headerClassName: styles.centered, value: (item) => getTag(item) ?? '', cell: (value) => value && , sort: compareBy((tag) => (tag && tag in tagConfig ? tagConfig[tag].sortOrder : 1000)), filter: (value) => `tag:${value || 'none'}`, csv: (value) => ['Tag', value || undefined], }), !isSpreadsheet && $featureFlags.newItems && c({ id: 'new', header: t('Organizer.Columns.New'), className: styles.new, headerClassName: styles.centered, value: (item) => newItems.has(item.id), cell: (value) => (value ? : undefined), defaultSort: SortDirection.DESC, filter: (value) => `${value ? '' : '-'}is:new`, }), c({ id: 'featured', header: t('Organizer.Columns.Featured'), className: styles.centered, headerClassName: styles.centered, defaultSort: SortDirection.DESC, value: (item) => item.featured, cell: (value) => value && , filter: (value) => `${value ? '' : '-'}is:featured`, csv: 'New Gear', }), c({ id: 'holofoil', header: t('Organizer.Columns.Holofoil'), className: styles.centered, headerClassName: styles.centered, defaultSort: SortDirection.DESC, value: (item) => item.holofoil, cell: (value) => value && , filter: (value) => `${value ? '' : '-'}is:holofoil`, csv: 'Holofoil', }), destinyVersion === 2 && isWeapon && c({ id: 'crafted', header: t('Organizer.Columns.Crafted'), value: (item) => item.craftedInfo?.craftedDate, cell: (craftedDate) => craftedDate ? <>{new Date(craftedDate * 1000).toLocaleString()} : undefined, defaultSort: SortDirection.DESC, filter: (value) => `${value ? '' : '-'}is:crafted`, // TODO: nicer to put the date in the CSV csv: (value) => ['Crafted', value ? 'crafted' : false], }), !isSpreadsheet && c({ id: 'recency', header: t('Organizer.Columns.Recency'), value: (item) => item.id, cell: () => '', }), destinyVersion === 2 && isWeapon && !isSpreadsheet && c({ id: 'wishList', header: , dropdownLabel: t('Organizer.Columns.WishList'), className: styles.centered, headerClassName: styles.centered, value: (item) => { const roll = wishList(item); return roll ? !roll.isUndesirable : undefined; }, cell: (value) => value !== undefined ? ( ) : undefined, sort: compareBy((wishList) => (wishList === undefined ? 0 : wishList ? -1 : 1)), filter: (value) => value === true ? 'is:wishlist' : value === false ? 'is:trashlist' : '-is:wishlist', }), c({ id: 'tier', header: t('Organizer.Columns.Tier'), csv: 'Rarity', value: (i) => i.rarity, filter: (value) => `is:${value}`, }), c({ id: 'itemTier', header: t('Organizer.Columns.ItemTier'), className: styles.centered, headerClassName: styles.centered, defaultSort: SortDirection.DESC, value: (item) => item.tier, filter: (value) => `tier:${value}`, csv: 'Tier', }), isSpreadsheet && !isGhost && c({ id: 'Type', header: 'Type', csv: 'Type', value: (i) => i.typeName, }), isSpreadsheet && isWeapon && c({ id: 'Category', header: 'Category', csv: 'Category', value: (i) => { switch (i.bucket.hash) { case BucketHashes.KineticWeapons: return i.destinyVersion === 2 ? 'KineticSlot' : 'Primary'; case BucketHashes.EnergyWeapons: return i.destinyVersion === 2 ? 'Energy' : 'Special'; case BucketHashes.PowerWeapons: return i.destinyVersion === 2 ? 'Power' : 'Heavy'; default: return i.bucket.name; } }, }), isSpreadsheet && c({ id: 'Equipped', header: 'Equipped', csv: 'Equipped', value: (i) => i.equipped, }), destinyVersion === 2 && isArmor && c({ id: 'modslot', header: t('Organizer.Columns.ModSlot'), className: styles.modslot, // TODO: only show if there are mod slots value: (item) => isArtifice(item) ? 'artifice' : getSpecialtySocketMetadata(item)?.slotTag, cell: (value, item) => value && , filter: (value) => (value !== undefined ? `modslot:${value}` : ''), csv: (value) => ['Seasonal Mod', value ?? ''], }), destinyVersion === 1 && c({ id: 'percentComplete', header: t('Organizer.Columns.PercentComplete'), csv: '% Leveled', value: (item) => item.percentComplete, cell: (value) => percent(value), filter: (value) => `percentage:>=${value}`, }), destinyVersion === 2 && isWeapon && !isSpreadsheet && c({ id: 'breaker', header: t('Organizer.Columns.Breaker'), value: (item) => item.breakerType?.displayProperties.name, cell: (value, item) => value && ( ), filter: (_val, item) => item.breakerType ? `breaker:${breakerTypeNames[item.breakerType.hash as BreakerTypeHashes]}` : undefined, }), destinyVersion === 2 && !isGhost && c({ id: 'archetype', header: isWeapon ? t('Organizer.Columns.Frame') : t('Organizer.Columns.Archetype'), className: styles.noWrap, csv: 'Archetype', value: (item) => item.bucket.inWeapons ? getWeaponArchetype(item)?.displayProperties.name : getArmorArchetype(item)?.displayProperties.name, cell: (_val, item) => { const plugged = item.bucket.inWeapons ? getWeaponArchetypeSocket(item)?.plugged : getArmorArchetypeSocket(item)?.plugged; return ( plugged && ( } >
{' '} {plugged.plugDef.displayProperties.name}
) ); }, filter: (value) => (value ? `exactperk:${quoteFilterString(value)}` : undefined), }), destinyVersion === 2 && isArmor && c({ id: 'tertiary', className: styles.centered, header: t('Organizer.Columns.TertiaryStat'), csv: 'Tertiary Stat', value: (item) => isArmor3(item) ? realD2ArmorStatSearchByHash[getArmor3StatFocus(item)[2]] : undefined, cell: (statName, item) => { if (statName) { const stat = item.stats?.find((s) => s.statHash === statHashByName[statName]); if (stat) { return ( ); } } }, filter: (statName) => `tertiarystat:${statName}`, }), destinyVersion === 2 && isArmor && c({ id: 'tuning', className: styles.centered, header: t('Organizer.Columns.TuningStat'), csv: 'Tuning Stat', value: (item) => isArmor3(item) ? realD2ArmorStatSearchByHash[getArmor3TuningStat(item)!] : undefined, cell: (statName, item) => { if (statName) { const stat = item.stats?.find((s) => s.statHash === statHashByName[statName]); if (stat) { return ( ); } } }, filter: (statName) => `tunedstat:${statName}`, }), destinyVersion === 2 && isArmor && !isSpreadsheet && c({ id: 'intrinsics', className: styles.perkLike, header: t('Organizer.Columns.Perks'), value: (item) => perkString(getIntrinsicSockets(item)), cell: (_val, item) => ( ), sort: perkStringSort, filter: perkStringFilter, }), destinyVersion === 2 && isWeapon && !isSpreadsheet && c({ id: 'traits', className: styles.perkLike, header: t('Organizer.Columns.Traits'), value: (item) => perkString(getSocketsByType(item, 'traits')), cell: (_val, item) => ( ), sort: perkStringSort, filter: perkStringFilter, }), (isWeapon || isSpreadsheet) && c({ id: 'perks', className: styles.perks, header: destinyVersion === 2 ? isWeapon ? t('Organizer.Columns.OtherPerks') : t('Organizer.Columns.Mods') : t('Organizer.Columns.Perks'), value: (item) => perkString( getSocketsByType(item, isSpreadsheet || destinyVersion === 1 ? 'perks' : 'components'), ), cell: (_val, item) => isD1Item(item) ? ( ) : ( ), sort: perkStringSort, filter: perkStringFilter, csv: (_value, item) => { // This could go on any of the perks columns, since it computes a very // different view of perks, but I just picked one. const perks = isD1Item(item) && item.talentGrid ? buildNodeNames(item.talentGrid.nodes) : item.sockets ? buildSocketNames(item) : []; // Return multiple columns return [`Perks`, perks]; }, }), destinyVersion === 2 && isWeapon && !isSpreadsheet && c({ id: 'originTrait', className: styles.perkLike, header: t('Organizer.Columns.OriginTraits'), value: (item) => perkString(getSocketsByType(item, 'origin')), cell: (_val, item) => ( ), sort: perkStringSort, filter: perkStringFilter, }), destinyVersion === 2 && !isSpreadsheet && c({ id: 'mods', className: styles.perkLike, header: t('Organizer.Columns.Mods'), value: (item) => perkString(getSocketsByType(item, 'mods')), cell: (_val, item) => ( ), sort: perkStringSort, filter: perkStringFilter, }), destinyVersion === 2 && !isSpreadsheet && c({ id: 'shaders', className: styles.perkLike, header: t('Organizer.Columns.Shaders'), value: (item) => perkString(getSocketsByType(item, 'cosmetics')), cell: (_val, item) => ( ), sort: perkStringSort, filter: perkStringFilter, }), !isSpreadsheet && (isWeapon || (isArmor && destinyVersion === 1)) && perksGridColumn( styles.perksGrid, styles.perks, onPlugClicked, undefined, tl('Organizer.Columns.PerksGrid'), ), ...statColumns, ...baseStatColumns, ...d1ArmorQualityByStat, destinyVersion === 1 && isArmor && c({ id: 'quality', header: t('Organizer.Columns.Quality'), csv: '% Quality', value: (item) => (isD1Item(item) && item.quality ? item.quality.min : 0), cell: (value) => {value}%, filter: (value) => `quality:>=${value}`, }), ...(destinyVersion === 2 && isArmor ? customStats : []), destinyVersion === 2 && c({ id: 'masterworkTier', header: t('Organizer.Columns.MasterworkTier'), value: (item) => item.masterworkInfo?.tier, defaultSort: SortDirection.DESC, filter: (value) => `masterwork:>=${value}`, csv: 'Masterwork Tier', }), destinyVersion === 2 && isWeapon && c({ id: 'masterworkStat', header: t('Organizer.Columns.MasterworkStat'), value: (item) => getMasterworkStatNames(item.masterworkInfo), csv: 'Masterwork Type', }), destinyVersion === 2 && isWeapon && c({ id: 'level', header: t('Organizer.Columns.Level'), value: (item) => item.craftedInfo?.level, defaultSort: SortDirection.DESC, csv: (value) => ['Crafted Level', value ?? 0], }), destinyVersion === 2 && isWeapon && !isSpreadsheet && c({ id: 'harmonizable', header: , dropdownLabel: t('Organizer.Columns.Harmonizable'), value: (item) => isHarmonizable(item), cell: (value, item) => (value ? : undefined), }), destinyVersion === 2 && isWeapon && c({ id: 'killTracker', header: t('Organizer.Columns.KillTracker'), value: (item) => { const killTrackerInfo = getItemKillTrackerInfo(item); return killTrackerInfo?.count; }, cell: (_val, item) => { const killTrackerInfo = getItemKillTrackerInfo(item); return ( killTrackerInfo && ( ) ); }, defaultSort: SortDirection.DESC, csv: (value) => ['Kill Tracker', value ?? 0], }), destinyVersion === 2 && isWeapon && c({ id: 'foundry', header: t('Organizer.Columns.Foundry'), csv: 'Foundry', value: (item) => item.foundry, filter: (value) => `foundry:${value}`, }), destinyVersion === 2 && c({ id: 'source', csv: 'Source', header: t('Organizer.Columns.Source'), value: (item) => { const s = source(item); return s === 'legendaryengram' ? 'engram' : s; }, filter: (value) => `source:${value === 'engram' ? 'legendaryengram' : value}`, }), c({ id: 'year', csv: 'Year', className: styles.centered, headerClassName: styles.centered, header: t('Organizer.Columns.Year'), value: (item) => getItemYear(item), filter: (value) => `year:${value}`, }), destinyVersion === 2 && c({ id: 'season', csv: 'Season', className: styles.centered, headerClassName: styles.centered, header: t('Organizer.Columns.Season'), value: (i) => getSeason(i), filter: (value) => `season:${value}`, }), destinyVersion === 2 && c({ id: 'event', header: t('Organizer.Columns.Event'), value: (item) => { const event = getEvent(item); return event ? D2EventInfo[event].name : undefined; }, filter: (value) => `event:${value}`, csv: (value) => ['Event', value ?? ''], }), c({ id: 'location', header: t('Organizer.Columns.Location'), value: (item) => item.owner, cell: (_val, item) => , csv: (value, _item, { storeNamesById }) => ['Owner', storeNamesById[value]], }), c({ id: 'loadouts', header: t('Organizer.Columns.Loadouts'), className: styles.loadouts, value: (item) => { const loadouts = loadoutsByItem[item.id]; // The raw comparison value compares by number of loadouts first, // then by first loadout name return ( loadouts && // 99999 loadouts ought to be enough for anyone `${loadouts.length.toString().padStart(5, '0')}:${loadouts .map((l) => l.loadout.name) .sort() .join(',')}` ); }, cell: (_val, item) => { const inloadouts = loadoutsByItem[item.id]; return ( inloadouts && inloadouts.length > 0 && ( l.loadout).sort(compareBy((l) => l.name))} owner={item.owner} /> ) ); }, filter: (value, item) => { if (typeof value === 'string') { const inloadouts = loadoutsByItem[item.id]; const loadout = inloadouts?.find(({ loadout }) => loadout.id === value); return loadout && `inloadout:${quoteFilterString(loadout.loadout.name)}`; } }, csv: (value) => ['Loadouts', value ?? ''], }), c({ id: 'notes', header: t('Organizer.Columns.Notes'), // It's important for the value to always be a string, because users // expect to be able to sort empty notes along with items that have notes. // See https://github.com/DestinyItemManager/DIM/issues/10694 value: (item) => getNotes(item) ?? '', cell: (_val, item) => , gridWidth: 'minmax(200px, 1fr)', filter: (value) => `notes:${quoteFilterString(value)}`, csv: (value) => ['Notes', value || undefined], }), isWeapon && hasWishList && c({ id: 'wishListNote', header: t('Organizer.Columns.WishListNotes'), value: (item) => wishList(item)?.notes?.trim() ?? '', gridWidth: 'minmax(200px, 1fr)', filter: (value) => `wishlistnotes:${quoteFilterString(value)}`, }), ]); return columns; } export function getStatColumns( stats: DimStat[], customStatDefs: CustomStatDef[], destinyVersion: DestinyVersion, { isArmor, isSpreadsheet = false, showStatLabel = false, extraStatInfo = false, className, headerClassName, }: { isArmor: boolean; isSpreadsheet?: boolean; showStatLabel?: boolean; /** Whether to show extra stat info icons (e.g. that the total includes tuners, or that the stat is tuned) and stat bars. */ extraStatInfo?: boolean; className?: string; headerClassName?: string; }, ) { const customStatHashes = customStatDefs.map((c) => c.statHash); const statsGroup: ColumnGroup = { id: 'stats', header: t('Organizer.Columns.Stats'), }; const baseStatsGroup: ColumnGroup = { id: 'baseStats', header: t('Organizer.Columns.BaseStats'), }; const baseMasterworkStatsGroup: ColumnGroup = { id: 'baseMasterworkStats', header: t('Compare.AssumeMasterworked'), }; const statQualityGroup: ColumnGroup = { id: 'statQuality', header: t('Organizer.Columns.StatQuality'), }; const csvStatNames = csvStatNamesForDestinyVersion(destinyVersion); const statColumns: ColumnWithStat[] = filterMap(stats, (stat): ColumnWithStat | undefined => { const statHash = stat.statHash as StatHashes; if (customStatHashes.includes(statHash)) { // Exclude custom total, it has its own column return undefined; } const statLabel = statLabels[statHash]; return { id: `stat${statHash}`, header: stat.displayProperties.hasIcon ? ( {showStatLabel && stat.displayProperties.name} ) : statLabel ? ( t(statLabel) ) : ( stat.displayProperties.name ), className, headerClassName, statHash, columnGroup: statsGroup, value: (item) => { const stat = item.stats?.find((s) => s.statHash === statHash); if (stat?.statHash === StatHashes.RecoilDirection) { return recoilValue(stat.value); } return stat?.value; }, cell: (_val, item, ctx) => { const stat = item.stats?.find((s) => s.statHash === statHash); if (!stat) { return null; } return ( ); }, defaultSort: stat.smallerIsBetter ? SortDirection.ASC : SortDirection.DESC, filter: (value) => { const statName = invert(statHashByName)[statHash]; return `stat:${statName}:${statName === 'rof' ? '=' : '>='}${value}`; }, csv: (_value, item) => { // Re-find the stat instead of using the value passed in, because the // value passed in can be different if it's Recoil. const stat = item.stats?.find((s) => s.statHash === statHash); return [csvStatNames.get(statHash) ?? `UnknownStat ${statHash}`, stat?.value ?? 0]; }, sort: (firstValue, secondValue, firstItem, secondItem) => { if (typeof firstValue === 'number' && typeof secondValue === 'number') { const firstItemTuningHash = getArmor3TuningStat(firstItem); const secondItemTuningHash = getArmor3TuningStat(secondItem); if ((statHash as number) === TOTAL_STAT_HASH) { if (firstItemTuningHash) { firstValue += 0.5; } else if (isArtifice(firstItem)) { firstValue += 0.3; } if (secondItemTuningHash) { secondValue += 0.5; } else if (isArtifice(secondItem)) { secondValue += 0.3; } } else { if (firstItemTuningHash === statHash) { firstValue += 0.5; } if (secondItemTuningHash === statHash) { secondValue += 0.5; } } } return primitiveComparator(firstValue, secondValue); }, }; }).sort(compareBy((s) => getStatSortOrder(s.statHash))); const baseStatColumns: ColumnWithStat[] = destinyVersion === 2 && (isArmor || !isSpreadsheet) ? statColumns.map((column) => ({ ...column, id: `base${column.statHash}`, columnGroup: baseStatsGroup, value: (item): number | undefined => { const stat = item.stats?.find((s) => s.statHash === column.statHash); if (stat?.statHash === StatHashes.RecoilDirection) { return recoilValue(stat.base); } return stat?.base; }, cell: (_val, item, ctx) => { const stat = item.stats?.find((s) => s.statHash === column.statHash); if (!stat) { return null; } // TODO: force a width if this is armor, so we see the bar? return ( ); }, filter: (value) => `basestat:${invert(statHashByName)[column.statHash]}:>=${value}`, csv: (_value, item) => { // Re-find the stat instead of using the value passed in, because the // value passed in can be different if it's Recoil. const stat = item.stats?.find((s) => s.statHash === column.statHash); return [ `${csvStatNames.get(column.statHash) ?? `UnknownStatBase ${column.statHash}`} (Base)`, stat?.base ?? 0, ]; }, })) : []; const baseMasterworkStatColumns: ColumnWithStat[] = destinyVersion === 2 && isArmor && !isSpreadsheet ? statColumns.map((column) => ({ ...column, id: `baseMasterwork${column.statHash}`, columnGroup: baseMasterworkStatsGroup, value: (item): number | undefined => { const stat = item.stats?.find((s) => s.statHash === column.statHash); return stat?.baseMasterworked; }, cell: (val, item, ctx) => { const stat = item.stats?.find((s) => s.statHash === column.statHash); if (typeof val !== 'number') { return null; } // TODO: force a width if this is armor, so we see the bar? return ( ); }, filter: (value) => `basestat:${invert(statHashByName)[column.statHash]}:>=${value}`, csv: (_value, item) => { // Re-find the stat instead of using the value passed in, because the // value passed in can be different if it's Recoil. const stat = item.stats?.find((s) => s.statHash === column.statHash); return [ `${csvStatNames.get(column.statHash) ?? `UnknownStatBase ${column.statHash}`} (Base)`, stat?.base ?? 0, ]; }, })) : []; const d1ArmorQualityByStat = destinyVersion === 1 && isArmor ? stats .map((stat): ColumnWithStat => { const statHash = stat.statHash as StatHashes; return { statHash, id: `quality_${statHash}`, columnGroup: statQualityGroup, header: t('Organizer.Columns.StatQualityStat', { stat: stat.displayProperties.name, }), className, headerClassName: className, value: (item: D1Item) => { const stat = item.stats?.find((s) => s.statHash === statHash); let pct = 0; if (stat?.scaled?.min) { pct = Math.round((100 * stat.scaled.min) / (stat.split || 1)); } return pct; }, cell: (value: number, item: D1Item) => { const stat = item.stats?.find((s) => s.statHash === statHash); return ( {value}% ); }, csv: (_value, item) => { if (!isD1Item(item)) { throw new Error('Expected D1 item'); } const stat = item.stats?.find((s) => s.statHash === statHash); return [ `% ${csvStatNames.get(statHash) ?? `UnknownStat ${statHash}`}Q`, stat?.scaled?.min ? Math.round((100 * stat.scaled.min) / (stat.split || 1)) : 0, ]; }, }; }) .sort(compareBy((s) => getStatSortOrder(s.statHash))) : []; return { statColumns, baseStatColumns, baseMasterworkStatColumns, d1ArmorQualityByStat, }; } function LoadoutsCell({ loadouts, owner, }: { loadouts: (Loadout | InGameLoadout)[]; owner: string; }) { return ( <> {loadouts.map((loadout) => ( ))} ); } function PerksCell({ item, sockets, onPlugClicked, }: { item: DimItem; sockets: DimSocket[]; onPlugClicked?: (value: { item: DimItem; socket: DimSocket; plugHash: number }) => void; }) { if (!sockets.length) { return null; } return ( <> {sockets.map((socket) => (
1, })} > {socket.plugOptions.map((p) => ( }>
1 && p === socket.plugged, [styles.perkSelectable]: socket.plugOptions.length > 1, })} data-filter-value={p.plugDef.displayProperties.name} onClick={ onPlugClicked && socket.plugOptions.length > 1 ? (e: React.MouseEvent) => { if (!e.shiftKey) { e.stopPropagation(); onPlugClicked({ item, socket, plugHash: p.plugDef.hash }); } } : undefined } >
{p.plugDef.displayProperties.name}
))}
))} ); } function D1PerksCell({ item }: { item: D1Item }) { if (!isD1Item(item) || !item.talentGrid) { return null; } const sockets = Object.values( Object.groupBy( item.talentGrid.nodes.filter((n) => n.column > 0), (n) => n.column, ), ); if (!sockets.length) { return null; } return ( <> {sockets.map((socket) => (
1 && socket[0].exclusiveInColumn, })} > {socket.map( (p) => isD1Item(item) && (
{p.description}
} >
{' '} {p.name} {p.xpRequired > 0 && (!p.unlocked || p.xp < p.xpRequired) && ( <> ({percent(p.xp / p.xpRequired)}) )}
), )}
))} ); } function StoreLocation({ storeId }: { storeId: string }) { const store = useSelector((state: RootState) => getStore(storesSelector(state), storeId)!); return (
{store.className}
); } export function perkString(sockets: DimSocket[]): string | undefined { if (!sockets.length) { return undefined; } // TODO: filter out empty sockets, maybe sort? return sockets .flatMap((socket) => socket.plugOptions.map((p) => p.plugDef.displayProperties.name)) .filter(Boolean) .join(','); } export function getIntrinsicSockets(item: DimItem): DimSocket[] { const intrinsicSocket = getIntrinsicArmorPerkSocket(item); const extraIntrinsicSockets = getExtraIntrinsicPerkSockets(item); return intrinsicSocket && // artifice already shows up in the "modslot" column !isArtificeSocket(intrinsicSocket) ? [intrinsicSocket, ...extraIntrinsicSockets] : extraIntrinsicSockets; } /** * This builds stat infos for all the stats that are relevant to a particular category of items. * It will return the same result for the same category, since all items in a category share stats. */ export function buildStatInfo(items: DimItem[]): DimStat[] { if (!items.length) { return emptyArray(); } const statHashes: { [statHash: number]: DimStat } = {}; for (const item of items) { if (item.stats) { for (const stat of item.stats) { // Just use the first item's stats as the source of truth. statHashes[stat.statHash] ??= stat; } } } return Object.values(statHashes); } // ignore raid & calus sources in favor of more detailed sources const sourceKeys = Object.keys(D2Sources).filter((k) => !['raid', 'calus'].includes(k)); function source(item: DimItem) { if (item.destinyVersion === 2) { return ( sourceKeys.find( (src) => (item.source && D2Sources[src].sourceHashes?.includes(item.source)) || D2Sources[src].itemHashes?.includes(item.hash), ) || '' ); } } ================================================ FILE: src/app/organizer/CustomStatColumns.tsx ================================================ import { CustomStatDef } from '@destinyitemmanager/dim-api-types'; import CompareStat from 'app/compare/CompareStat'; import { CustomStatWeightsDisplay } from 'app/dim-ui/CustomStatWeights'; import { primitiveComparator } from 'app/utils/comparators'; import { getArmor3TuningStat, isArtifice } from 'app/utils/item-utils'; import { collectRelevantStatHashes } from 'app/utils/stats'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { ColumnDefinition, SortDirection } from './table-types'; export function createCustomStatColumns( customStatDefs: CustomStatDef[], { hideFormula = false, withMasterwork = false, extraStatInfo = false, className, headerClassName, }: { hideFormula?: boolean; withMasterwork?: boolean; /** Whether to show extra stat info icons (e.g. that the total includes tuners, or that the stat is tuned) and stat bars. */ extraStatInfo?: boolean; className?: string; headerClassName?: string; } = {}, ): ColumnDefinition[] { return customStatDefs.map((c): ColumnDefinition => { const { class: guardianClass, label, shortLabel, statHash, weights } = c; const relevantStatHashes = collectRelevantStatHashes(weights); return { id: `customstat_${shortLabel}${statHash}`, header: hideFormula ? ( label ) : (
{label}
), className, headerClassName, value: (item) => item.stats?.find((s) => s.statHash === statHash)?.[ withMasterwork ? 'baseMasterworked' : 'base' ] ?? 0, cell: (val, item, ctx) => { const stat = item.stats?.find((s) => s.statHash === statHash); if (!stat || typeof val !== 'number') { return null; } // TODO: force a width if this is armor, so we see the bar? return ( ); }, defaultSort: SortDirection.DESC, filter: (value) => `stat:${label}:>=${value}`, columnGroup: { id: shortLabel + statHash, header: label, }, limitToClass: guardianClass === DestinyClass.Unknown ? undefined : guardianClass, sort: (firstValue, secondValue, firstItem, secondItem) => { if (typeof firstValue === 'number' && typeof secondValue === 'number') { if (isArtifice(firstItem)) { firstValue += 0.3; } else if (relevantStatHashes.includes(getArmor3TuningStat(firstItem)!)) { firstValue += 0.5; } if (isArtifice(secondItem)) { secondValue += 0.3; } else if (relevantStatHashes.includes(getArmor3TuningStat(secondItem)!)) { secondValue += 0.5; } } return primitiveComparator(firstValue, secondValue); }, }; }); } ================================================ FILE: src/app/organizer/DropDown.m.scss ================================================ @use '../variables' as *; $dropdown-menu: 10; .dropDown { display: inline-block; position: relative; z-index: $dropdown-menu; } .button { display: inline-block; > :global(.app-icon) { font-size: 10px; margin-left: 4px; } } .menu { composes: visibleScrollbars from '../dim-ui/common.m.scss'; position: absolute; z-index: 5; left: 0; max-height: calc(var(--viewport-height) - 50px - var(--header-height)); overflow: auto; margin-top: 2px; width: max-content; background: var(--theme-dropdown-menu-bg); columns: 4; column-gap: 0; &.right { left: initial; right: 0; } } .checkButton { border-radius: 0; border: none; box-sizing: border-box; color: var(--theme-text); cursor: pointer; display: flex; flex-direction: row; font-size: 12px; margin: 0; padding: 8px; text-shadow: 1px 1px 3px rgb(0, 0, 0, 0.25); min-width: 15em; gap: 8px; @include interactive($hover: true, $active: true) { color: var(--theme-text-invert); box-shadow: none; background-color: var(--theme-accent-primary); } :global(.app-icon) { margin: auto 0 auto 4px; } label { display: flex; gap: 4px; margin-right: 4px; > :global(.app-icon) { margin-right: 4px; } > img { margin-right: 4px; vertical-align: middle; } } } ================================================ FILE: src/app/organizer/DropDown.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'button': string; 'checkButton': string; 'dropDown': string; 'menu': string; 'right': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/organizer/DropDown.tsx ================================================ import ClickOutside from 'app/dim-ui/ClickOutside'; import { StatTotalToggle } from 'app/dim-ui/CustomStatTotal'; import { t } from 'app/i18next-t'; import { AppIcon, enabledIcon, expandDownIcon, unselectedCheckIcon } from 'app/shell/icons'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import React, { ReactNode, useState } from 'react'; import * as styles from './DropDown.m.scss'; export interface DropDownItem { id: string; content: ReactNode; dropdownLabel?: ReactNode; checked?: boolean; onItemSelect?: (e: React.MouseEvent) => void; } function MenuItem({ item, forClass }: { item: DropDownItem; forClass?: DestinyClass }) { return (
{item.checked !== undefined && ( )}
); } function DropDown({ buttonText, buttonDisabled, dropDownItems, forClass, right, }: { buttonText: ReactNode; buttonDisabled?: boolean; dropDownItems: DropDownItem[]; forClass?: DestinyClass; /** Is this right-aligned? */ right?: boolean; }) { const [dropdownOpen, setDropdownOpen] = useState(false); return ( setDropdownOpen(false)} className={styles.dropDown}>
{dropdownOpen && dropDownItems.map((item) => )}
); } export default DropDown; ================================================ FILE: src/app/organizer/EnabledColumnsSelector.tsx ================================================ import { t } from 'app/i18next-t'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { memo } from 'react'; import { getColumnSelectionId } from './Columns'; import DropDown, { DropDownItem } from './DropDown'; import { ColumnDefinition } from './table-types'; /** * Component for selection of which columns are displayed in the organizer table. * Props: * columns: all possible columns in the table (whether showing or not) * enabledColumns: a list of the column id's for the currently visible columns * onChangeEnabledColumn: handler for when column visibility is toggled * * TODO: Convert to including drag and drop functionality so that columns can be reordered. */ // TODO: Save to settings export default memo(function EnabledColumnsSelector({ columns, enabledColumns, forClass, onChangeEnabledColumn, }: { columns: ColumnDefinition[]; enabledColumns: string[]; forClass: DestinyClass; onChangeEnabledColumn: (item: { checked: boolean; id: string }) => void; }) { const items: { [id: string]: DropDownItem } = {}; for (const column of columns) { const id = getColumnSelectionId(column); const header = column.columnGroup ? column.columnGroup.header : column.header; const dropdownLabel = column.columnGroup ? column.columnGroup.dropdownLabel : column.dropdownLabel; if ( id === 'selection' || column.noHide || (column.limitToClass !== undefined && column.limitToClass !== forClass) ) { continue; } const checked = enabledColumns.includes(id) || false; if (!(id in items)) { items[id] = { id, content: header, dropdownLabel: dropdownLabel, checked, onItemSelect: () => onChangeEnabledColumn({ id, checked: !checked }), }; } } return ( ); }); ================================================ FILE: src/app/organizer/ItemActions.m.scss ================================================ .itemActions { flex: 1; composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; gap: 4px; } .actionButton { display: inline-block; } .tip { @media (max-width: 1270px) { display: none; } } .label { padding-left: 3px; @media (max-width: 900px) { display: none; } } .keyHelp { margin-left: auto; } .storeName { margin-right: 8px; } ================================================ FILE: src/app/organizer/ItemActions.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'actionButton': string; 'itemActions': string; 'keyHelp': string; 'label': string; 'storeName': string; 'tip': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/organizer/ItemActions.tsx ================================================ import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; import Dropdown, { Option } from 'app/dim-ui/Dropdown'; import KeyHelp from 'app/dim-ui/KeyHelp'; import { useHotkey, useHotkeys } from 'app/hotkeys/useHotkey'; import { I18nKey, t } from 'app/i18next-t'; import { TagCommand, itemTagList } from 'app/inventory/dim-item-info'; import { DimStore } from 'app/inventory/store-types'; import { getCurrentStore, getVault } from 'app/inventory/stores-helpers'; import { AppIcon, compareIcon, lockIcon, moveIcon, stickyNoteIcon, tagIcon, unlockedIcon, } from 'app/shell/icons'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { useCallback, useMemo } from 'react'; import * as styles from './ItemActions.m.scss'; export interface TagCommandInfo { type?: TagCommand; label: I18nKey; sortOrder?: number; displacePriority?: number; hotkey?: string; icon?: string | IconDefinition; } const bulkItemTags: TagCommandInfo[] = Array.from(itemTagList); bulkItemTags.push({ type: 'clear', label: 'Tags.ClearTag', hotkey: 'shift+0' }); function ItemActions({ stores, itemsAreSelected, onLock, onNote, onTagSelectedItems, onMoveSelectedItems, onCompareSelectedItems, }: { stores: DimStore[]; itemsAreSelected: boolean; onLock: (locked: boolean) => void; onNote: () => void; onTagSelectedItems: (tagInfo: TagCommandInfo) => void; onMoveSelectedItems: (store: DimStore) => void; onCompareSelectedItems: () => void; }) { const isPhonePortrait = useIsPhonePortrait(); const currentStore = getCurrentStore(stores)!; const vault = getVault(stores)!; const tagItems: Option[] = bulkItemTags.map((tagInfo) => ({ key: tagInfo.label, content: ( <> {tagInfo.icon && } {t(tagInfo.label)} {!isPhonePortrait && tagInfo.hotkey && ( )} ), onSelected: () => onTagSelectedItems(tagInfo), })); const moveItems: Option[] = stores.map((store) => ({ key: store.id, content: ( <> {' '} {store.name} {!isPhonePortrait && (store === vault ? ( ) : store === currentStore ? ( ) : null)} ), onSelected: () => onMoveSelectedItems(store), })); const hotkeys = useMemo(() => { const hotkeys = []; for (const tag of bulkItemTags) { if (tag.hotkey) { hotkeys.push({ combo: tag.hotkey, description: t('Hotkey.MarkItemAs', { tag: tag.type! }), callback: () => onTagSelectedItems(tag), }); } } return hotkeys; }, [onTagSelectedItems]); useHotkeys(hotkeys); useHotkey('n', t('Hotkey.Note'), onNote); useHotkey('c', t('Compare.ButtonHelp'), onCompareSelectedItems); useHotkey( 'p', t('Hotkey.Pull'), useCallback(() => onMoveSelectedItems(currentStore), [currentStore, onMoveSelectedItems]), ); useHotkey( 'v', t('Hotkey.Vault'), useCallback(() => onMoveSelectedItems(vault), [vault, onMoveSelectedItems]), ); return (
{t('Organizer.BulkTag')} {t('Organizer.BulkMove')} {t('Organizer.ShiftTip')}
); } export default ItemActions; ================================================ FILE: src/app/organizer/ItemTable.m.scss ================================================ @use '../variables' as *; // stacking rules $dropdown-menu: 10; $toolbar: 9; $header-cells: 7; $content-cells: 5; :root { --table-header-height: 30px; --item-table-toolbar-height: 0; } // Put these on their own layer so they can be overridden easily @layer itemtable { .table { display: grid; margin: 8px 0 16px 0; // Lower specificity to allow overrides :where([role='cell']) { display: flex; flex-direction: row; z-index: $content-cells; } } .toolbar { grid-column: 1 / -1; z-index: $toolbar; box-sizing: border-box; top: var(--header-height); padding: 8px; background-color: var(--theme-organizer-row-odd-bg); display: flex; flex-direction: row; align-items: center; position: sticky; left: env(safe-area-inset-left); // Reserve space for always-on scrollbars :-( width: calc( 100vw - var(--scrollbar-width) - env(safe-area-inset-left) - env(safe-area-inset-right) ); gap: 4px; } .importButton { @media (max-width: 1000px) { display: none; } } // Column headers .header { vertical-align: bottom; text-align: left; border-bottom: 1px solid #ddd !important; background: #313233; padding: 4px 8px; white-space: nowrap; padding-top: 4px !important; display: flex; flex-direction: column; justify-content: flex-end !important; img { height: 16px; width: 16px; vertical-align: bottom; } } .sorter { margin-left: 2px; } .selection { background-color: var(--org-row-bg); padding-left: 8px !important; padding-right: 2px !important; min-width: 20px; left: env(safe-area-inset-left); position: sticky; top: calc(var(--header-height) + var(--table-header-height) + var(--item-table-toolbar-height)); z-index: $header-cells; &.header { top: calc(var(--header-height) + var(--item-table-toolbar-height)); } } // Indicate cells that can be filtered on shift-click .shiftHeld { .hasFilter { @include interactive($hover: true) { background-color: var(--theme-accent-primary) !important; cursor: pointer; } } } .noItems { text-align: center; grid-column: 1 / -1; padding: 2em !important; } .row { --org-row-bg: var(--theme-organizer-row-odd-bg); display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; background-color: var(--org-row-bg); &:nth-of-type(2n) { --org-row-bg: var(--theme-organizer-row-even-bg); } &:hover { background-color: rgb(255, 255, 255, 0.1); } } :where(.row > div) { padding: 4px 8px; padding-top: calc(var(--item-size) * 0.75 * 0.5 - 4px); } .headerRow { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; background: #313233; position: sticky; z-index: $header-cells + 1; top: calc(var(--header-height) + var(--item-table-toolbar-height)); > * { background-color: var(--theme-organizer-row-odd-bg); } } } ================================================ FILE: src/app/organizer/ItemTable.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'hasFilter': string; 'header': string; 'headerRow': string; 'importButton': string; 'noItems': string; 'row': string; 'selection': string; 'shiftHeld': string; 'sorter': string; 'table': string; 'toolbar': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/organizer/ItemTable.tsx ================================================ import { destinyVersionSelector } from 'app/accounts/selectors'; import { languageSelector, settingSelector } from 'app/dim-api/selectors'; import UserGuideLink from 'app/dim-ui/UserGuideLink'; import useBulkNote from 'app/dim-ui/useBulkNote'; import useConfirm from 'app/dim-ui/useConfirm'; import { t, tl } from 'app/i18next-t'; import { bulkLockItems, bulkTagItems } from 'app/inventory/bulk-actions'; import { DimItem, DimStat } from 'app/inventory/item-types'; import { allItemsSelector, createItemContextSelector, getNotesSelector, getTagSelector, newItemsSelector, storesSelector, } from 'app/inventory/selectors'; import { downloadCsvFiles, importTagsNotesFromCsv } from 'app/inventory/spreadsheets'; import { DimStore } from 'app/inventory/store-types'; import { applySocketOverrides, useSocketOverridesForItems, } from 'app/inventory/store/override-sockets'; import { applyLoadout } from 'app/loadout-drawer/loadout-apply'; import { convertToLoadoutItem, newLoadout } from 'app/loadout-drawer/loadout-utils'; import { loadoutsByItemSelector } from 'app/loadout/selectors'; import { useD2Definitions } from 'app/manifest/selectors'; import { showNotification } from 'app/notifications/notifications'; import { searchFilterSelector } from 'app/search/items/item-search-filter'; import { setSettingAction } from 'app/settings/actions'; import { toggleSearchQueryComponent } from 'app/shell/actions'; import { AppIcon, faCaretDown, faCaretUp, spreadsheetIcon, uploadIcon } from 'app/shell/icons'; import { loadingTracker } from 'app/shell/loading-tracker'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { Comparator, chainComparator, compareBy, reverseComparator } from 'app/utils/comparators'; import { emptyArray } from 'app/utils/empty'; import { useSetCSSVarToHeight, useShiftHeld } from 'app/utils/hooks'; import { LookupTable } from 'app/utils/util-types'; import { hasWishListSelector, wishListFunctionSelector } from 'app/wishlists/selectors'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import React, { ReactNode, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Dropzone, { DropzoneOptions } from 'react-dropzone'; import { useSelector } from 'react-redux'; import { buildStatInfo, getColumnSelectionId, getColumns } from './Columns'; import EnabledColumnsSelector from './EnabledColumnsSelector'; import ItemActions, { TagCommandInfo } from './ItemActions'; import { compareSelectedItems } from 'app/compare/actions'; import { useTableColumnSorts } from 'app/dim-ui/table-columns'; import { compact, filterMap } from 'app/utils/collections'; import { errorMessage } from 'app/utils/errors'; import { DimLanguage } from 'app/i18n'; import { localizedSorter } from 'app/utils/intl'; import * as styles from './ItemTable.m.scss'; import { ItemCategoryTreeNode, armorTopLevelCatHashes } from './ItemTypeSelector'; import { ColumnDefinition, ColumnSort, Row, SortDirection, TableContext } from './table-types'; const categoryToClass: LookupTable = { [ItemCategoryHashes.Hunter]: DestinyClass.Hunter, [ItemCategoryHashes.Titan]: DestinyClass.Titan, [ItemCategoryHashes.Warlock]: DestinyClass.Warlock, }; const downloadButtonSettings = [ { categoryId: ['weapons'], csvType: 'weapon' as const, label: tl('Bucket.Weapons') }, { categoryId: ['hunter', 'titan', 'warlock'], csvType: 'armor' as const, label: tl('Bucket.Armor'), }, { categoryId: ['ghosts'], csvType: 'ghost' as const, label: tl('Bucket.Ghost') }, ]; export const MemoRow = memo(TableRow); const EXPAND_INCREMENT = 20; export default function ItemTable({ categories }: { categories: ItemCategoryTreeNode[] }) { const [columnSorts, toggleColumnSort] = useTableColumnSorts([ { columnId: 'name', sort: SortDirection.ASC }, ]); const [selectedItemIds, setSelectedItemIds] = useState([]); // Track the last selection for shift-selecting const lastSelectedId = useRef(null); const [socketOverrides, onPlugClicked] = useSocketOverridesForItems(); const [maxItems, setMaxItems] = useState(EXPAND_INCREMENT); useEffect(() => { setMaxItems(EXPAND_INCREMENT); }, [categories]); const expandItems = useCallback(() => setMaxItems((m) => m + EXPAND_INCREMENT), []); const allItems = useSelector(allItemsSelector); const searchFilter = useSelector(searchFilterSelector); const originalItems = useMemo(() => { const terminal = Boolean(categories.at(-1)?.terminal); if (!terminal) { return emptyArray(); } const categoryHashes = categories.map((s) => s.itemCategoryHash).filter((h) => h !== 0); // a top level class-specific category implies armor if (armorTopLevelCatHashes.some((h) => categoryHashes.includes(h))) { categoryHashes.push(ItemCategoryHashes.Armor); } const items = allItems.filter( (i) => i.comparable && categoryHashes.every((h) => i.itemCategoryHashes.includes(h)) && searchFilter(i), ); return items; }, [allItems, categories, searchFilter]); const firstCategory = categories[1]; const isWeapon = Boolean(firstCategory?.itemCategoryHash === ItemCategoryHashes.Weapon); const isGhost = Boolean(firstCategory?.itemCategoryHash === ItemCategoryHashes.Ghost); const isArmor = !isWeapon && !isGhost; const itemType = isWeapon ? 'weapon' : isArmor ? 'armor' : 'ghost'; const stores = useSelector(storesSelector); const getTag = useSelector(getTagSelector); const getNotes = useSelector(getNotesSelector); const wishList = useSelector(wishListFunctionSelector); const hasWishList = useSelector(hasWishListSelector); const enabledColumns = useSelector(settingSelector(columnSetting(itemType))); const itemCreationContext = useSelector(createItemContextSelector); const loadoutsByItem = useSelector(loadoutsByItemSelector); const newItems = useSelector(newItemsSelector); const destinyVersion = useSelector(destinyVersionSelector); const dispatch = useThunkDispatch(); const { customStats } = itemCreationContext; const classCategoryHash = categories .map((n) => n.itemCategoryHash) .find((hash) => hash in categoryToClass); const classIfAny: DestinyClass = classCategoryHash ? (categoryToClass[classCategoryHash] ?? DestinyClass.Unknown) : DestinyClass.Unknown; // Calculate the true height of the table header, for sticky-ness const tableRef = useRef(null); useEffect(() => { if (tableRef.current) { let height = 0; for (const node of tableRef.current.children) { if (node.classList.contains(styles.header)) { height = Math.max(node.clientHeight, height); } else if (height > 0) { break; } } document.querySelector('html')!.style.setProperty('--table-header-height', `${height + 1}px`); } }); // Are we at a item category that can show items? const terminal = Boolean(categories.at(-1)?.terminal); const defs = useD2Definitions(); const items = useMemo( () => defs ? originalItems.map((item) => applySocketOverrides(itemCreationContext, item, socketOverrides[item.id]), ) : originalItems, [itemCreationContext, defs, originalItems, socketOverrides], ); // Build a list of all the stats relevant to this set of items const stats = useMemo( () => (terminal ? buildStatInfo(items) : emptyArray()), [terminal, items], ); const columns: ColumnDefinition[] = useMemo( () => getColumns( 'organizer', itemType, stats, getTag, getNotes, wishList, hasWishList, customStats, loadoutsByItem, newItems, destinyVersion, onPlugClicked, ), [ wishList, hasWishList, stats, itemType, getTag, getNotes, customStats, loadoutsByItem, newItems, destinyVersion, onPlugClicked, ], ); // This needs work for sure const filteredColumns = useMemo( () => compact( columns.filter( (column) => enabledColumns.includes(getColumnSelectionId(column)) && (column.limitToClass === undefined || column.limitToClass === classIfAny), ), ), [columns, enabledColumns, classIfAny], ); // process items into Rows const [unsortedRows, tableContext] = useMemo( () => buildRows(items, filteredColumns), [filteredColumns, items], ); const language = useSelector(languageSelector); const rows = useMemo( () => sortRows(unsortedRows, columnSorts, filteredColumns, language), [unsortedRows, filteredColumns, columnSorts, language], ); const shiftHeld = useShiftHeld(); const onChangeEnabledColumn = useCallback( ({ checked, id }: { checked: boolean; id: string }) => { dispatch( setSettingAction( columnSetting(itemType), Array.from( new Set( filterMap(columns, (c) => { const cId = getColumnSelectionId(c); if (cId === id) { return checked ? cId : undefined; } else { return enabledColumns.includes(cId) ? cId : undefined; } }), ), ), ), ); }, [dispatch, columns, enabledColumns, itemType], ); const selectedItems = items.filter((i) => selectedItemIds.includes(i.id)); const onLock = loadingTracker.trackPromise(async (lock: boolean) => { dispatch(bulkLockItems(selectedItems, lock)); }); const [bulkNoteDialog, bulkNote] = useBulkNote(); const onNote = () => bulkNote(selectedItems); /** * Handles Click Events for Table Rows * When shift-clicking a value, if there's a filter function defined, narrow/un-narrow the search * When ctrl-clicking toggles selected value */ const onRowClick = useCallback( ( row: Row, column: ColumnDefinition, ): React.MouseEventHandler | undefined => column.filter ? (e) => { if (e.shiftKey) { const node = e.target as HTMLElement; const filter = column.filter!( node.dataset.filterValue ?? node.parentElement?.dataset.filterValue ?? // look for filter-value at most 1 element up row.values[column.id], row.item, ); if (filter !== undefined) { dispatch(toggleSearchQueryComponent(filter)); } } if (e.ctrlKey) { setSelectedItemIds( selectedItemIds.findIndex((selectedItemId) => selectedItemId === row.item.id) === -1 ? [...selectedItemIds, row.item.id] : selectedItemIds.filter((id) => id !== row.item.id), ); } } : undefined, [dispatch, selectedItemIds], ); const onMoveSelectedItems = useCallback( (store: DimStore) => { if (selectedItems.length) { const loadout = newLoadout( t('Organizer.BulkMoveLoadoutName'), selectedItems.map((i) => convertToLoadoutItem(i, false)), ); dispatch(applyLoadout(store, loadout, { allowUndo: true })); } }, [dispatch, selectedItems], ); const onTagSelectedItems = useCallback( (tagInfo: TagCommandInfo) => { if (tagInfo.type && selectedItemIds.length) { const selectedItems = items.filter((i) => selectedItemIds.includes(i.id)); dispatch(bulkTagItems(selectedItems, tagInfo.type, true)); } }, [dispatch, items, selectedItemIds], ); const onCompareSelectedItems = useCallback(() => { if (selectedItemIds.length) { const selectedItems = items.filter((i) => selectedItemIds.includes(i.id)); dispatch(compareSelectedItems(selectedItems)); } }, [dispatch, items, selectedItemIds]); const gridSpec = `min-content ${filteredColumns .map((c) => c.gridWidth ?? 'min-content') .join(' ')}`; /** * Select all items, or if any are selected, clear the selection. */ const selectAllItems: React.ChangeEventHandler = () => { if (selectedItems.length === 0) { setSelectedItemIds(rows.map((r) => r.item.id)); } else { setSelectedItemIds([]); } }; /** * Select and unselect items. Supports shift-held range selection. */ const selectItem = (e: React.ChangeEvent, item: DimItem) => { const checked = e.target.checked; let changingIds = [item.id]; if (shiftHeld && lastSelectedId.current) { let startIndex = rows.findIndex((r) => r.item.id === lastSelectedId.current); let endIndex = rows.findIndex((r) => r.item === item); if (startIndex > endIndex) { const tmp = startIndex; startIndex = endIndex; endIndex = tmp; } changingIds = rows.slice(startIndex, endIndex + 1).map((r) => r.item.id); } if (checked) { setSelectedItemIds((selected) => [...new Set([...selected, ...changingIds])]); } else { setSelectedItemIds((selected) => selected.filter((i) => !changingIds.includes(i))); } lastSelectedId.current = item.id; }; let downloadAction: ReactNode | null = null; const downloadButtonSetting = downloadButtonSettings.find((setting) => setting.categoryId.includes(categories[1]?.id), ); if (downloadButtonSetting) { const downloadHandler = (e: React.MouseEvent) => { e.preventDefault(); dispatch(downloadCsvFiles(downloadButtonSetting.csvType)); return false; }; downloadAction = ( ); } const [confirmDialog, confirm] = useConfirm(); const importCsv: DropzoneOptions['onDrop'] = async (acceptedFiles) => { if (acceptedFiles.length < 1) { showNotification({ type: 'error', title: t('Csv.ImportWrongFileType') }); return; } if (!(await confirm(t('Csv.ImportConfirm')))) { return; } try { const result = await dispatch(importTagsNotesFromCsv(acceptedFiles)); showNotification({ type: 'success', title: t('Csv.ImportSuccess', { count: result }) }); } catch (e) { showNotification({ type: 'error', title: t('Csv.ImportFailed', { error: errorMessage(e) }) }); } }; const toolbarRef = useRef(null); useSetCSSVarToHeight(toolbarRef, '--item-table-toolbar-height'); return ( <>
{confirmDialog} {bulkNoteDialog}
{({ getRootProps, getInputProps }) => (
{t('Settings.CsvImport')}
)}
{downloadAction}
{ el && (el.indeterminate = selectedItems.length !== rows.length && selectedItems.length > 0); }} onChange={selectAllItems} />
{filteredColumns.map((column) => { const columnSort = column.noSort ? undefined : columnSorts.find((c) => c.columnId === column.id); return (
{column.header} {columnSort && ( )}
); })}
{rows.length === 0 &&
{t('Organizer.NoItems')}
} {rows.slice(0, maxItems).map((row) => (
selectItem(e, row.item)} />
))}
{rows.length > maxItems && } ); } /** * Build a list of rows with materialized values. */ export function buildRows(items: DimItem[], filteredColumns: ColumnDefinition[]) { const unsortedRows: Row[] = items.map((item) => ({ item, values: filteredColumns.reduce((memo, col) => { memo[col.id] = col.value(item); return memo; }, {}), })); // Build a map of min/max values for each column // TODO: Use these to color stats in the ItemTable view const ctx: TableContext = { minMaxValues: {} }; for (const column of filteredColumns) { if (column.cell) { for (const row of unsortedRows) { const value = row.values[column.id]; if (typeof value === 'number') { const minMax = (ctx.minMaxValues[column.id] ??= { min: value, max: value }); minMax.min = Math.min(minMax.min, value); minMax.max = Math.max(minMax.max, value); } } } } return [unsortedRows, ctx] as const; } /** * Sort the rows based on the selected columns. */ export function sortRows( unsortedRows: Row[], columnSorts: ColumnSort[], filteredColumns: ColumnDefinition[], language: DimLanguage, defaultComparator?: Comparator, ) { if (!columnSorts.length && defaultComparator) { return unsortedRows.toSorted(defaultComparator); } const comparator = chainComparator( ...columnSorts.map((sorter) => { const column = filteredColumns.find((c) => c.id === sorter.columnId); if (column) { const sort = column.sort; const compare: Comparator = sort ? (row1, row2) => sort(row1.values[column.id], row2.values[column.id], row1.item, row2.item) : unsortedRows.some((row) => typeof row.values[column.id] === 'string') ? localizedSorter(language, (row) => (row.values[column.id] ?? '') as string) : compareBy((row) => row.values[column.id] ?? 0); // Always sort undefined values to the end return chainComparator( compareBy((row) => row.values[column.id] === undefined), sorter.sort === SortDirection.ASC ? compare : reverseComparator(compare), ); } return compareBy(() => 0); }), ); return unsortedRows.toSorted(comparator); } function TableRow({ row, filteredColumns, onRowClick, tableCtx, }: { row: Row; filteredColumns: ColumnDefinition[]; tableCtx: TableContext; onRowClick: ( row: Row, column: ColumnDefinition, ) => ((event: React.MouseEvent) => void) | undefined; }) { return ( <> {filteredColumns.map((column) => ( // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
{column.cell ? column.cell(row.values[column.id], row.item, tableCtx.minMaxValues[column.id]) : row.values[column.id]}
))} ); } function columnSetting(itemType: 'weapon' | 'armor' | 'ghost') { switch (itemType) { case 'weapon': return 'organizerColumnsWeapons'; case 'armor': return 'organizerColumnsArmor'; case 'ghost': return 'organizerColumnsGhost'; } } function ItemListExpander({ onExpand, numItems }: { onExpand: () => void; numItems: number }) { const ref = useRef(null); useEffect(() => { const elem = ref.current; if (!elem) { return; } const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { onExpand(); } } }, { root: null, rootMargin: '16px', threshold: 0, }, ); observer.observe(elem); return () => observer.unobserve(elem); }, [ onExpand, // This is a hack to fix the case where: // 1. The expander is on screen when the component renders. // 2. After adding more items, it's still on screen. Since the observer only // runs if the item is initially onscreen, or enters the screen, there // are no changes. So we'll just reconstruct the observer every time to // allow it to re-fire if it's still on the screen. numItems, ]); return
; } ================================================ FILE: src/app/organizer/ItemTypeSelector.m.scss ================================================ @use '../variables' as *; .selector { position: sticky; box-sizing: border-box; left: calc(8px + env(safe-area-inset-left)); // Reserve space for always-on scrollbars (and margin) :-( width: calc( 100vw - var(--scrollbar-width) - 16px - env(safe-area-inset-left) - env(safe-area-inset-right) ); padding: 8px; margin: 16px 8px 8px 8px; display: flex; flex-direction: column; align-items: center; gap: 12px; } .level { display: flex; flex-flow: row wrap; justify-content: center; align-items: flex-start; gap: 4px; } .buttonItemCount { opacity: 0.5; } ================================================ FILE: src/app/organizer/ItemTypeSelector.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttonItemCount': string; 'level': string; 'selector': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/organizer/ItemTypeSelector.tsx ================================================ import { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import BucketIcon from 'app/dim-ui/svgs/BucketIcon'; import { useDefinitions } from 'app/manifest/selectors'; import { filteredItemsSelector } from 'app/search/items/item-search-filter'; import { count } from 'app/utils/collections'; import clsx from 'clsx'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import { useSelector } from 'react-redux'; import * as styles from './ItemTypeSelector.m.scss'; /** * Each branch of the drilldown options is represented by a SelectionTreeNode * which tells which item category to filter with, as well as what sub-categories * can still be drilled down into. */ export interface ItemCategoryTreeNode { id: string; itemCategoryHash: ItemCategoryHashes | 0; subCategories?: ItemCategoryTreeNode[]; /** A terminal node can have items displayed for it. It may still have other drilldowns available. */ terminal?: boolean; } // Each class has the same armor const armorCategories = [ { id: 'helmet', itemCategoryHash: ItemCategoryHashes.Helmets, terminal: true, }, { id: 'arms', itemCategoryHash: ItemCategoryHashes.Arms, terminal: true, }, { id: 'chest', itemCategoryHash: ItemCategoryHashes.Chest, terminal: true, }, { id: 'legs', itemCategoryHash: ItemCategoryHashes.Legs, terminal: true, }, { id: 'classItem', itemCategoryHash: ItemCategoryHashes.ClassItems, terminal: true, }, ]; // Each weapon type may be in several subcategories const kinetic: ItemCategoryTreeNode = { id: 'kinetic', itemCategoryHash: ItemCategoryHashes.KineticWeapon, terminal: true, }; const energy: ItemCategoryTreeNode = { id: 'energy', itemCategoryHash: ItemCategoryHashes.EnergyWeapon, terminal: true, }; const power: ItemCategoryTreeNode = { id: 'power', itemCategoryHash: ItemCategoryHashes.PowerWeapon, terminal: true, }; /** * Generate a tree of all the drilldown options for item filtering. This tree is * used to generate the list of selected subcategories. */ const d2SelectionTree: ItemCategoryTreeNode = { id: 'all', itemCategoryHash: 0, subCategories: [ { id: 'weapons', itemCategoryHash: ItemCategoryHashes.Weapon, terminal: true, subCategories: [ { id: 'autorifle', itemCategoryHash: ItemCategoryHashes.AutoRifle, subCategories: [kinetic, energy], terminal: true, }, { id: 'handcannon', itemCategoryHash: ItemCategoryHashes.HandCannon, subCategories: [kinetic, energy], terminal: true, }, { id: 'pulserifle', itemCategoryHash: ItemCategoryHashes.PulseRifle, subCategories: [kinetic, energy], terminal: true, }, { id: 'scoutrifle', itemCategoryHash: ItemCategoryHashes.ScoutRifle, subCategories: [kinetic, energy], terminal: true, }, { id: 'sidearm', itemCategoryHash: ItemCategoryHashes.Sidearm, subCategories: [kinetic, energy], terminal: true, }, { id: 'bow', itemCategoryHash: ItemCategoryHashes.Bows, subCategories: [kinetic, energy, power], terminal: true, }, { id: 'submachine', itemCategoryHash: ItemCategoryHashes.SubmachineGuns, subCategories: [kinetic, energy], terminal: true, }, { id: 'fusionrifle', itemCategoryHash: ItemCategoryHashes.FusionRifle, subCategories: [kinetic, energy, power], terminal: true, }, { id: 'sniperrifle', itemCategoryHash: ItemCategoryHashes.SniperRifle, subCategories: [kinetic, energy, power], terminal: true, }, { id: 'shotgun', itemCategoryHash: ItemCategoryHashes.Shotgun, subCategories: [kinetic, energy, power], terminal: true, }, { id: 'specialgrenadelauncher', itemCategoryHash: -ItemCategoryHashes.GrenadeLaunchers, subCategories: [kinetic, energy], terminal: true, }, { id: 'tracerifle', itemCategoryHash: ItemCategoryHashes.TraceRifles, subCategories: [kinetic, energy], terminal: true, }, { id: 'glaive', itemCategoryHash: ItemCategoryHashes.Glaives, subCategories: [kinetic, energy, power], terminal: true, }, { id: 'machinegun', itemCategoryHash: ItemCategoryHashes.MachineGun, terminal: true, }, { id: 'sword', itemCategoryHash: ItemCategoryHashes.Sword, terminal: true, }, { id: 'heavygrenadelauncher', itemCategoryHash: ItemCategoryHashes.GrenadeLaunchers, subCategories: [power], terminal: true, }, { id: 'rocketlauncher', itemCategoryHash: ItemCategoryHashes.RocketLauncher, terminal: true, }, { id: 'linearfusionrifle', itemCategoryHash: ItemCategoryHashes.LinearFusionRifles, subCategories: [kinetic, energy, power], terminal: true, }, ], }, { id: 'hunter', itemCategoryHash: ItemCategoryHashes.Hunter, subCategories: armorCategories, terminal: true, }, { id: 'titan', itemCategoryHash: ItemCategoryHashes.Titan, subCategories: armorCategories, terminal: true, }, { id: 'warlock', itemCategoryHash: ItemCategoryHashes.Warlock, subCategories: armorCategories, terminal: true, }, { id: 'ghosts', itemCategoryHash: ItemCategoryHashes.Ghost, terminal: true, }, ], }; // Each class has the same armor const d1ArmorCategories = [ ...armorCategories, { id: 'artifacts', itemCategoryHash: 38, terminal: true, }, ]; /** * Generate a tree of all the drilldown options for item filtering. This tree is * used to generate the list of selected subcategories. */ const d1SelectionTree: ItemCategoryTreeNode = { id: 'all', itemCategoryHash: 0, subCategories: [ { id: 'weapons', itemCategoryHash: ItemCategoryHashes.Weapon, subCategories: [ { id: 'autorifle', itemCategoryHash: ItemCategoryHashes.AutoRifle, terminal: true, }, { id: 'handcannon', itemCategoryHash: ItemCategoryHashes.HandCannon, terminal: true, }, { id: 'pulserifle', itemCategoryHash: ItemCategoryHashes.PulseRifle, terminal: true, }, { id: 'scoutrifle', itemCategoryHash: ItemCategoryHashes.ScoutRifle, terminal: true, }, { id: 'fusionrifle', itemCategoryHash: ItemCategoryHashes.FusionRifle, terminal: true, }, { id: 'sniperrifle', itemCategoryHash: ItemCategoryHashes.SniperRifle, terminal: true, }, { id: 'shotgun', itemCategoryHash: ItemCategoryHashes.Shotgun, terminal: true, }, { id: 'machinegun', itemCategoryHash: ItemCategoryHashes.MachineGun, terminal: true, }, { id: 'rocketlauncher', itemCategoryHash: ItemCategoryHashes.RocketLauncher, terminal: true, }, { id: 'sidearm', itemCategoryHash: ItemCategoryHashes.Sidearm, terminal: true, }, { id: 'sword', itemCategoryHash: ItemCategoryHashes.Sword, terminal: true, }, ], }, { id: 'hunter', itemCategoryHash: ItemCategoryHashes.Hunter, subCategories: d1ArmorCategories, }, { id: 'titan', itemCategoryHash: ItemCategoryHashes.Titan, subCategories: d1ArmorCategories, }, { id: 'warlock', itemCategoryHash: ItemCategoryHashes.Warlock, subCategories: d1ArmorCategories, }, { id: 'ghosts', itemCategoryHash: ItemCategoryHashes.Ghost, terminal: true, }, ], }; export function getSelectionTree(destinyVersion: DestinyVersion) { return destinyVersion === 2 ? d2SelectionTree : d1SelectionTree; } export const armorTopLevelCatHashes: ItemCategoryHashes[] = [ ItemCategoryHashes.Hunter, ItemCategoryHashes.Titan, ItemCategoryHashes.Warlock, ]; /** * This component offers a means for narrowing down your selection to a single item type * (hunter helmets, hand cannons, etc.) for the Organizer table. */ export default function ItemTypeSelector({ selectionTree, selection, onSelection, }: { selectionTree: ItemCategoryTreeNode; selection: ItemCategoryTreeNode[]; onSelection: (selection: ItemCategoryTreeNode[]) => void; }) { const defs = useDefinitions()!; const filteredItems = useSelector(filteredItemsSelector); selection = selection.length ? selection : [selectionTree]; const handleSelection = (depth: number, subCategory: ItemCategoryTreeNode) => onSelection([...selection.slice(0, depth + 1), subCategory]); return (
{selection.map((currentSelection, depth) => { const upstreamCategories: number[] = []; for (let i = 1; i < depth + 1; i++) { selection[i].itemCategoryHash && upstreamCategories.push(selection[i].itemCategoryHash); } return ( currentSelection.subCategories && (
{currentSelection.subCategories?.map((subCategory) => { const categoryHashList = [...upstreamCategories, subCategory.itemCategoryHash]; // a top level class-specific category implies armor if (armorTopLevelCatHashes.some((h) => categoryHashList.includes(h))) { categoryHashList.push(ItemCategoryHashes.Armor); } const itemCategory = defs.ItemCategory.get(Math.abs(subCategory.itemCategoryHash)); return ( ); })}
) ); })}
); } ================================================ FILE: src/app/organizer/Organizer.m.scss ================================================ @use '../variables.scss' as *; .organizer { width: min-content; :global(.issue-banner-shown) & { @include desktop { padding-bottom: $issue-banner-height; } } :global(#spreadsheets) { max-width: 500px; background-color: #222; padding: 8px; margin-top: 16px; :global(.setting) { margin: 0 0 4px 0; background-color: #222; } label { flex: 1; display: block; margin-right: 1em; } :global(.horizontal) { display: flex; flex-direction: row; margin-bottom: 4px; align-items: center; &:last-child { margin-bottom: 0; } } h2 { text-transform: uppercase; font-size: 16px; font-weight: normal; letter-spacing: 1px; margin: 0 0 4px 0 !important; } } } .noMobile { padding: 2em; font-size: 18px; text-align: center; height: 70vh; box-sizing: border-box; display: flex; align-items: center; } ================================================ FILE: src/app/organizer/Organizer.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'noMobile': string; 'organizer': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/organizer/Organizer.tsx ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { useLoadStores } from 'app/inventory/store/hooks'; import { setSearchQuery } from 'app/shell/actions'; import { querySelector, useIsPhonePortrait } from 'app/shell/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { usePageTitle } from 'app/utils/hooks'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { useLocation, useNavigate } from 'react-router'; import ItemTable from './ItemTable'; import ItemTypeSelector, { ItemCategoryTreeNode, getSelectionTree } from './ItemTypeSelector'; import * as styles from './Organizer.m.scss'; interface Props { account: DestinyAccount; } /** * Given a tree of item categories, and a flat list of item category hashes that * describe a path through that tree, return the nodes from the tree along that * path. */ function drillToSelection( selectionTree: ItemCategoryTreeNode | undefined, selectedItemCategoryHashes: ItemCategoryHashes[], ): ItemCategoryTreeNode[] { const selectedItemCategoryHash = selectedItemCategoryHashes[0]; if ( !selectionTree || selectedItemCategoryHash === undefined || selectionTree.itemCategoryHash !== selectedItemCategoryHash ) { return []; } if (selectionTree.subCategories && selectedItemCategoryHashes.length) { for (const category of selectionTree.subCategories) { const subselection = drillToSelection(category, selectedItemCategoryHashes.slice(1)); if (subselection.length) { return [selectionTree, ...subselection]; } } } return [selectionTree]; } export default function Organizer({ account }: Props) { usePageTitle(t('Organizer.Organizer')); const dispatch = useThunkDispatch(); const isPhonePortrait = useIsPhonePortrait(); const searchQuery = useSelector(querySelector); const storesLoaded = useLoadStores(account); const navigate = useNavigate(); const location = useLocation(); const params = new URLSearchParams(location.search); // Get selected categories from URL const selectedItemCategoryHashes = [ 0, ...(params.get('category') || '').split('~').map((s) => parseInt(s, 10) || 0), ]; const types = getSelectionTree(account.destinyVersion); const selection = drillToSelection(types, selectedItemCategoryHashes); // TODO: useSearchParams? // On the first render, apply the search from the query params if possible. Otherwise, // update the query params with the current search. const firstRender = useRef(true); useEffect(() => { if (!firstRender.current) { searchQuery ? params.set('search', searchQuery) : params.delete('search'); navigate( { ...location, search: params.toString(), }, { replace: true }, ); } else if (params.has('search') && searchQuery !== params.get('search')) { dispatch(setSearchQuery(params.get('search')!)); } firstRender.current = false; // We only want to do this when the search query changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQuery]); // When new item categories are selected, set the URL to the new selection, and // allow the URL to set the state. The URL is our state store, and this means // it's easy to link to a selection or preserve state across reloads. const onSelection = (selection: ItemCategoryTreeNode[]) => { params.set( 'category', selection .slice(1) .map((s) => s.itemCategoryHash) .join('~'), ); navigate( { ...location, search: params.toString(), }, { replace: true }, ); }; if (isPhonePortrait) { return
{t('Organizer.NoMobile')}
; } if (!storesLoaded) { return ; } return (
); } ================================================ FILE: src/app/organizer/table-types.ts ================================================ import { SortDirection } from 'app/dim-ui/table-columns'; import { DimItem } from 'app/inventory/item-types'; import { CsvValue } from 'app/utils/csv'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import React from 'react'; export { SortDirection, type ColumnSort } from 'app/dim-ui/table-columns'; export type Value = string | number | boolean | undefined; /** * Columns can optionally belong to a column group - if so, they're shown/hidden as a group. */ export interface ColumnGroup { id: string; header: React.ReactNode; dropdownLabel?: React.ReactNode; } export type CSVColumn = [name: string, value: CsvValue]; // TODO: column groupings? // TODO: custom configs like the total column? // prop methods make this invariant over V, so disable the rule here /* eslint-disable @typescript-eslint/method-signature-style */ export interface ColumnDefinition { /** Unique ID for this column. */ id: string; /** Whether to start with descending or ascending sort. Default: 'asc' */ defaultSort?: SortDirection; /** This column can't be hidden. Default: false (column can be hidden). */ noHide?: boolean; /** This column can't be sorted. Default: false (column can be sorted). */ noSort?: boolean; /** A CSS grid expression for the width of the cell. Default: min-content. */ gridWidth?: string; /** Header renderer */ header: React.ReactNode; /** * If the column-toggler dropdown needs a more verbose label when the header is only an icon `e.g.Power or Locked`, that label content goes here */ dropdownLabel?: React.ReactNode; /** Columns can optionally belong to a column group - if so, they're shown/hidden as a group. */ columnGroup?: ColumnGroup; /** The raw value of the column for this item. */ value(item: DimItem): V; /** Renderer for the cell. Default: value. If the value is numeric we may pass max and min in. */ cell?(value: V, item: DimItem, context?: { max?: number; min?: number }): React.ReactNode; /** A generator for search terms matching this item. Default: No filtering. */ filter?(value: V, item: DimItem): string | undefined; /** A custom sort function. Default: Something reasonable. */ sort?( this: void, firstValue: V, secondValue: V, firstItem: DimItem, secondItem: DimItem, ): 0 | 1 | -1; /** * a column def needs to exist all the time, so enabledness setting is aware of it, * but sometimes a custom stat should be limited to only displaying for a certain class */ limitToClass?: DestinyClass; /** An optional class name to apply to the cell. */ className?: string; /** An optional class name to apply to the header. */ headerClassName?: string; /** * A name for this column when it is output as CSV. This will reuse the value * function as-is. We could reuse the header, but that's localized, while * historically our CSV column names haven't been. * * Alternately, provide a function to override both the column name and the * value, or emit multiple columns at once. This is mostly to achieve * compatibility with the existing CSV format, but sometimes it's used to * output complex data for CSV. For example, perks are output as multiple * columns. */ csv?: | string | { bivarianceHack(value: V, item: DimItem, spreadsheetContext: SpreadsheetContext): CSVColumn; }['bivarianceHack']; } export interface SpreadsheetContext { storeNamesById: { [key: string]: string }; } /** * A row is the calculated values for a single item, for all columns. */ export interface Row { item: DimItem; values: { [columnId: string]: Value }; } /** * Additional context about the table as a whole (all the rows in it.) */ export interface TableContext { /** * For numeric-valued columns, the min and max values across all rows. */ minMaxValues: { [columnId: string]: { max: number; min: number } | undefined }; } export type ColumnWithStat = ColumnDefinition & { statHash: number }; ================================================ FILE: src/app/progress/ActivityModifier.m.scss ================================================ .modifier { display: flex; flex-direction: row; align-items: center; cursor: pointer; margin-bottom: 2px; img { width: 18px; height: 18px; margin: 0 6px 0 2px; } &.small { opacity: 0.5; margin-bottom: 0; } } ================================================ FILE: src/app/progress/ActivityModifier.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'modifier': string; 'small': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/ActivityModifier.tsx ================================================ import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { useD2Definitions } from 'app/manifest/selectors'; import clsx from 'clsx'; import BungieImage from '../dim-ui/BungieImage'; import { PressTip } from '../dim-ui/PressTip'; import * as styles from './ActivityModifier.m.scss'; export function ActivityModifier({ modifierHash, small, }: { modifierHash: number; small?: boolean; }) { const defs = useD2Definitions()!; const modifier = defs.ActivityModifier.get(modifierHash); const modifierName = modifier.displayProperties.name; const modifierIcon = modifier.displayProperties.icon; if (!modifier?.displayInActivitySelection) { return null; } return (
{Boolean(modifierIcon) && } {Boolean(modifierName) && ( }>
{modifierName}
)}
); } ================================================ FILE: src/app/progress/BountyGuide.m.scss ================================================ @use '../variables.scss' as *; .guide { composes: flexRow from '../dim-ui/common.m.scss'; flex-wrap: wrap; margin: 8px 0; gap: 4px; @include phone-portrait { margin: 8px 6px; } } .pill { composes: pill from '../dim-ui/FilterPills.m.scss'; } .invert { filter: invert(1); } .count { opacity: 0.5; } .synergy { border-color: #ccc; } .selected { border-color: var(--theme-accent-primary); background: rgb(0, 0, 0, 0.6); } .pullItem { display: inline-block; margin-left: 6px; margin-right: -2px; padding-left: 6px; border-left: 1px solid rgb(255, 255, 255, 0.2); @include interactive($hover: true) { :global(.app-icon) { transform: scale(1.3); } } :global(.app-icon) { transition: transform 150ms ease-in-out; } } ================================================ FILE: src/app/progress/BountyGuide.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'count': string; 'guide': string; 'invert': string; 'pill': string; 'pullItem': string; 'selected': string; 'synergy': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/BountyGuide.tsx ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import BungieImage from 'app/dim-ui/BungieImage'; import BucketIcon from 'app/dim-ui/svgs/BucketIcon'; import { I18nKey, t, tl } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { moveItemTo } from 'app/inventory/move-item'; import { DimStore } from 'app/inventory/store-types'; import { useItemPicker } from 'app/item-picker/item-picker'; import { useD2Definitions } from 'app/manifest/selectors'; import { AppIcon, addIcon } from 'app/shell/icons'; import { ThunkDispatchProp } from 'app/store/types'; import { chainComparator, compareBy, reverseComparator } from 'app/utils/comparators'; import { itemCanBeEquippedBy } from 'app/utils/item-utils'; import { LookupTable, isIn } from 'app/utils/util-types'; import { DestinyDisplayPropertiesDefinition } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { TraitHashes } from 'data/d2/generated-enums'; import grenade from 'destiny-icons/weapons/grenade.svg'; import headshot from 'destiny-icons/weapons/headshot.svg'; import melee from 'destiny-icons/weapons/melee.svg'; import React from 'react'; import { useDispatch } from 'react-redux'; import * as styles from './BountyGuide.m.scss'; import { xpItems } from './xp'; const enum KillType { Melee, Super, Grenade, Finisher, Precision, ClassAbilities, } const killTypeIcons: LookupTable = { [KillType.Melee]: melee, [KillType.Grenade]: grenade, [KillType.Precision]: headshot, }; const killTypeDescriptions: Record = { [KillType.Melee]: tl('KillType.Melee'), [KillType.Super]: tl('KillType.Super'), [KillType.Grenade]: tl('KillType.Grenade'), [KillType.Finisher]: tl('KillType.Finisher'), [KillType.Precision]: tl('KillType.Precision'), [KillType.ClassAbilities]: tl('KillType.ClassAbilities'), }; export type DefType = | 'ActivityMode' | 'Destination' | 'DamageType' | 'ItemCategory' | 'KillType' | 'Reward' | 'QuestTrait'; const pursuitCategoryTraitHashes: TraitHashes[] = [ TraitHashes.Seasonal_Quests, TraitHashes.TheFinalShape, TraitHashes.Exotics, TraitHashes.Playlists, TraitHashes.ThePast, ]; // Reward types we'll show in the bounty guide. Could be expanded (e.g. to seasonal mats) const rewardAllowList = [ ...Object.keys(xpItems).map((i) => parseInt(i, 10)), 2817410917, // InventoryItem "Bright Dust" 3168101969, // InventoryItem "Bright Dust" ]; export interface BountyFilter { type: DefType; hash: number; } /** * This provides a little visual guide to what bounties you have - specifically, what weapons/activities/locations are required for your bounties. * * This is meant to be usable in both the Progress page and Active Mode. */ export default function BountyGuide({ store, bounties, selectedFilters, onSelectedFiltersChanged, pursuitsInfo, }: { store: DimStore; bounties: DimItem[]; selectedFilters: BountyFilter[]; onSelectedFiltersChanged: (filters: BountyFilter[]) => void; pursuitsInfo: { [hash: string]: { [type in DefType]?: number[] } }; }) { const defs = useD2Definitions()!; const dispatch = useDispatch(); const showItemPicker = useItemPicker(); const pullItemCategory = async (e: React.MouseEvent, itemCategory: number) => { e.stopPropagation(); const bucket = defs.ItemCategory.get(itemCategory)?.displayProperties.name; const item = await showItemPicker({ filterItems: (item) => item.itemCategoryHashes.includes(itemCategory) && itemCanBeEquippedBy(item, store), prompt: t('MovePopup.PullItem', { bucket, store: store.name, }), }); if (item) { await dispatch(moveItemTo(item, store)); } }; const mapped: { [type in DefType]: { [key: number]: DimItem[] } } = { ActivityMode: {}, Destination: {}, DamageType: {}, ItemCategory: {}, KillType: {}, Reward: {}, QuestTrait: {}, }; for (const i of bounties) { const expired = i.pursuit?.expiration ? i.pursuit.expiration.expirationDate.getTime() < Date.now() : false; if (!i.complete && !expired) { const info = pursuitsInfo[i.hash]; if (info) { for (const key in info) { const infoKey = key as keyof typeof info; const values = info[infoKey]; if (values) { for (const value of values) { (mapped[infoKey][value] ??= []).push(i); } } } } if (i.pursuit) { for (const reward of i.pursuit.rewards) { if (rewardAllowList.includes(reward.itemHash)) { (mapped.Reward[reward.itemHash] ??= []).push(i); } } } // Don't look up InventoryItem for "items" that were created from Records. if (!i.pursuit?.recordHash) { const traitHashes = defs.InventoryItem.get(i.hash)?.traitHashes; if (traitHashes) { for (const traitHash of traitHashes) { if (pursuitCategoryTraitHashes.includes(traitHash)) { (mapped.QuestTrait[traitHash] ??= []).push(i); } } } } } } const flattened = Object.entries(mapped).flatMap(([type, mapping]) => Object.entries(mapping).map(([value, bounties]) => ({ type: type as DefType, value: parseInt(value, 10), bounties, })), ); if (flattened.length === 0) { return null; } flattened.sort(chainComparator(reverseComparator(compareBy((f) => f.bounties.length)))); const onClickPill = (e: React.MouseEvent, type: DefType, value: number) => { e.stopPropagation(); const match = (f: BountyFilter) => f.type === type && f.hash === value; if (e.shiftKey) { const existing = selectedFilters.find(match); if (existing) { onSelectedFiltersChanged(selectedFilters.filter((f) => !match(f))); } else { onSelectedFiltersChanged([...selectedFilters, { type, hash: value }]); } } else if (selectedFilters.length > 1 || !selectedFilters.some(match)) { onSelectedFiltersChanged([{ type, hash: value }]); } else { onSelectedFiltersChanged([]); } }; const clearSelection = (e: React.MouseEvent) => { e.stopPropagation(); onSelectedFiltersChanged([]); }; return (
{flattened.map(({ type, value, bounties }) => ( ))}
); } function contentFromDisplayProperties( { displayProperties, }: { displayProperties: DestinyDisplayPropertiesDefinition; }, hideIcon?: boolean, ) { return ( <> {displayProperties.hasIcon && !hideIcon && ( )} {displayProperties.name} ); } function PillContent({ type, defs, value, }: { type: DefType; defs: D2ManifestDefinitions; value: number; }) { switch (type) { case 'ActivityMode': case 'Destination': case 'DamageType': return contentFromDisplayProperties(defs[type].get(value)); case 'ItemCategory': return ( <> {defs.ItemCategory.get(value)?.displayProperties.name} ); case 'KillType': return ( <> {isIn(value, killTypeIcons) && ( )} {t(killTypeDescriptions[value as KillType])} ); case 'Reward': return contentFromDisplayProperties(defs.InventoryItem.get(value)); case 'QuestTrait': return contentFromDisplayProperties( defs.Trait.get(value), // the seasonal quest trait has the Season of the Lost icon? /* hideIcon */ value === TraitHashes.Seasonal_Quests, ); } } function matchPill(type: DefType, hash: number, filters: BountyFilter[]) { return filters.some((f) => f.type === type && f.hash === hash); } /** * Returns true if the filter list is empty, or if the item matches *any* of the provided filters ("or"). */ export function matchBountyFilters( defs: D2ManifestDefinitions, item: DimItem, filters: BountyFilter[], pursuitsInfo: { [hash: string]: { [type in DefType]?: number[] } }, ) { if (filters.length === 0) { return true; } const info = pursuitsInfo[item.hash]; for (const filter of filters) { if (filter.type === 'Reward') { return item.pursuit?.rewards.some((r) => r.itemHash === filter.hash); } else if (filter.type === 'QuestTrait') { return defs.InventoryItem.get(item.hash)?.traitHashes?.includes(filter.hash); } else if (info?.[filter.type]?.includes(filter.hash)) { return true; } } return false; } ================================================ FILE: src/app/progress/Event.m.scss ================================================ .noRecords { padding: 1em; } ================================================ FILE: src/app/progress/Event.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'noRecords': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/Event.tsx ================================================ import { trackedTriumphsSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { InventoryBuckets } from 'app/inventory/inventory-buckets'; import { profileResponseSelector } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { toRecord } from 'app/records/presentation-nodes'; import { filterMap } from 'app/utils/collections'; import { DestinyEventCardDefinition, DestinyPresentationNodeState, DestinyRecordState, } from 'bungie-api-ts/destiny2'; import { useSelector } from 'react-redux'; import * as styles from './Event.m.scss'; import Pursuit from './Pursuit'; import PursuitGrid from './PursuitGrid'; import { sortPursuits } from './Pursuits'; import { recordToPursuitItem } from './milestone-items'; /** * A component for showing objectives of seasonal events v2, * the format with event cards introduced in Solstice 2022. */ export function Event({ card, store, buckets, }: { card: DestinyEventCardDefinition; store: DimStore; buckets: InventoryBuckets; }) { const defs = useD2Definitions()!; const profileResponse = useSelector(profileResponseSelector)!; const trackedRecords = useSelector(trackedTriumphsSelector); const challengesRootNode = defs.PresentationNode.get(card.triumphsPresentationNodeHash); const childrenNodes = challengesRootNode.children.presentationNodes; const classSpecificNodeHash = childrenNodes.length === 1 ? // If we only have one node, it's probably the right node. childrenNodes[0] : // This is for Solstice, which has three different nodes for the three characters. // The PresentationNodes component makes two of them invisible per character and one // stays visible, so find the one that's actually visible. childrenNodes.find((node) => { const relevantNodeInfo = profileResponse.characterPresentationNodes?.data?.[store.id]?.nodes[ node.presentationNodeHash ]; return ( relevantNodeInfo && (relevantNodeInfo.state & DestinyPresentationNodeState.Invisible) === 0 ); }); const classSpecificNode = classSpecificNodeHash && defs.PresentationNode.get(classSpecificNodeHash.presentationNodeHash); const presentationNodes = classSpecificNode ? [classSpecificNode] : childrenNodes.map((n) => defs.PresentationNode.get(n.presentationNodeHash)); const records = presentationNodes.flatMap((n) => filterMap(n.children.records, (h) => toRecord(defs, profileResponse, h.recordHash)), ); const pursuits = records .filter((r) => { // Don't show records that have been redeemed const state = r.recordComponent.state; const acquired = Boolean(state & DestinyRecordState.RecordRedeemed); return !acquired; }) .map((r) => recordToPursuitItem( r, buckets, store, card.displayProperties.name, trackedRecords.includes(r.recordDef.hash), ), ); if (!pursuits.length) { return
{t('Progress.NoEventChallenges')}
; } return ( {pursuits.sort(sortPursuits).map((item) => ( ))} ); } ================================================ FILE: src/app/progress/FactionIcon.m.scss ================================================ .factionIcon { margin: 2px 14px -2px 2px; position: relative; display: inline-block; width: var(--item-size); height: var(--item-size); } ================================================ FILE: src/app/progress/FactionIcon.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'factionIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/FactionIcon.tsx ================================================ import { DestinyFactionDefinition, DestinyProgression, DestinyVendorComponent, } from 'bungie-api-ts/destiny2'; import { bungieNetPath } from '../dim-ui/BungieImage'; import DiamondProgress from '../dim-ui/DiamondProgress'; import * as styles from './FactionIcon.m.scss'; export default function FactionIcon({ factionProgress, factionDef, vendor, }: { factionProgress: DestinyProgression; factionDef: DestinyFactionDefinition; vendor?: DestinyVendorComponent; }) { const level = (vendor?.progression?.level ?? factionProgress.level) + 1; return ( ); } ================================================ FILE: src/app/progress/Milestones.m.scss ================================================ .header { margin-top: 1.5em !important; margin-bottom: 1em; } ================================================ FILE: src/app/progress/Milestones.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'header': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/Milestones.tsx ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { InventoryBuckets } from 'app/inventory/inventory-buckets'; import { DimStore } from 'app/inventory/store-types'; import { dropPowerLevelSelector } from 'app/inventory/store/selectors'; import { useD2Definitions } from 'app/manifest/selectors'; import { uniqBy } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { DestinyMilestone, DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import { useSelector } from 'react-redux'; import * as styles from './Milestones.m.scss'; import Pursuit from './Pursuit'; import PursuitGrid from './PursuitGrid'; import { sortPursuits } from './Pursuits'; import { getEngramPowerBonus } from './engrams'; import { milestoneToItems } from './milestone-items'; import { getCharacterProgressions } from './selectors'; const sortPowerBonus = compareBy((powerBonus: number | undefined) => -(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. */ export default function Milestones({ profileInfo, store, buckets, }: { store: DimStore; profileInfo: DestinyProfileResponse; buckets: InventoryBuckets; }) { const defs = useD2Definitions()!; const profileMilestones = milestonesForProfile(defs, profileInfo, store.id); const dropPower = useSelector(dropPowerLevelSelector(store.id)); const milestoneItems = uniqBy( [...milestonesForCharacter(defs, profileInfo, store), ...profileMilestones], (m) => m.milestoneHash, ).flatMap((milestone) => milestoneToItems(milestone, defs, buckets, store)); const milestonesByPower = Map.groupBy(milestoneItems, (m) => { for (const reward of m.pursuit?.rewards ?? []) { const [powerBonus] = getEngramPowerBonus(reward.itemHash, dropPower, m.hash); if (powerBonus !== undefined) { return powerBonus; } } }); return ( <> {[...milestonesByPower.keys()].sort(sortPowerBonus).map((powerBonus) => (

{powerBonus === undefined ? t('Progress.PowerBonusHeaderUndefined') : t('Progress.PowerBonusHeader', { powerBonus })}

{milestonesByPower .get(powerBonus)! .sort(sortPursuits) .map((item) => ( ))}
))} ); } /** * Get all the milestones that are valid across the whole profile. This still requires a character (any character) * to look them up, and the assumptions underlying this may get invalidated as the game evolves. */ function milestonesForProfile( defs: D2ManifestDefinitions, profileInfo: DestinyProfileResponse, characterId: string, ): DestinyMilestone[] { const profileMilestoneData = profileInfo.characterProgressions?.data?.[characterId]?.milestones; const allMilestones: DestinyMilestone[] = profileMilestoneData ? Object.values(profileMilestoneData) : []; const filteredMilestones = allMilestones.filter( (milestone) => !milestone.availableQuests?.length && !milestone.activities?.length && (!milestone.vendors?.length || Boolean(milestone.rewards?.length)) && defs.Milestone.get(milestone.milestoneHash), ); return filteredMilestones.sort(compareBy((milestone) => milestone.order)); } /** * Get all the milestones to show for a particular character, filtered to active milestones and sorted. */ function milestonesForCharacter( defs: D2ManifestDefinitions, profileInfo: DestinyProfileResponse, character: DimStore, ): DestinyMilestone[] { const characterMilestoneData = getCharacterProgressions(profileInfo, character.id)?.milestones; const allMilestones: DestinyMilestone[] = characterMilestoneData ? Object.values(characterMilestoneData) : []; const filteredMilestones = allMilestones.filter((milestone) => { const def = defs.Milestone.get(milestone.milestoneHash); return ( def && (def.showInExplorer || def.showInMilestones) && (Boolean(milestone.activities?.length) || !milestone.availableQuests?.length || milestone.availableQuests.every( (q) => q.status.stepObjectives.length > 0 && q.status.started && (!q.status.completed || !q.status.redeemed), )) ); }); return filteredMilestones.sort(compareBy((milestone) => milestone.order)); } ================================================ FILE: src/app/progress/Objective.m.scss ================================================ @use '../variables.scss' as *; .progressContainer { // Include the global name for now so other things can style it. composes: objective-progress from global; flex: 1; background-color: var(--objective-background-color, #333); position: relative; min-height: 17px; display: flex; flex-direction: row; align-items: center; // If this is a date-time objective, don't show the background &:has(time) { background-color: transparent; } } .description { position: relative; // so it z-stacks over the progress bar display: flex; flex-direction: row; align-items: center; padding: 0 4px; flex: 1; text-shadow: 1px 1px 3px rgb(0, 0, 0, 0.25); } .text { position: relative; // so it z-stacks over the progress bar margin-right: 4px; text-shadow: 1px 1px 3px rgb(0, 0, 0, 0.25); } .objective { display: flex; flex-direction: row; margin-bottom: 4px; // TODO: use flex containers instead - ObjectiveGroup? &:last-child { margin-bottom: 0; } &.objectiveComplete { --objective-background-color: #222; opacity: 0.5; } &.boolean { --objective-background-color: transparent; .description { padding: 0; } } } .counter { height: 17px; margin-left: 4px; margin-right: 2px; text-align: center; box-sizing: border-box; } .integer { flex: 1; position: relative; display: flex; flex-direction: row; align-items: center; color: var(--theme-text); img { width: 16px; height: 16px; margin-right: 8px; } } .passageFlawed::after { content: '\274c'; position: relative; top: -1px; left: -0.2px; } .checkbox { width: 17px; height: 17px; border: 1px solid #999; margin-right: 4px; box-sizing: border-box; background-color: rgb(0, 0, 0, 0.3); &.complete::after { content: ''; display: block; margin: 2px; height: calc(100% - 4px); width: calc(100% - 4px); background-color: var(--objective-checkbox-color, $xp); } } .progressBar { background-color: var(--objective-progress-color, $xp); position: absolute; top: 0; left: 0; bottom: 0; } ================================================ FILE: src/app/progress/Objective.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'boolean': string; 'checkbox': string; 'complete': string; 'counter': string; 'description': string; 'integer': string; 'objective': string; 'objectiveComplete': string; 'passageFlawed': string; 'progressBar': string; 'progressContainer': string; 'text': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/Objective.tsx ================================================ import { D1ObjectiveDefinition, D1ObjectiveProgress } from 'app/destiny1/d1-manifest-types'; import BungieImage from 'app/dim-ui/BungieImage'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { t } from 'app/i18next-t'; import { getValueStyle, isBooleanObjective, isFlawlessObjective, isObjectiveWithPlaceholderGoal, isRoundsWonObjective, } from 'app/inventory/store/objectives'; import { useDefinitions } from 'app/manifest/selectors'; import { percent, percentWithSingleDecimal } from 'app/shell/formatters'; import { timerDurationFromMs } from 'app/utils/time'; import { DestinyObjectiveDefinition, DestinyObjectiveProgress, DestinyUnlockValueUIStyle, } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import React from 'react'; import * as styles from './Objective.m.scss'; /** * Display an Objective given Destiny Objective information. This will figure * out the right way to display it. If you know exactly how you want to display * something you can use the lower-level ObjectiveDisplay component. */ export default function Objective({ objective, suppressObjectiveDescription, isTrialsPassage, showHidden, noCheckbox, }: { objective: DestinyObjectiveProgress | D1ObjectiveProgress; suppressObjectiveDescription?: boolean; isTrialsPassage?: boolean; showHidden?: boolean; noCheckbox?: boolean; }) { const defs = useDefinitions()!; const objectiveDef = defs.Objective.get(objective.objectiveHash); const progress = objective.progress || 0; if (!objectiveDef) { return null; } if ( 'minimumVisibilityThreshold' in objectiveDef && objectiveDef.minimumVisibilityThreshold > 0 && progress < objectiveDef.minimumVisibilityThreshold ) { return null; } // These two are to support D1 objectives const completionValue = 'completionValue' in objective ? objective.completionValue : objectiveDef.completionValue; const complete = 'complete' in objective ? objective.complete : objective.isComplete; const isD2Def = 'progressDescription' in objectiveDef; const progressDescription = // D1 display description (!isD2Def && objectiveDef.displayDescription) || (!suppressObjectiveDescription && isD2Def && objectiveDef.progressDescription) || (complete ? t('Objectives.Complete') : t('Objectives.Incomplete')); const valueStyle = getValueStyle(objectiveDef, progress, completionValue); if (!showHidden && valueStyle === DestinyUnlockValueUIStyle.Hidden) { return null; } const isDate = isD2Def && valueStyle === DestinyUnlockValueUIStyle.DateTime; if (valueStyle === DestinyUnlockValueUIStyle.Integer) { const icon = objectiveDef && 'displayProperties' in objectiveDef && objectiveDef.displayProperties.hasIcon ? objectiveDef.displayProperties.icon : undefined; return (
{progress.toLocaleString()}
); } const isBoolean = isBooleanObjective(objectiveDef, progress, completionValue); const showAsCounter = isTrialsPassage && isRoundsWonObjective(objective.objectiveHash); const passageFlawed = isTrialsPassage && isFlawlessObjective(objective.objectiveHash) && !complete; // TODO: green pips, red pips const showCheckbox = !noCheckbox && !showAsCounter && !isDate; const showProgress = progress !== undefined && completionValue !== undefined && !isBoolean && !isDate; const showComplete = complete && !showAsCounter && !isDate; return ( {showCheckbox && ( )} {showProgress && !showComplete && ( )} {!isBoolean && ( )} {showAsCounter &&
{progress}
}
); } /** * An ObjectiveRow is the top-level container for a single objective display. */ export function ObjectiveRow({ complete, boolean, children, className, }: { complete?: boolean; boolean?: boolean; children?: React.ReactNode; className?: string; }) { const classes = clsx(className, styles.objective, { [styles.objectiveComplete]: complete, [styles.boolean]: boolean, }); return
{children}
; } /** * This is the little checkbox that's shown next to an objective. */ export function ObjectiveCheckbox({ completed, passageFlawed, }: { completed: boolean; passageFlawed?: boolean; }) { return (
); } /** * This is the progress bar that's shown behind objectives that have progress. */ export function ObjectiveProgressBar({ progress, completionValue, className, }: { progress: number; completionValue: number; className?: string; }) { return (
); } /** * This is the container component for the progress bar and description. */ export function ObjectiveProgress({ children }: { children: React.ReactNode }) { return
{children}
; } /** * This is the description of the objective, e.g. "kill enemies" */ export function ObjectiveDescription({ icon, description, }: { icon?: string | React.ReactNode; description: string; }) { return (
{icon && typeof icon === 'string' ? : icon}
); } /** * The text at the end of the objective, usually this shows the value of a counter (e.g. 4/10) */ export function ObjectiveText({ children }: { children: React.ReactNode }) { return
{children}
; } /** * This is the formatted value of the objective, e.g. "5/10". It depends on the * value style of the objective. */ export function ObjectiveValue({ objectiveDef, progress, completionValue = 0, }: { objectiveDef: DestinyObjectiveDefinition | D1ObjectiveDefinition | undefined; progress: number; completionValue?: number; }) { const valueStyle = getValueStyle(objectiveDef, progress, completionValue); // TODO: pips switch (valueStyle) { case DestinyUnlockValueUIStyle.DateTime: return ( ); case DestinyUnlockValueUIStyle.Percentage: if (completionValue === 100) { return <>{percent(progress / completionValue)}; } else if (completionValue === 1000) { return <>{percentWithSingleDecimal(progress / completionValue)}; } break; case DestinyUnlockValueUIStyle.ExplicitPercentage: return <>{`${progress}%`}; case DestinyUnlockValueUIStyle.FractionFloat: return <>{percent(progress * completionValue)}; case DestinyUnlockValueUIStyle.Multiplier: return <>{`${progress.toLocaleString()}𝗑`}; case DestinyUnlockValueUIStyle.RawFloat: return <>{(progress / 100).toLocaleString()}; case DestinyUnlockValueUIStyle.TimeDuration: return <>{timerDurationFromMs(progress)}; case DestinyUnlockValueUIStyle.Checkbox: // This was already rendered as a checkbox case DestinyUnlockValueUIStyle.Hidden: return null; default: break; } // Default return completionValue === 0 || isObjectiveWithPlaceholderGoal(objectiveDef, completionValue) ? ( <>{progress.toLocaleString()} ) : ( <> {progress.toLocaleString()} / {completionValue.toLocaleString()} ); } ================================================ FILE: src/app/progress/Pathfinder.m.scss ================================================ @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 { --item-size: 35px; composes: flexColumn from '../dim-ui/common.m.scss'; box-sizing: border-box; gap: 8px; flex: 1; flex-direction: column-reverse; justify-content: center; max-width: 250px; min-width: 120px; @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; } } @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) { --item-size: 25px; flex-direction: row !important; 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%); } } :global(.milestone-quest) { align-items: center; background: rgb(255, 255, 255, 0.05); padding: 8px; width: 100%; } } .completed { :global(.milestone-icon) img { border-color: $xp; border-width: 2px; padding: 3px; } } ================================================ FILE: src/app/progress/Pathfinder.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'completed': string; 'pathfinderRow': string; 'pathfinderTree': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/Pathfinder.tsx ================================================ 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 * as 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) => ( ))}
))}
); } ================================================ FILE: src/app/progress/Progress.m.scss ================================================ @use '../variables.scss' as *; .progress { --item-size: 50px; } .menuLinks { // Give some space between the character selector and the links margin-top: 16px; } :global(#ranks) { :global(.milestone-quest) { @include phone-portrait { flex-direction: column; align-items: center; text-align: center; } } } ================================================ FILE: src/app/progress/Progress.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'menuLinks': string; 'progress': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/Progress.tsx ================================================ import CharacterSelect from 'app/dim-ui/CharacterSelect'; import PageWithMenu from 'app/dim-ui/PageWithMenu'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { bucketsSelector, profileResponseSelector, sortedStoresSelector, } from 'app/inventory/selectors'; import { useLoadStores } from 'app/inventory/store/hooks'; import { getCurrentStore, getStore } from 'app/inventory/stores-helpers'; import { destiny2CoreSettingsSelector, useD2Definitions } from 'app/manifest/selectors'; import { RAID_NODE } from 'app/search/d2-known-values'; import { querySelector, useIsPhonePortrait } from 'app/shell/selectors'; import { compact } from 'app/utils/collections'; import { usePageTitle } from 'app/utils/hooks'; import { PanInfo, motion } from 'motion/react'; import { useState } from 'react'; import { useSelector } from 'react-redux'; import { DestinyAccount } from '../accounts/destiny-account'; 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 * as styles from './Progress.m.scss'; import Pursuits from './Pursuits'; import Raids from './Raids'; import Ranks from './Ranks'; import SeasonalChallenges from './SeasonalChallenges'; import SeasonalRank from './SeasonalRank'; import { TrackedTriumphs } from './TrackedTriumphs'; import WellRestedPerkIcon from './WellRestedPerkIcon'; export default function Progress({ account }: { account: DestinyAccount }) { const defs = useD2Definitions(); const isPhonePortrait = useIsPhonePortrait(); const stores = useSelector(sortedStoresSelector); const buckets = useSelector(bucketsSelector); const profileInfo = useSelector(profileResponseSelector); const searchQuery = useSelector(querySelector); const coreSettings = useSelector(destiny2CoreSettingsSelector); usePageTitle(t('Progress.Progress')); const [selectedStoreId, setSelectedStoreId] = useState(undefined); const storesLoaded = useLoadStores(account); if (!defs || !profileInfo || !storesLoaded) { return ; } // TODO: make milestones and pursuits look similar? // TODO: search/filter by activity // TODO: dropdowns for searches (reward, activity) const handleSwipe = (_e: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { // Velocity is in px/ms if (Math.abs(info.offset.x) < 10 || Math.abs(info.velocity.x) < 300) { return; } const direction = -Math.sign(info.velocity.x); const characters = stores.filter((s) => !s.isVault); const selectedStoreIndex = selectedStoreId ? characters.findIndex((s) => s.id === selectedStoreId) : characters.findIndex((s) => s.current); if (direction > 0 && selectedStoreIndex < characters.length - 1) { setSelectedStoreId(characters[selectedStoreIndex + 1].id); } else if (direction < 0 && selectedStoreIndex > 0) { setSelectedStoreId(characters[selectedStoreIndex - 1].id); } }; const selectedStore = selectedStoreId ? getStore(stores, selectedStoreId)! : getCurrentStore(stores)!; if (!buckets) { return null; } const raidNode = defs.PresentationNode.get(RAID_NODE); const raidTitle = raidNode?.displayProperties.name; const eventCardHash = profileInfo.profile.data?.activeEventCardHash; const eventCard = eventCardHash !== undefined && defs.EventCard.get(eventCardHash); const seasonalChallengesPresentationNode = coreSettings?.seasonalChallengesPresentationNodeHash !== undefined && defs.PresentationNode.get(coreSettings.seasonalChallengesPresentationNodeHash); const paleHeartPathfinderNode = defs.PresentationNode.get(1062988660); const menuItems = compact([ { id: 'ranks', title: t('Progress.CrucibleRank') }, { id: 'trackedTriumphs', title: t('Progress.TrackedTriumphs') }, eventCard && { id: 'event', title: eventCard.displayProperties.name || t('Progress.SeasonalHub'), }, { id: 'milestones', title: t('Progress.Milestones') }, paleHeartPathfinderNode && { id: 'paleHeartPathfinder', title: t('Progress.PaleHeartPathfinder'), }, 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 }, ]); return ( {selectedStore && ( )} {!isPhonePortrait && (
{menuItems.map((menuItem) => ( {menuItem.title} ))}
)}
{eventCard && (
)}
{paleHeartPathfinderNode && ( )} {seasonalChallengesPresentationNode && ( )} {raidNode && (
)}
); } ================================================ FILE: src/app/progress/Pursuit.tsx ================================================ import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import ItemPopupTrigger from 'app/inventory/ItemPopupTrigger'; import { DimItem } from 'app/inventory/item-types'; import { isNewSelector } from 'app/inventory/selectors'; import { isBooleanObjective } from 'app/inventory/store/objectives'; import ItemExpiration from 'app/item-popup/ItemExpiration'; import { useD2Definitions } from 'app/manifest/selectors'; import { searchFilterSelector } from 'app/search/items/item-search-filter'; import { percent } from 'app/shell/formatters'; import { RootState } from 'app/store/types'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; import { ObjectiveValue } from './Objective'; import PursuitItem from './PursuitItem'; /** * A Pursuit is an inventory item that represents a bounty or quest. This displays * a pursuit tile for the Progress page. */ export default function Pursuit({ item, searchHidden: alreadySearchHidden, className, }: { item: DimItem; searchHidden?: boolean; className?: string; }) { const defs = useD2Definitions()!; const isNew = useSelector(isNewSelector(item)); const searchHidden = useSelector( (state: RootState) => alreadySearchHidden || !searchFilterSelector(state)(item), ); const expired = showPursuitAsExpired(item); const objectives = item.objectives || []; const firstObjective = objectives.length > 0 ? objectives[0] : undefined; const firstObjectiveDef = firstObjective && defs.Objective.get(firstObjective.objectiveHash); const isBoolean = firstObjective && firstObjectiveDef && isBooleanObjective(firstObjectiveDef, firstObjective.progress, firstObjective.completionValue); const showObjectiveDetail = objectives.length === 1 && !isBoolean; const showObjectiveProgress = objectives.length > 1 || (objectives.length === 1 && !isBoolean); return ( {(ref, onClick) => ( )} ); } /** * Should this item be displayed as expired (no longer completable)? */ export function showPursuitAsExpired(item: DimItem) { if (!item.pursuit?.expiration) { return false; } // Suppress description when expiration is shown const suppressExpiration = item.pursuit.expiration.suppressExpirationWhenObjectivesComplete && item.complete; return !suppressExpiration && item.pursuit.expiration.expirationDate.getTime() < Date.now(); } ================================================ FILE: src/app/progress/PursuitGrid.m.scss ================================================ @use '../variables.scss' as *; .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); grid-auto-rows: min-content; gap: 24px 16px; margin: 16px 0; width: 100%; box-sizing: border-box; @include phone-portrait { margin: 16px 10px; gap: 16px; width: auto; &.ranks { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); } } } ================================================ FILE: src/app/progress/PursuitGrid.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'grid': string; 'ranks': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/PursuitGrid.tsx ================================================ import clsx from 'clsx'; import * as styles from './PursuitGrid.m.scss'; /** * A grid of pursuits or milestones for the Progress page. Used for the * Milestones, Bounties, Quests, Raids and Ranks sections, etc. For the bordered * Triumph style use Records. */ export default function PursuitGrid({ ranks, children, }: { /** Is this the "Ranks" section? It gets slightly different styling on mobile. */ ranks?: boolean; children: React.ReactNode; }) { return
{children}
; } ================================================ FILE: src/app/progress/PursuitItem.m.scss ================================================ @use '../variables.scss' as *; .pursuit { position: relative; width: var(--item-size); height: var(--item-size); transition: opacity 0.2s, transform 0.2s; box-sizing: border-box; :global(.pathfinder) &, :global(#milestones) &, :global(#seasonal-challenges) & { height: calc(var(--item-size) * 10 / 9 + 4px); } } .pursuit.flawedPassage::before { content: ''; background: rgb(255, 0, 0, 0.2); width: 100%; height: 100%; position: absolute; } .image { width: var(--item-size); height: var(--item-size); box-sizing: border-box; border: 1px solid #666; background-color: #181818; background-position: center; background-repeat: no-repeat; background-size: contain; .tracked & { border-color: #bdfc7f; border-width: 2px; } &:focus { outline: none; } :global(.pathfinder) & { border-radius: 50%; padding: 4px; } :global(#milestones) &, :global(#event) &, :global(#seasonal-challenges) & { border: 0; background-color: transparent; } } .amount { position: absolute; right: 1px; bottom: 1px; background-color: #ddd; color: black; height: calc(#{$badge-height}); font-size: calc(#{$badge-font-size}); text-align: right; box-sizing: border-box; padding: 0 2px; } .fullstack { font-weight: bold; color: #f2721b; } .expired { position: absolute; display: block; width: calc((var(--item-size) + 1px) / 2) !important; height: calc((var(--item-size) + 1px) / 2) !important; top: 0; left: 0; } .complete { position: absolute; display: block; width: calc((var(--item-size) + 1px) / 2) !important; height: calc((var(--item-size) + 1px) / 2) !important; right: 0; bottom: 0; } .trackedIcon { position: absolute; display: block; width: calc(var(--item-size) / 3.1) !important; height: auto !important; right: calc(var(--item-size) / 13); top: 0; } .progress { background: rgb(0, 0, 0, 0.5); position: absolute; width: auto; left: 2px; right: 2px; opacity: 1; bottom: 2px; height: calc(var(--item-size) / 9); } .progressAmount { height: 100%; background-color: $xp; } ================================================ FILE: src/app/progress/PursuitItem.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'amount': string; 'complete': string; 'expired': string; 'flawedPassage': string; 'fullstack': string; 'image': string; 'progress': string; 'progressAmount': string; 'pursuit': string; 'tracked': string; 'trackedIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/PursuitItem.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import NewItemIndicator from 'app/inventory/NewItemIndicator'; import { DimItem } from 'app/inventory/item-types'; import { isBooleanObjective, isFlawlessPassage, isTrialsPassage, } from 'app/inventory/store/objectives'; import { useD2Definitions } from 'app/manifest/selectors'; import { percent } from 'app/shell/formatters'; import { count } from 'app/utils/collections'; import { DestinyObjectiveProgress } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import dimTrackedIcon from 'images/dimTrackedIcon.svg'; import pursuitComplete from 'images/pursuitComplete.svg'; import pursuitExpired from 'images/pursuitExpired.svg'; import trackedIcon from 'images/trackedIcon.svg'; import React from 'react'; import { showPursuitAsExpired } from './Pursuit'; import * as styles from './PursuitItem.m.scss'; export default function PursuitItem({ item, isNew, ref, }: { item: DimItem; isNew: boolean; ref: React.Ref; }) { const defs = useD2Definitions()!; const expired = showPursuitAsExpired(item); // Either there's a counter progress bar, or multiple checkboxes const showProgressBoolean = (objectives: DestinyObjectiveProgress[]) => { const numBooleans = count(objectives, (o) => isBooleanObjective(defs.Objective.get(o.objectiveHash), o.progress, o.completionValue), ); return numBooleans > 1 || objectives.length !== numBooleans; }; const isFlawedTrialsPassage = isTrialsPassage(item.hash) && !isFlawlessPassage(item.objectives); const showProgressBar = item.objectives && item.objectives.length > 0 && !item.complete && !expired && showProgressBoolean(item.objectives); const trackedInGame = item.tracked && (!item.pursuit?.recordHash || item.pursuit.trackedInGame); const trackedInDim = Boolean( item.tracked && item.pursuit?.recordHash && !item.pursuit.trackedInGame, ); const itemImageStyles = { [styles.tracked]: trackedInGame, [styles.tracked]: trackedInDim, [styles.flawedPassage]: isFlawedTrialsPassage, }; return (
{showProgressBar && } {item.maxStackSize > 1 && item.amount > 1 && ( 1 && item.amount === item.maxStackSize} /> )} {$featureFlags.newItems && isNew && } {expired && } {trackedInGame && } {trackedInDim && } {item.complete && }
); } export function ProgressBar({ percentComplete, className, }: { percentComplete: number; className?: string; }) { return (
); } export function StackAmount({ amount, full }: { amount: number; full?: boolean }) { return (
{amount.toLocaleString()}
); } ================================================ FILE: src/app/progress/Pursuits.tsx ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import CollapsibleTitle from 'app/dim-ui/CollapsibleTitle'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { findItemsByBucket } from 'app/inventory/stores-helpers'; import { useD2Definitions } from 'app/manifest/selectors'; import { MILESTONE_QUEST_BUCKET } from 'app/search/d2-known-values'; import { chainComparator, compareBy } from 'app/utils/comparators'; import { BucketHashes, ItemCategoryHashes } from 'data/d2/generated-enums'; import pursuitsInfoFile from 'data/d2/pursuits.json'; import { useState } from 'react'; import BountyGuide, { BountyFilter, DefType, matchBountyFilters } from './BountyGuide'; import Pursuit, { showPursuitAsExpired } from './Pursuit'; import PursuitGrid from './PursuitGrid'; const defaultExpirationDate = new Date(8640000000000000); export const sortPursuits = chainComparator( compareBy(showPursuitAsExpired), compareBy((item) => !item.tracked), compareBy((item) => item.complete), compareBy((item) => (item.pursuit?.expiration?.expirationDate || defaultExpirationDate).getTime(), ), compareBy((item) => item.typeName), compareBy((item) => item.icon), compareBy((item) => item.name), ); const pursuitsOrder = ['Bounties', 'Quests', 'Items'] as const; /** * List out all the Pursuits for the character, grouped out in a useful way. */ export default function Pursuits({ store }: { store: DimStore }) { // checked upstream in Progress const defs = useD2Definitions()!; // Get all items in this character's inventory that represent quests - some are actual items that take // up inventory space, others are in the "Progress" bucket and need to be separated from the quest items // that represent milestones. const milestoneQuests = findItemsByBucket(store, MILESTONE_QUEST_BUCKET); const quests = findItemsByBucket(store, BucketHashes.Quests); const pursuits = Object.groupBy([...milestoneQuests, ...quests], (item) => { const itemDef = defs.InventoryItem.get(item.hash); if (!item.objectives || item.objectives.length === 0 || item.sockets) { return 'Items'; } if ( item.itemCategoryHashes.includes(ItemCategoryHashes.QuestStep) || itemDef?.objectives?.questlineItemHash ) { return 'Quests'; } return 'Bounties'; }); return ( <> {pursuitsOrder.map( (group) => pursuits[group] && (
), )} ); } export function PursuitsGroup({ defs, store, pursuits, pursuitsInfo = pursuitsInfoFile, }: { defs: D2ManifestDefinitions; store: DimStore; pursuits: DimItem[]; pursuitsInfo?: { [hash: string]: { [type in DefType]?: number[] } }; }) { const [bountyFilters, setBountyFilters] = useState([]); return ( <> {pursuits.sort(sortPursuits).map((item) => ( ))} ); } ================================================ FILE: src/app/progress/Raid.tsx ================================================ import { useD2Definitions } from 'app/manifest/selectors'; import { DestinyMilestone } from 'bungie-api-ts/destiny2'; import { RaidActivity, RaidDisplay } from './RaidDisplay'; import './milestone.scss'; /** * Raids offer powerful rewards. Unlike Milestones, some raids have multiple tiers, * so this function enumerates the Activities within the Milstones */ export function Raid({ raid }: { raid: DestinyMilestone }) { const defs = useD2Definitions()!; // convert character's DestinyMilestone to manifest's DestinyMilestoneDefinition const raidDef = defs.Milestone.get(raid.milestoneHash); // try to find the version of the raid with phase data let activities = raid.activities?.filter((activity) => activity.phases) || []; // we may have overfiltered. maybe this raid has no available phase data if (activities.length === 0) { activities = raid.activities || []; } // can be used to override the sometimes cryptic individual activity names let displayNameOverride: string | undefined; if (activities.length === 1 && activities[0].activityHash === 2122313384) { // this used to be a more general check, but ultimately, it is just to override // this goofy activity named "Last Wish: Level 55". the milestone is a // clean "Last Wish Raid" so we provide an override displayNameOverride = raidDef.displayProperties.name; } return ( {activities.length === 0 && ( {raidDef.displayProperties.name} )} {activities.map((activity) => ( ))} ); } ================================================ FILE: src/app/progress/RaidDisplay.m.scss ================================================ .raidActivity { composes: flexColumn from '../dim-ui/common.m.scss'; gap: 4px; } // ALSOHACK .raidTiers { composes: flexColumn from '../dim-ui/common.m.scss'; gap: 14px; } .questObjectives { margin-top: 4px; } ================================================ FILE: src/app/progress/RaidDisplay.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'questObjectives': string; 'raidActivity': string; 'raidTiers': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/RaidDisplay.tsx ================================================ import { useD2Definitions } from 'app/manifest/selectors'; import { ARMSMASTER_ACTIVITY_MODIFIER, ENCOUNTERS_COMPLETED_OBJECTIVE, } from 'app/search/d2-known-values'; import { DestinyDisplayPropertiesDefinition, DestinyMilestoneChallengeActivity, } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import React from 'react'; import BungieImage from '../dim-ui/BungieImage'; import { ActivityModifier } from './ActivityModifier'; import { ObjectiveCheckbox, ObjectiveDescription, ObjectiveProgress, ObjectiveRow, } from './Objective'; import * as styles from './RaidDisplay.m.scss'; /** * Outer wrapper of a Raid type (example: EoW) with icon */ export function RaidDisplay(props: { displayProperties: DestinyDisplayPropertiesDefinition; children?: React.ReactNode; }) { const { displayProperties, children } = props; return (
{displayProperties.hasIcon && ( )}
{children}
); } /** * a Raid Activity, (examples: "EoW", or "EoW Prestige") * describes its phases and difficulty tier if applicable * * a Raid Phase, described in EN strings as an "Encounter", is a segment of a Raid * which offers loot 1x per week, whose completion is tracked by the game & API */ export function RaidActivity({ activity, displayName, hideName, }: { activity: DestinyMilestoneChallengeActivity; /** an override label to use instead of the activity's name */ displayName?: string; hideName?: boolean; }) { const defs = useD2Definitions()!; // a manifest-localized string describing raid segments with loot. "Encounters completed" const encountersString = defs.Objective.get(ENCOUNTERS_COMPLETED_OBJECTIVE).progressDescription; // convert character's DestinyMilestoneChallengeActivity to manifest's DestinyActivityDefinition const activityDef = defs.Activity.get(activity.activityHash); // override individual activity name if there's only 1 tier of the raid const activityName = displayName || activityDef.displayProperties.name; return (
{!hideName && {activityName}} {activity.modifierHashes?.map( (modifierHash) => modifierHash !== ARMSMASTER_ACTIVITY_MODIFIER && ( ), )} {activity.phases && activity.phases.length > 0 && ( p.complete)} className={styles.questObjectives} boolean > {activity.phases?.map((phase) => ( ))} )}
); } ================================================ FILE: src/app/progress/Raids.tsx ================================================ import { DimStore } from 'app/inventory/store-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { RAID_ACTIVITY_TYPE_HASH, RAID_MILESTONE_HASHES } from 'app/search/d2-known-values'; import { compareBy } from 'app/utils/comparators'; import { DestinyMilestone, DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import PursuitGrid from './PursuitGrid'; import { Raid } from './Raid'; import { getCharacterProgressions } from './selectors'; /** * Displays all of the raids available to a user as milestones * reverses raid release order for maximum relevance first */ export default function Raids({ store, profileInfo, }: { store: DimStore; profileInfo: DestinyProfileResponse; }) { const defs = useD2Definitions()!; const characterMilestoneData = getCharacterProgressions(profileInfo, store.id)?.milestones; const allMilestones: DestinyMilestone[] = characterMilestoneData ? Object.values(characterMilestoneData) : []; // filter to milestones with child activities that are raids const filteredMilestones = allMilestones.filter((milestone) => { const milestoneActivities = defs.Milestone.get(milestone.milestoneHash)?.activities; return ( RAID_MILESTONE_HASHES.includes(milestone.milestoneHash) || milestoneActivities?.some( (activity) => defs.Activity.get(activity.activityHash)?.activityTypeHash === RAID_ACTIVITY_TYPE_HASH, // prefer to use DestinyActivityModeType.Raid, but it appears inconsistently in activity defs ) ); }); const raids = filteredMilestones.sort(compareBy((f) => f.order)); return ( {raids.map((raid) => ( ))} ); } ================================================ FILE: src/app/progress/Ranks.tsx ================================================ import { rankProgressionHashesSelector } from 'app/manifest/selectors'; import { LookupTable } from 'app/utils/util-types'; import { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import { ProgressionHashes } from 'data/d2/generated-enums'; import { useSelector } from 'react-redux'; import PursuitGrid from './PursuitGrid'; import { ReputationRank } from './ReputationRank'; import { getCharacterProgressions } from './selectors'; // There are 2 similar DestinyProgression definitions for each rank // system. The rank progression definition contains detailed rank names and // resetInfo, while the streak progression definition contains information about // current streak status. There is no way to map between them automatically, so // this table must be kept up to date manually, by pasting the following code // into the body of the component while you have some streaks and using that to // figure out which unmapped streak progression matches with which rank // progression: // // const defs = useD2Definitions()!; // const progressions = Object.values(defs.Progression.getAll()).filter( // (d) => // d.scope === DestinyProgressionScope.MappedUnlockValue && // d.steps?.length === 5 && // !Object.values(rankProgressionToStreakProgression).includes(d.hash) // ); // console.log( // progressions.map((p) => ({ // hash: p.hash, // streak: firstCharacterProgression[p.hash].stepIndex, // })), // progressionHashes.map((p) => ({ // name: defs?.Progression.get(p).displayProperties.name, // hash: p, // })) // ); const rankProgressionToStreakProgression: LookupTable = { [ProgressionHashes.StrangeFavor]: 1999336308, }; // This set contains progression hashes that should not be displayed in the // Ranks component. The API still returns them, but they are no longer visible // in the game. const hideProgressionHashes = new Set([ 784742260, // Engram Ensiders (Rahool) 2411069437, // Gunsmith Rank 1471185389, // Xûr Rank 527867935, // Strange Favor (Dares of Eternity) ]); // This doesn't show up in the automatic progression hashes, but it is mildly // useful in that it will affect your Crucible reward multiplier. const crucibleRewardRankProgressionHash = 2206541810; /** * Displays all ranks for the account */ export default function Ranks({ profileInfo, children, }: { profileInfo: DestinyProfileResponse; children?: React.ReactNode; }) { const firstCharacterProgression = getCharacterProgressions(profileInfo)?.progressions ?? {}; const progressionHashes = useSelector(rankProgressionHashesSelector); return ( {children} {[crucibleRewardRankProgressionHash, ...progressionHashes].map( (progressionHash) => !hideProgressionHashes.has(progressionHash) && firstCharacterProgression[progressionHash] && ( ), )} ); } ================================================ FILE: src/app/progress/ReputationRank.m.scss ================================================ @use '../variables.scss' as *; .factionInfo { display: flex; flex-direction: column; } .activityRank { display: flex; flex-direction: row; align-items: flex-start; } .factionName { text-transform: uppercase; font-size: 14px; } .crucibleRankIcon { margin: 2px 10px -2px 2px; position: relative; width: 64px; height: 64px; } .gridLayout { @include phone-portrait { .factionInfo { align-items: center; text-align: center; } &.activityRank { align-items: center; flex-direction: column; } .crucibleRankIcon { margin: 0 0 4px 0; } } } .crucibleRankProgress { fill: none; } .crucibleRankTotalProgress { stroke: var(--theme-accent-secondary); fill: none; } .factionLevel { color: var(--theme-text-secondary); &.max { color: var(--theme-accent-primary); } } .rankIcon { height: 10px; margin-right: 4px; } .winStreak { /* yellow for crucibles, green override for gambit */ --objective-checkbox-color: #dde330; margin-top: 5px; gap: 4px; :global(.faction-3008065600) & { --objective-checkbox-color: #409a80; } & > * { height: 14px; width: 14px; margin: 0; } } @include phone-portrait { :global(.ranks-for-character) { .faction { flex-direction: column; } } } ================================================ FILE: src/app/progress/ReputationRank.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'activityRank': string; 'crucibleRankIcon': string; 'crucibleRankProgress': string; 'crucibleRankTotalProgress': string; 'faction': string; 'factionInfo': string; 'factionLevel': string; 'factionName': string; 'gridLayout': string; 'max': string; 'rankIcon': string; 'winStreak': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/ReputationRank.tsx ================================================ import { useDynamicStringReplacer } from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { t, tl } from 'app/i18next-t'; import { useD2Definitions } from 'app/manifest/selectors'; import { unadvertisedResettableVendors } from 'app/search/d2-known-values'; import { sumBy } from 'app/utils/collections'; import { DestinyProgression } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import BungieImage, { bungieNetPath } from '../dim-ui/BungieImage'; import { ObjectiveCheckbox, ObjectiveRow } from './Objective'; import * as styles from './ReputationRank.m.scss'; /** * displays a single reputation rank for the account */ export function ReputationRank({ progress, streak, isProgressRanks, }: { progress: DestinyProgression; streak?: DestinyProgression; isProgressRanks?: boolean; }) { const defs = useD2Definitions()!; const replacer = useDynamicStringReplacer(); const progressionDef = defs.Progression.get(progress.progressionHash); const step = progressionDef.steps[Math.min(progress.level, progressionDef.steps.length - 1)]; const canReset = typeof progress.currentResetCount === 'number' || unadvertisedResettableVendors.includes(progress.progressionHash); const resetLabel = canReset ? tl('Progress.PercentPrestige') : tl('Progress.PercentMax'); const rankTotal = sumBy(progressionDef.steps, (cur) => cur.progressTotal); const rankPercent = Math.floor((progress.currentProgress / rankTotal) * 100); const streakCheckboxes = streak && Array(5).fill(true).fill(false, streak.currentProgress); // language-agnostic css class name to identify which rank type we are in const factionClass = `faction-${progress.progressionHash}`; return (
{t('Progress.Rank', { name: progressionDef.displayProperties.name, rank: progress.level + 1, })}
{step.stepName}
{progressionDef.rankIcon && ( )} {progress.currentProgress} ({progress.progressToNextLevel} / {progress.nextLevelAt})
{streakCheckboxes && ( {streakCheckboxes.map((c, i) => ( ))} )}
{t(resetLabel, { pct: rankPercent, })}
{Boolean(progress.currentResetCount) && (
{t('Progress.Resets', { count: progress.currentResetCount })}
)}
); } function ReputationRankIcon({ progress }: { progress: DestinyProgression }) { const defs = useD2Definitions()!; const progressionDef = defs.Progression.get(progress.progressionHash); const step = progressionDef.steps[Math.min(progress.level, progressionDef.steps.length - 1)]; const canReset = progressionDef.steps.length === progress.levelCap; const rankTotal = sumBy(progressionDef.steps, (step) => step.progressTotal); const circumference = 2 * 22 * Math.PI; const circumference2 = 2 * 25 * Math.PI; const strokeColor = progressionDef.color ? `rgb(${progressionDef.color.red}, ${progressionDef.color.green},${progressionDef.color.blue})` : 'white'; return (
{progress.progressToNextLevel > 0 && ( = progress.nextLevelAt ? 'none' : `${(circumference * progress.progressToNextLevel) / progress.nextLevelAt} ${circumference}` } stroke={strokeColor} /> )} {canReset && progress.currentProgress > 0 && ( = rankTotal ? 'none' : `${(circumference2 * progress.currentProgress) / rankTotal} ${circumference2}` } /> )}
); } ================================================ FILE: src/app/progress/Reward.m.scss ================================================ .reward { display: flex; flex-direction: row; align-items: center; margin: 8px 0 0 0; img { width: 17px; height: 17px; margin-right: 4px; } } ================================================ FILE: src/app/progress/Reward.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'reward': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/Reward.tsx ================================================ import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { DimStore } from 'app/inventory/store-types'; import { dropPowerLevelSelector } from 'app/inventory/store/selectors'; import { useD2Definitions } from 'app/manifest/selectors'; import { DestinyItemQuantity } from 'bungie-api-ts/destiny2'; import { useSelector } from 'react-redux'; import BungieImage from '../dim-ui/BungieImage'; import * as styles from './Reward.m.scss'; import { getEngramPowerBonus } from './engrams'; import { getXPValue } from './xp'; export function Reward({ reward, store, itemHash, }: { reward: DestinyItemQuantity; // If provided, will help make engram bonuses more accurate store?: DimStore; // If provided, will help make engram bonuses more accurate itemHash?: number; }) { const defs = useD2Definitions()!; const dropPower = useSelector(dropPowerLevelSelector(store?.id)); const [powerBonus, rewardItemHash] = getEngramPowerBonus(reward.itemHash, dropPower, itemHash); const rewardItem = defs.InventoryItem.get(rewardItemHash); const rewardDisplay = rewardItem.displayProperties; const xpValue = getXPValue(reward.itemHash); return (
{powerBonus !== undefined && `+${powerBonus} `} {reward.quantity > 1 && ` +${reward.quantity.toLocaleString()}`} {xpValue !== undefined && ` (${xpValue.toLocaleString()} XP)`}
); } ================================================ FILE: src/app/progress/SeasonalChallenges.tsx ================================================ import { trackedTriumphsSelector } from 'app/dim-api/selectors'; import CollapsibleTitle from 'app/dim-ui/CollapsibleTitle'; import { createItemContextSelector } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { DimPresentationNode, DimRecord, toPresentationNodeTree, } from 'app/records/presentation-nodes'; import { DestinyPresentationNodeDefinition, DestinyRecordState } from 'bungie-api-ts/destiny2'; import seasonalChallengesInfo from 'data/d2/seasonal-challenges.json'; import { useSelector } from 'react-redux'; import { PursuitsGroup } from './Pursuits'; import { recordToPursuitItem } from './milestone-items'; /** * List out all the seasonal challenges for the character, grouped out in a useful way. */ export default function SeasonalChallenges({ seasonalChallengesPresentationNode, store, }: { seasonalChallengesPresentationNode: DestinyPresentationNodeDefinition; store: DimStore; }) { const itemCreationContext = useSelector(createItemContextSelector); const nodeTree = toPresentationNodeTree( itemCreationContext, seasonalChallengesPresentationNode.hash, ); const allRecords = nodeTree ? flattenRecords(nodeTree) : []; const trackedRecords = useSelector(trackedTriumphsSelector); const pursuits = allRecords .filter((r) => { // Don't show records that have been redeemed const state = r.recordComponent.state; const acquired = Boolean(state & DestinyRecordState.RecordRedeemed); return !acquired; }) .map((r) => recordToPursuitItem( r, itemCreationContext.buckets, store, seasonalChallengesPresentationNode.displayProperties.name, trackedRecords.includes(r.recordDef.hash), ), ); return (
); } function flattenRecords(nodeTree: DimPresentationNode): DimRecord[] { let records = nodeTree.records || []; if (nodeTree.childPresentationNodes) { records = [...records, ...nodeTree.childPresentationNodes.flatMap(flattenRecords)]; } return records; } ================================================ FILE: src/app/progress/SeasonalRank.m.scss ================================================ @use '../variables.scss' as *; .seasonalRewardWrapper { composes: pursuit from './PursuitItem.m.scss'; width: var(--item-size); height: var(--item-size) !important; img { border: $item-border-width solid #666; box-sizing: border-box; } &.free { background: #2b2f33; } &.premium { background: #016062; } } .hasPremiumRewards { :global(.milestone-icon) { max-width: calc(var(--item-size) * 2.1); width: min-content; } } .progress { width: 90%; text-align: center; > span { white-space: nowrap; } } .seasonEnd { font-style: italic; } .seasonalRewards { display: flex; flex-direction: row; gap: 2px; } .progressBar { position: relative; bottom: 0; left: 0; margin-top: 5px; } .seasonName { font-size: 14; color: var(--theme-text-secondary); } .seasonInfo { display: flex; flex-direction: column; } .activityRank { display: flex; flex-direction: row; align-items: flex-start; } .seasonRankIcon { margin: 2px 10px -2px 2px; position: relative; width: 64px; height: 64px; } .gridLayout { @include phone-portrait { .seasonInfo { align-items: center; text-align: center; } &.activityRank { align-items: center; flex-direction: column; } .seasonRankIcon { margin: 0 0 4px 0; } } } .seasonRankProgress { fill: none; } .seasonLevel { color: var(--theme-text-secondary); } .seassonTitle { font-size: 14px; color: var(--theme-text); text-transform: uppercase; } @include phone-portrait { :global(.ranks-for-character) { .season { flex-direction: column; } } } ================================================ FILE: src/app/progress/SeasonalRank.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'activityRank': string; 'free': string; 'gridLayout': string; 'hasPremiumRewards': string; 'premium': string; 'progress': string; 'progressBar': string; 'season': string; 'seasonEnd': string; 'seasonInfo': string; 'seasonLevel': string; 'seasonName': string; 'seasonRankIcon': string; 'seasonRankProgress': string; 'seasonalRewardWrapper': string; 'seasonalRewards': string; 'seassonTitle': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/SeasonalRank.tsx ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import Countdown from 'app/dim-ui/Countdown'; import { useDynamicStringReplacer } from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { t } from 'app/i18next-t'; import { DimStore } from 'app/inventory/store-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { isClassCompatible } from 'app/utils/item-utils'; import { useCurrentSeasonInfo } from 'app/utils/seasons'; import { DestinyClass, DestinyProfileResponse, DestinyProgression, DestinyProgressionRewardItemQuantity, DestinySeasonDefinition, DestinySeasonPassDefinition, DestinySeasonPassReference, } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import brightEngramsBonus from 'data/d2/bright-engram-bonus.json'; import BungieImage, { bungieNetPath } from '../dim-ui/BungieImage'; import { ProgressBar, StackAmount } from './PursuitItem'; import * as styles from './SeasonalRank.m.scss'; import { getCharacterProgressions } from './selectors'; export default function SeasonalRank({ store, profileInfo, }: { store: DimStore; profileInfo: DestinyProfileResponse; }) { const defs = useD2Definitions()!; const { season, seasonPass, seasonPassStartEnd } = useCurrentSeasonInfo(defs, profileInfo); if (!season || !seasonPass) { return null; } const prestigeRewardHash = season.artifactItemHash || 0; const prestigeRewardLevel = 9999; // fake reward level for fake item for prestige level // Get season details const seasonNameDisplay = season.displayProperties.name; const { seasonPassLevel, rewardItems, prestigeProgression, progressToNextLevel, nextLevelAt, prestigeMode, hasPremiumRewards, } = getSeasonPassStatus(defs, profileInfo, seasonPass, season); if ( // Only add the fake rewards once !rewardItems.some((item) => item.rewardedAtProgressionLevel === prestigeRewardLevel) ) { rewardItems.push(fakeReward(prestigeRewardHash, prestigeRewardLevel)); } const nextRewardItems = rewardItems .filter((item) => { // Get the reward items for the next progression level if ( prestigeMode ? item.rewardedAtProgressionLevel !== prestigeRewardLevel : item.rewardedAtProgressionLevel !== seasonPassLevel + 1 ) { return false; } // Filter class-specific items const def = defs.InventoryItem.get(item.itemHash); if (!def) { return false; } const plugCategoryId = def.plug?.plugCategoryIdentifier ?? ''; if (def.itemSubType === 21) { // Ornament Only Filtering if (plugCategoryId.includes('_titan_')) { return DestinyClass.Titan === store.classType; } else if (plugCategoryId.includes('_hunter_')) { return DestinyClass.Hunter === store.classType; } else if (plugCategoryId.includes('_warlock_')) { return DestinyClass.Warlock === store.classType; } } return isClassCompatible(def.classType, store.classType); }) // Premium reward first to match companion .reverse(); if (!rewardItems.length) { return null; } const rewardPassEnd = seasonPassStartEnd?.seasonPassEndDate; return !prestigeMode ? (
{nextRewardItems.map((item) => { // Don't show premium reward if player doesn't own the season pass if (!hasPremiumRewards && item.uiDisplayStyle === 'premium') { return; } // Get the item info for UI display const itemInfo = defs.InventoryItem.get(item.itemHash); return (
{item.quantity > 1 && }
); })}
{progressToNextLevel.toLocaleString()} / {nextLevelAt.toLocaleString()}
{t('Milestone.SeasonalRank', { rank: seasonPassLevel })}
{seasonNameDisplay} {rewardPassEnd && (
{t('Progress.RewardPassEndsIn')}
)}
) : ( ); } export function SeasonPrestigeRank({ seasonPass, seasonPassStartEnd, progress, isProgressRanks, }: { seasonPass: DestinySeasonPassDefinition; seasonPassStartEnd: DestinySeasonPassReference | undefined; progress: DestinyProgression; isProgressRanks?: boolean; }) { const defs = useD2Definitions()!; const replacer = useDynamicStringReplacer(); const progressionDef = defs.Progression.get(progress.progressionHash); // We need to get the latest bright engram bonus icon const brightEngramBonus = defs.InventoryItem.get( brightEngramsBonus[brightEngramsBonus.length - 1], ); const rewardPassEnd = seasonPassStartEnd?.seasonPassEndDate; const rewardPassSeasonName = seasonPass.displayProperties.name; return (
{t('Progress.RewardPassPrestigeRank', { rank: progress.level, })}
{rewardPassSeasonName}
{progress.progressToNextLevel.toLocaleString()} /{' '} {progress.nextLevelAt.toLocaleString()}
{rewardPassEnd && (
{t('Progress.RewardPassEndsIn')}
)}
); } export function ReputationRankIcon({ progress, icon, }: { progress: DestinyProgression; icon: string; }) { const circumference = 2 * 22 * Math.PI; return (
{progress.progressToNextLevel > 0 && ( )}
); } export function getSeasonPassStatus( defs: D2ManifestDefinitions, profileInfo: DestinyProfileResponse, seasonPass: DestinySeasonPassDefinition, season: DestinySeasonDefinition, storeId?: string, ) { const characterProgressions = getCharacterProgressions(profileInfo, storeId)!; const seasonPassProgressionHash = seasonPass.rewardProgressionHash; const seasonProgression = characterProgressions.progressions[seasonPassProgressionHash]; const seasonProgressionDef = defs.Progression.get(seasonPassProgressionHash); const baseLevels = seasonProgressionDef.steps.filter((step) => step.progressTotal === 100000).length + 1; const prestigeProgressionHash = seasonPass.prestigeProgressionHash; const prestigeProgression = characterProgressions.progressions[prestigeProgressionHash]; const prestigeProgressionDef = defs.Progression.get(prestigeProgressionHash); // Take seasonpass level and add prestige progress adjusted to remove 500k xp levels double counting const seasonPassLevel = Math.min(seasonProgression.level, baseLevels) + prestigeProgression.level; const { rewardItems } = defs.Progression.get(seasonPassProgressionHash); const prestigeMode = seasonProgression.level === seasonProgression.levelCap; const { progressToNextLevel, nextLevelAt } = prestigeMode ? prestigeProgression : seasonProgression; const hasPremiumRewards = (profileInfo?.profile?.data?.seasonHashes || []).includes(season.hash); const weeklyProgress = prestigeMode ? prestigeProgression.weeklyProgress : seasonProgression.weeklyProgress; return { seasonPassLevel, rewardItems, progressToNextLevel, nextLevelAt, weeklyProgress, // The number of regular 100k xp requiring levels baseLevels, /** The player hit the end of the season pass and is now "prestiging it" to levels beyond the reward track. */ prestigeMode, hasPremiumRewards, // Raw progression information seasonProgression, seasonProgressionDef, prestigeProgression, prestigeProgressionDef, }; } function fakeReward(hash: number, level: number): DestinyProgressionRewardItemQuantity { return { acquisitionBehavior: 1, claimUnlockDisplayStrings: [''], hasConditionalVisibility: false, itemHash: hash, quantity: 1, rewardedAtProgressionLevel: level, uiDisplayStyle: 'free', rewardItemIndex: 0, socketOverrides: [], }; } ================================================ FILE: src/app/progress/TrackedTriumphs.m.scss ================================================ .noRecords { padding: 1em; } ================================================ FILE: src/app/progress/TrackedTriumphs.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'noRecords': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/progress/TrackedTriumphs.tsx ================================================ import { trackedTriumphsSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { profileResponseSelector } from 'app/inventory/selectors'; import { useD2Definitions } from 'app/manifest/selectors'; import { RecordGrid } from 'app/records/Record'; import { searchDisplayProperties, toRecord } from 'app/records/presentation-nodes'; import { filterMap } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { DestinyPresentationNodeDefinition, DestinyRecordDefinition } from 'bungie-api-ts/destiny2'; import { useSelector } from 'react-redux'; import * as styles from './TrackedTriumphs.m.scss'; export function TrackedTriumphs({ searchQuery }: { searchQuery?: string }) { const defs = useD2Definitions()!; const profileResponse = useSelector(profileResponseSelector)!; const trackedTriumphs = useSelector(trackedTriumphsSelector); const trackedRecordHash = profileResponse?.profileRecords?.data?.trackedRecordHash || 0; const recordHashes = trackedRecordHash ? [...new Set([trackedRecordHash, ...trackedTriumphs])] : trackedTriumphs; let records = filterMap(recordHashes, (h) => toRecord(defs, profileResponse, h, /* mayBeMissing */ true), ); if (searchQuery) { records = records.filter((r) => searchDisplayProperties(r.recordDef.displayProperties, searchQuery), ); } // determine absolute path of record for sorting purpose const recordPath = (r: DestinyRecordDefinition) => { const path: string[] = []; let parent: DestinyRecordDefinition | DestinyPresentationNodeDefinition = r; while (parent?.parentNodeHashes?.length > 0) { path.unshift(parent.displayProperties.name); parent = defs.PresentationNode.get(parent.parentNodeHashes[0]); } return path; }; // sort by parent node groups (alphabetically) records = records.sort(compareBy((record) => recordPath(record.recordDef).join('/'))); if (!records.length) { return (
{recordHashes.length > 0 && searchQuery ? t('Progress.QueryFilteredTrackedTriumphs') : t('Progress.NoTrackedTriumph')}
); } return ; } ================================================ FILE: src/app/progress/WellRestedPerkIcon.tsx ================================================ import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { useD2Definitions } from 'app/manifest/selectors'; import { WELL_RESTED_PERK } from 'app/search/d2-known-values'; import { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import BungieImage from '../dim-ui/BungieImage'; import { useIsWellRested } from '../inventory/store/well-rested'; export default function WellRestedPerkIcon({ profileInfo, }: { profileInfo: DestinyProfileResponse; }) { const defs = useD2Definitions()!; const wellRestedInfo = useIsWellRested(defs, profileInfo); if (!wellRestedInfo.wellRested) { return null; } const wellRestedPerk = defs.SandboxPerk.get(WELL_RESTED_PERK); if (!wellRestedPerk) { return null; } const perkDisplay = wellRestedPerk.displayProperties; return (
{wellRestedInfo.weeklyProgress !== undefined && wellRestedInfo.requiredXP !== undefined && ( {wellRestedInfo.weeklyProgress.toLocaleString()} / {wellRestedInfo.requiredXP.toLocaleString()} )}
{perkDisplay.name}
); } ================================================ FILE: src/app/progress/engrams.ts ================================================ import { powerLevelByKeyword } from 'app/search/power-levels'; import { HashLookup } from 'app/utils/util-types'; import { clamp } from 'es-toolkit'; /** * Logic for determining how much power bonus an engram can provide. */ const enum PowerCap { /** Pinnacle engrams can go up to +2 over the powerful cap, up to the hard cap */ Pinnacle, /** Powerful engrams cannot go above the powerful cap */ Powerful, } const engrams: HashLookup<{ cap: PowerCap; bonus: number }> = { // Pinnacle 73143230: { cap: PowerCap.Pinnacle, bonus: 5, }, // Pinnacle Deepsight Weapon 323631491: { cap: PowerCap.Pinnacle, bonus: 5, }, // Prime Engram from Pathfinder 853937142: { cap: PowerCap.Pinnacle, bonus: 5, }, // Pinnacle from Pathfinder 863286832: { cap: PowerCap.Pinnacle, bonus: 5, }, // Tier 1 3114385605: { cap: PowerCap.Powerful, bonus: 3, }, // Tier 1 Exotic 2643364263: { cap: PowerCap.Powerful, bonus: 3, }, // Powerful 4039143015: { cap: PowerCap.Powerful, bonus: 3, }, // Tier 2 3114385606: { cap: PowerCap.Powerful, bonus: 4, }, // Rose (from Competitive) 882778888: { cap: PowerCap.Powerful, bonus: 4, }, // Tier 3 3114385607: { cap: PowerCap.Powerful, bonus: 5, }, // Powerful (Tier 3) from Pathfinder 602242147: { cap: PowerCap.Powerful, bonus: 5, }, }; /** * How much above the player's current max power will this reward drop? */ export function getEngramPowerBonus( itemHash: number, maxPower?: number, parentItemHash?: number, ): [powerLevel: number | undefined, itemHash: number] { if (parentItemHash === 3603098564) { // Hawthorne's Clan Rewards gives out a +2 pinnacle even though it's listed as a powerful itemHash = 73143230; } else if (parentItemHash === 3243997895) { // Captain's Log is a powerful level 2 even though it's listed as a pinnacle. itemHash = 3114385606; } else if (parentItemHash === 373284212) { // Enterprising Explorer II is powerful level 2 itemHash = 3114385606; } else if (parentItemHash === 373284213) { // Enterprising Explorer III is powerful level 3 itemHash = 3114385607; } const engramInfo = engrams[itemHash]; if (engramInfo) { maxPower ||= 0; maxPower = Math.floor(maxPower); const powerfulCap = powerLevelByKeyword.powerfulcap; if (engramInfo.cap === PowerCap.Powerful) { // Powerful engrams can't go above the powerful cap return [clamp(powerfulCap - maxPower, 0, engramInfo.bonus), itemHash]; } else if (engramInfo.cap === PowerCap.Pinnacle) { // Once you're at or above the powerful cap, pinnacles only give +2, up to the hard cap const pinnacleCap = Math.min( powerLevelByKeyword.pinnaclecap, Math.max(maxPower, powerfulCap) + 2, ); return [clamp(pinnacleCap - maxPower, 0, engramInfo.bonus), itemHash]; } } return [undefined, itemHash]; } ================================================ FILE: src/app/progress/milestone-items.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { InventoryBuckets } from 'app/inventory/inventory-buckets'; import { DimItem, DimPursuitExpiration } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { DimRecord } from 'app/records/presentation-nodes'; import { d2MissingIcon } from 'app/search/d2-known-values'; import { sumBy } from 'app/utils/collections'; import { isClassCompatible } from 'app/utils/item-utils'; import { DestinyAmmunitionType, DestinyDisplayPropertiesDefinition, DestinyMilestone, DestinyMilestoneDefinition, DestinyMilestoneQuest, DestinyMilestoneRewardCategory, DestinyMilestoneRewardCategoryDefinition, DestinyMilestoneRewardEntry, DestinyMilestoneType, DestinyObjectiveProgress, DestinyRecordState, } from 'bungie-api-ts/destiny2'; import { BucketHashes, ItemCategoryHashes } from 'data/d2/generated-enums'; export function milestoneToItems( milestone: DestinyMilestone, defs: D2ManifestDefinitions, buckets: InventoryBuckets, store: DimStore, ): DimItem[] { const milestoneDef = defs.Milestone.get(milestone.milestoneHash); // TODO: activity locations (nightfalls, etc) if (milestone.availableQuests) { return milestone.availableQuests.map((availableQuest) => availableQuestToItem(defs, buckets, milestone, milestoneDef, availableQuest, store), ); } else if (milestone.activities?.length) { const item = activityMilestoneToItem(buckets, milestoneDef, milestone, defs, store); return item ? [item] : []; } else if (milestone.rewards) { // Weekly Clan Milestones const rewards = milestone.rewards[0]; const milestoneRewardDef = milestoneDef.rewards[rewards.rewardCategoryHash]; return rewards.entries .filter((r) => !r.earned) .map((rewardEntry) => weeklyClanMilestoneToItems( buckets, rewardEntry, milestoneDef, milestone, milestoneRewardDef, store, ), ); } else { const item = makeMilestonePursuitItem( buckets, milestone, milestoneDef, milestoneDef.displayProperties, [], store, ); return item ? [item] : []; } } function milestoneExpiration(milestone: DestinyMilestone): DimPursuitExpiration | undefined { return milestone.endDate ? { expirationDate: new Date(milestone.endDate), suppressExpirationWhenObjectivesComplete: false, expiredInActivityMessage: undefined, } : undefined; } function availableQuestToItem( defs: D2ManifestDefinitions, buckets: InventoryBuckets, milestone: DestinyMilestone, milestoneDef: DestinyMilestoneDefinition, availableQuest: DestinyMilestoneQuest, store: DimStore, ): DimItem { const questDef = milestoneDef.quests[availableQuest.questItemHash]; const questItem = defs.InventoryItem.get(questDef.questItemHash); const challengeItemHash = questItem.setData?.itemList[0].itemHash; const challengeItem = challengeItemHash ? defs.InventoryItem.get(challengeItemHash) : undefined; const displayProperties: DestinyDisplayPropertiesDefinition = questDef.displayProperties || milestoneDef.displayProperties; // Only look at the first reward, the rest are screwy (old engram versions, etc) const questRewards = questDef.questRewards ? questDef.questRewards.items // 75% of "rewards" are the invalid hash 0 .filter((r) => r.itemHash) .map((r) => defs.InventoryItem.get(r.itemHash)) // Filter out rewards that are for other characters .filter( (i) => i && isClassCompatible(i.classType, store.classType) && // And quest steps, they're not interesting !i.itemCategoryHashes?.includes(ItemCategoryHashes.QuestStep), ) .slice(0, 1) : []; const objectives = availableQuest.status.stepObjectives; const dimItem = makeMilestonePursuitItem( buckets, milestone, milestoneDef, displayProperties, objectives, store, ); if (dimItem.pursuit === null) { throw new Error(''); // can't happen } dimItem.secondaryIcon = challengeItem?.secondaryIcon; dimItem.pursuit.modifierHashes = availableQuest?.activity?.modifierHashes || []; if (questRewards?.length) { dimItem.pursuit.rewards = questRewards.map((r) => ({ itemHash: r.hash, quantity: 1, hasConditionalVisibility: false, })); } else if (questDef.questItemHash) { const questItem = defs.InventoryItem.get(questDef.questItemHash); if (questItem?.value?.itemValue.length) { dimItem.pursuit.rewards = questItem.value.itemValue .filter((v) => v.itemHash !== 0) .map((v) => ({ itemHash: v.itemHash, quantity: v.quantity || 1, hasConditionalVisibility: false, })); } } return dimItem; } function activityMilestoneToItem( buckets: InventoryBuckets, milestoneDef: DestinyMilestoneDefinition, milestone: DestinyMilestone, defs: D2ManifestDefinitions, store: DimStore, ): DimItem | null { // Find the first activity that hasn't been completed. TODO: Can we show all of them? const activity = milestone.activities.find((a) => a.challenges.some((c) => !c.objective.complete), ); // Ignore the milestone if all activities are complete or there are no challenges. if (!activity) { return null; } const activityDef = activity && defs.Activity.get(activity.activityHash); const objectives = activity.challenges.map((a) => a.objective); const dimItem = makeMilestonePursuitItem( buckets, milestone, milestoneDef, milestoneDef.displayProperties, objectives, store, ); if (dimItem.pursuit === null) { throw new Error(''); // can't happen } dimItem.pursuit.modifierHashes = activity.modifierHashes || []; if (activityDef) { if (!milestone.rewards) { dimItem.pursuit.rewards = activityDef.challenges .filter((c) => objectives.some((o) => o.objectiveHash === c.objectiveHash)) .flatMap((c) => c.dummyRewards); } if (milestoneDef.hash === 2029743966 /* Nightfall Weekly Score Challenge */) { dimItem.name = `${dimItem.name}: ${activityDef.displayProperties.description}`; } else if (milestoneDef.hash === 1506285920 /* Crucible Labs Playlist Challenge */) { dimItem.name = `${dimItem.name}: ${activityDef.displayProperties.name}`; } } return dimItem; } /** Build an individual clan milestone activity into a pursuit. */ function weeklyClanMilestoneToItems( buckets: InventoryBuckets, rewardEntry: DestinyMilestoneRewardEntry, milestoneDef: DestinyMilestoneDefinition, milestone: DestinyMilestone, milestoneRewardDef: DestinyMilestoneRewardCategoryDefinition, store: DimStore, ): DimItem { const reward = milestoneRewardDef.rewardEntries[rewardEntry.rewardEntryHash]; const displayProperties: DestinyDisplayPropertiesDefinition = { ...milestoneDef.displayProperties, ...reward.displayProperties, }; const dimItem = makeFakePursuitItem( buckets, displayProperties, rewardEntry.rewardEntryHash, milestoneDef.displayProperties.name, store, ); dimItem.pursuit = { expiration: milestoneExpiration(milestone), modifierHashes: [], rewards: reward.items, }; return dimItem; } function makeFakePursuitItem( buckets: InventoryBuckets, displayProperties: DestinyDisplayPropertiesDefinition, hash: number, typeName: string, store: DimStore, ): DimItem { const bucket = buckets.byHash[BucketHashes.Quests]; return { // figure out what year this item is probably from destinyVersion: 2, // The bucket the item is currently in location: bucket, // The bucket the item normally resides in (even though it may be in the vault/postmaster) bucket: bucket, hash, itemCategoryHashes: [], // see defs.ItemCategory rarity: 'Rare', isExotic: false, name: displayProperties.name, description: displayProperties.description, icon: displayProperties.icon || d2MissingIcon, notransfer: true, canPullFromPostmaster: false, id: '0', // zero for non-instanced is legacy hack instanced: false, equipped: false, equipment: false, // TODO: this has a ton of good info for the item move logic complete: false, amount: 1, primaryStat: null, typeName, equipRequiredLevel: 0, maxStackSize: 1, // 0: titan, 1: hunter, 2: warlock, 3: any classType: 3, classTypeNameLocalized: 'Any', element: null, lockable: false, tracked: false, locked: false, masterwork: false, crafted: false, highlightedObjective: false, classified: false, isEngram: false, percentComplete: 0, // filled in later hidePercentage: false, stats: null, // filled in later objectives: undefined, // filled in later ammoType: DestinyAmmunitionType.None, missingSockets: false, breakerType: null, pursuit: null, taggable: false, comparable: false, wishListEnabled: false, power: 0, index: hash.toString(), infusable: false, infusionFuel: false, sockets: null, masterworkInfo: null, infusionCategoryHashes: null, owner: store.id, uniqueStack: false, trackable: false, energy: null, featured: false, tier: 0, adept: false, holofoil: false, }; } function makeMilestonePursuitItem( buckets: InventoryBuckets, milestone: DestinyMilestone, milestoneDef: DestinyMilestoneDefinition, displayProperties: DestinyDisplayPropertiesDefinition, objectives: DestinyObjectiveProgress[], store: DimStore, ) { const dimItem = makeFakePursuitItem( buckets, displayProperties, milestone.milestoneHash, milestoneTypeName(milestoneDef.milestoneType), store, ); if (objectives) { dimItem.objectives = objectives; dimItem.percentComplete = calculatePercentComplete(dimItem.objectives); } dimItem.pursuit = { expiration: milestoneExpiration(milestone), modifierHashes: milestone.activities?.[0]?.modifierHashes || [], rewards: [], }; if (milestone.rewards) { dimItem.pursuit.rewards = processRewards(milestone.rewards, milestoneDef); } return dimItem; } function milestoneTypeName(milestoneType: DestinyMilestoneType) { switch (milestoneType) { case DestinyMilestoneType.Daily: return t('Milestone.Daily'); case DestinyMilestoneType.Weekly: return t('Milestone.Weekly'); case DestinyMilestoneType.Special: return t('Milestone.Special'); case DestinyMilestoneType.Tutorial: return t('Milestone.Tutorial'); case DestinyMilestoneType.OneTime: return t('Milestone.OneTime'); case DestinyMilestoneType.Unknown: return t('Milestone.Unknown'); } } export function recordToPursuitItem( record: DimRecord, buckets: InventoryBuckets, store: DimStore, typeName: string, tracked: boolean, ) { const dimItem = makeFakePursuitItem( buckets, record.recordDef.displayProperties, record.recordDef.hash, typeName, store, ); if (record.recordComponent.objectives) { dimItem.objectives = record.recordComponent.objectives; dimItem.percentComplete = calculatePercentComplete(dimItem.objectives); } const state = record.recordComponent.state; const acquired = Boolean(state & DestinyRecordState.RecordRedeemed); dimItem.complete = !acquired && !(state & DestinyRecordState.ObjectiveNotCompleted); dimItem.pursuit = { expiration: undefined, modifierHashes: [], rewards: [], recordHash: record.recordDef.hash, trackedInGame: record.trackedInGame, }; if (record.recordDef.rewardItems) { dimItem.pursuit.rewards = record.recordDef.rewardItems.filter( (_r, i) => record.recordComponent.rewardVisibilty?.[i] ?? true, ); } dimItem.trackable = true; dimItem.tracked = record.trackedInGame || tracked; return dimItem; } function calculatePercentComplete(objectives: DestinyObjectiveProgress[]) { const length = objectives.length; return sumBy(objectives, (objective) => { if (objective.completionValue) { return Math.min(1, (objective.progress || 0) / objective.completionValue) / length; } else { return 0; } }); } function processRewards( rewards: DestinyMilestoneRewardCategory[], milestoneDef: DestinyMilestoneDefinition, ) { return rewards.flatMap((reward) => { const rewardCategoryDef = milestoneDef.rewards[reward.rewardCategoryHash]; return reward.entries.flatMap( (entry) => rewardCategoryDef.rewardEntries[entry.rewardEntryHash]?.items ?? [], ); }); } ================================================ FILE: src/app/progress/milestone.scss ================================================ @use '../variables.scss' as *; @layer base { // copied from resetButton in common.m.scss: button.milestone-quest { color: inherit; text-align: left; appearance: none; background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer; font-size: inherit; font-family: inherit; } .milestone-quest { display: flex; flex-direction: row; align-items: flex-start; align-self: start; transition: opacity 0.2s; overflow: hidden; user-select: text; .complete { color: #a1a2a2; } &.search-hidden { opacity: 0.2; } .milestone-icon { margin-right: 8px; display: flex; flex-direction: column; align-items: center; max-width: var(--item-size); .milestone-img { width: var(--item-size); height: var(--item-size); } span { font-size: 10px; text-align: center; } > span > span { white-space: nowrap; } .item { margin: 0; height: auto; } } .milestone-name { font-size: 14px; } // Apply hover style only on clickable milestone buttons &[type='button'] { @include interactive($hover: true) { .milestone-name { color: var(--theme-accent-primary); } } } .milestone-description { color: var(--theme-text-secondary); white-space: pre-wrap; line-height: 1.3; max-height: 3.9em; overflow: hidden; } .milestone-info { flex: 1; flex-basis: 0; display: flex; flex-direction: column; // HACK .item-details { margin: 2px 0 0 4px; } } } .quest-expiration { .app-icon { margin-right: 4px; } &.expires-soon { color: #c74141; } .milestone-info & { color: #999; font-weight: bold; font-size: 11px; float: right; margin-top: 2px; } } } ================================================ FILE: src/app/progress/selectors.ts ================================================ import { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; /** * get DestinyCharacterProgressionComponent from a DestinyProfileResponse. * if no character ID is specified, uses the "first" one in the character list */ export const getCharacterProgressions = ( profileResponse: DestinyProfileResponse | undefined, characterId?: string, ) => { // try to fill in missing character ID with a valid value characterId ??= profileResponse?.characterProgressions?.data ? Object.keys(profileResponse.characterProgressions.data)[0] : ''; return profileResponse?.characterProgressions?.data?.[characterId]; }; ================================================ FILE: src/app/progress/xp.ts ================================================ import { HashLookup } from 'app/utils/util-types'; /** Map inventory item ID to XP value */ export const xpItems: HashLookup = { 1858002338: 4000, // XP 3348653032: 6000, // XP+ 3582080006: 12000, // XP++ 2174060729: 25000, // Challenger XP 691393048: 50000, // Challenger XP+ 2250176030: 100000, // Challenger XP++ 1711513650: 200000, // Challenger XP+++ }; /** * If this is an XP item, return how much XP it grants. This is based on hardcoded community research. * https://www.reddit.com/r/DestinyTheGame/comments/pp4chw/how_many_xps_is/ */ export function getXPValue(hash: number) { return xpItems[hash]; } ================================================ FILE: src/app/records/Collectible.tsx ================================================ import { VendorItemDisplay } from 'app/vendors/VendorItemComponent'; import { DestinyCollectibleState } from 'bungie-api-ts/destiny2'; import { DimCollectible } from './presentation-nodes'; interface Props { collectible: DimCollectible; owned: boolean; } export default function Collectible({ collectible, owned }: Props) { const { state, item } = collectible; const acquired = !(state & DestinyCollectibleState.NotAcquired); return ( ); } ================================================ FILE: src/app/records/CollectiblesGrid.m.scss ================================================ .collectibles { display: grid; grid-template-columns: repeat(auto-fill, var(--item-size)); grid-template-rows: min-content; gap: 4px; margin: 6px 10px 8px; /* Collections leaf nodes need less padding */ :global(.always-expanded) & { margin: 2px 0 8px; } /* Set cards (armor sets, ornament sets): no extra margins inside the card padding */ :global(.set-card) & { margin: 0; } } ================================================ FILE: src/app/records/CollectiblesGrid.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'collectibles': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/CollectiblesGrid.tsx ================================================ import * as styles from './CollectiblesGrid.m.scss'; /** * A grid of items for collections, etc. * TODO: probably could be a generic item grid component */ export default function CollectiblesGrid({ children }: { children: React.ReactNode }) { return
{children}
; } ================================================ FILE: src/app/records/Craftable.tsx ================================================ import { VendorItemDisplay } from 'app/vendors/VendorItemComponent'; import { DimCraftable } from './presentation-nodes'; export default function Craftable({ craftable }: { craftable: DimCraftable }) { const { item, canCraftAllPlugs, canCraftThis } = craftable; return ( ); } ================================================ FILE: src/app/records/Metric.m.scss ================================================ @use '../variables.scss' as *; .metric { position: relative; box-sizing: border-box; background: rgb(255, 255, 255, 0.05); border: 1px solid #666; padding: 12px 12px 12px 8px; display: flex; flex-direction: row; align-items: flex-start; user-select: text; gap: 8px; } .masterworked { color: $gold; } .completed { border-color: $gold; } .info { flex: 1; } .name { font-size: 14px; } .value { float: right; font-weight: bold; font-size: 16px; text-align: right; margin-left: 4px; } .goal { font-size: 11px; font-weight: normal; color: var(--theme-text-secondary); text-align: right; } .icon { margin-top: -13px; } .description { display: flex; flex-direction: row; align-items: center; margin: 4px 0 0; color: var(--theme-text-secondary); img { filter: invert(0.7); } } .hasProgressBar { padding-bottom: 18px; } .progressBar { position: absolute; width: 100%; height: 6px; left: 0; bottom: 0; background-color: rgb(255, 255, 255, 0.1); &.complete { background-color: $xp; } } .progressFill { height: 100%; background-color: white; } ================================================ FILE: src/app/records/Metric.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'complete': string; 'completed': string; 'description': string; 'goal': string; 'hasProgressBar': string; 'icon': string; 'info': string; 'masterworked': string; 'metric': string; 'name': string; 'progressBar': string; 'progressFill': string; 'value': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/Metric.tsx ================================================ import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { getValueStyle, isObjectiveWithPlaceholderGoal } from 'app/inventory/store/objectives'; import { useD2Definitions } from 'app/manifest/selectors'; import { ObjectiveValue } from 'app/progress/Objective'; import { percent } from 'app/shell/formatters'; import { DestinyUnlockValueUIStyle } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import * as styles from './Metric.m.scss'; import MetricBanner from './MetricBanner'; import { DimMetric } from './presentation-nodes'; interface Props { metric: DimMetric; } export default function Metric({ metric }: Props) { const defs = useD2Definitions()!; const { metricDef, metricComponent } = metric; const metricHash = metricDef.hash; const name = metricDef.displayProperties.name; const description = metricDef.displayProperties.description; const masterwork = metricComponent.objectiveProgress.complete; const objectiveDef = defs.Objective.get(metricComponent.objectiveProgress.objectiveHash); const completionValue = objectiveDef?.completionValue ?? 0; const hasGoal = completionValue > 0 && !isObjectiveWithPlaceholderGoal(objectiveDef, completionValue); const progress = metricComponent.objectiveProgress.progress || 0; const hasProgressBar = hasGoal && getValueStyle(objectiveDef, progress, completionValue) !== DestinyUnlockValueUIStyle.TimeDuration; return (
{hasGoal && (
/
)}
{name}
{description && (

)}
{hasProgressBar && (
{!masterwork && (
)}
)}
); } ================================================ FILE: src/app/records/MetricBanner.m.scss ================================================ @use '../variables.scss' as *; .icon { position: relative; width: 21px; height: 53px; flex-shrink: 0; } .bannerIcon { position: absolute; width: 21px; } .scopeIcon { position: absolute; width: 19px; left: 1px; top: 19px; } .metricIcon { position: absolute; width: 19px; left: 1px; top: 34px; } ================================================ FILE: src/app/records/MetricBanner.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bannerIcon': string; 'icon': string; 'metricIcon': string; 'scopeIcon': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/MetricBanner.tsx ================================================ import { useD2Definitions } from 'app/manifest/selectors'; import { METRICS_ACCOUNT_NODE } from 'app/search/d2-known-values'; import { DestinyObjectiveProgress } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import masterworkOverlay from 'images/masterwork-metric.png'; import BungieImage from '../dim-ui/BungieImage'; import * as styles from './MetricBanner.m.scss'; import { getMetricTimeScope } from './presentation-nodes'; interface Props { metricHash: number; className?: string; objectiveProgress: DestinyObjectiveProgress; } export default function MetricBanner({ metricHash, objectiveProgress, className }: Props) { const defs = useD2Definitions()!; const metricDef = defs.Metric.get(metricHash); if (!metricDef) { return null; } const metricIcon = metricDef.displayProperties.icon; const metricScope = getMetricTimeScope(defs, metricDef); const parentNodeHash = metricDef.parentNodeHashes.length ? metricDef.parentNodeHashes[0] : METRICS_ACCOUNT_NODE; const parentNode = defs.PresentationNode.get(parentNodeHash); const bannerIcon = parentNode?.displayProperties.icon; const scopeIcon = metricScope.displayProperties.iconSequences[0].frames[2]; const masterwork = objectiveProgress.complete; return (
{bannerIcon && } {masterwork && }
); } ================================================ FILE: src/app/records/Metrics.m.scss ================================================ @use '../variables.scss' as *; .metrics { display: flex; flex-direction: column; margin: 6px 10px 8px; gap: 8px; } .metricsGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 8px; margin: 8px 0; @include phone-portrait { grid-template-columns: none; } } .title { composes: flexRow from '../dim-ui/common.m.scss'; color: var(--theme-text); font-size: 16px; text-transform: uppercase; align-items: center; img { height: 25px; width: 25px; margin-right: 4px; margin-left: 5px; } } ================================================ FILE: src/app/records/Metrics.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'metrics': string; 'metricsGrid': string; 'title': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/Metrics.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import { useD2Definitions } from 'app/manifest/selectors'; import Metric from './Metric'; import * as styles from './Metrics.m.scss'; import { DimMetric, getMetricTimeScope } from './presentation-nodes'; export default function Metrics({ metrics }: { metrics: DimMetric[] }) { const defs = useD2Definitions()!; const groupedMetrics = Map.groupBy(metrics, (m) => getMetricTimeScope(defs, m.metricDef)); return (
{[...groupedMetrics.entries()].map(([trait, metrics]) => (
{trait.displayProperties.name}
{metrics.map((metric) => ( ))}
))}
); } ================================================ FILE: src/app/records/PresentationNode.m.scss ================================================ @use '../variables.scss' as *; @use './set-card'; .presentationNode { /* indent child nodes more than their parent */ > * > * > .presentationNode { margin-left: 10px; } } /* Grid wrapper for CategorySets hierarchy nodes */ .categorySetGrid { @include set-card.set-card-grid; } /* The smaller headers for things like Armor and Weapons Collections leaf nodes */ .alwaysExpanded { display: flex; flex-direction: row; align-items: center; justify-content: space-between; margin: 0 16px 0 0; min-height: 34px; text-transform: uppercase; font-size: 14px; } .onlyChild { margin-left: 0 !important; } .nodeImg { height: 24px; vertical-align: middle; } /* styling for completed triumph categories */ .completed { color: $gold; opacity: 0.7; } h3.completed { background-color: rgb(245, 220, 86, 0.1) !important; } /* Styles for PresentationNodeProgress */ .nodeProgress { text-align: right; font-size: 12px; text-transform: none; letter-spacing: 0; } .nodeProgressBar { background: #666; height: 2px; width: 8rem; margin-top: 2px; } .nodeProgressBarAmount { background: #ccc; height: 100%; .completed & { background: $gold; } } .incompleteTitleIcon { filter: grayscale(1); } .isGilded { color: $sealtitle; &.gildedThisSeason { color: $gildedtitle; } .gildedNum { font-size: 10px; font-style: normal; margin-left: 1px; vertical-align: super; line-height: 0; } } ================================================ FILE: src/app/records/PresentationNode.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'alwaysExpanded': string; 'categorySetGrid': string; 'completed': string; 'gildedNum': string; 'gildedThisSeason': string; 'incompleteTitleIcon': string; 'isGilded': string; 'nodeImg': string; 'nodeProgress': string; 'nodeProgressBar': string; 'nodeProgressBarAmount': string; 'onlyChild': string; 'presentationNode': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/PresentationNode.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { CollapsedSection, Title } from 'app/dim-ui/CollapsibleTitle'; import { scrollToPosition } from 'app/dim-ui/scroll'; import { DimTitle } from 'app/inventory/store-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { percent } from 'app/shell/formatters'; import { emptyArray } from 'app/utils/empty'; import { DestinyPresentationScreenStyle } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { FontGlyphs } from 'data/font/d2-font-glyphs'; import { deepEqual } from 'fast-equals'; import { useEffect, useId, useRef } from 'react'; import { useSelector } from 'react-redux'; import BungieImage from '../dim-ui/BungieImage'; import * as styles from './PresentationNode.m.scss'; import PresentationNodeLeaf from './PresentationNodeLeaf'; import SetCard from './SetCard'; import { DimPresentationNode } from './presentation-nodes'; export default function PresentationNode({ node, ownedItemHashes, path, parents, onNodePathSelected, isInTriumphs, isRootNode, overrideName, }: { node: DimPresentationNode; ownedItemHashes?: Set; path: number[]; parents: number[]; isInTriumphs?: boolean; overrideName?: string; isRootNode?: boolean; onNodePathSelected: (nodePath: number[]) => void; }) { const defs = useD2Definitions()!; const redactedRecordsRevealed = useSelector(settingSelector('redactedRecordsRevealed')); const sortRecordProgression = useSelector(settingSelector('sortRecordProgression')); const presentationNodeHash = node.hash; const headerRef = useScrollNodeIntoView(path, presentationNodeHash); const expandChildren = () => { const childrenExpanded = path.includes(presentationNodeHash); onNodePathSelected(childrenExpanded ? parents : [...parents, presentationNodeHash]); return false; }; const { visible, acquired } = node; const completed = Boolean(acquired >= visible); const id = useId(); const contentId = `content-${id}`; const headerId = `header-${id}`; if (!visible) { return null; } const parent = parents.slice(-1)[0]; const thisAndParents = [...parents, presentationNodeHash]; // "CategorySet" DestinyPresentationScreenStyle is for armor sets const aParentIsCategorySetStyle = thisAndParents.some( (p) => p > 0 && defs.PresentationNode.get(p)?.screenStyle === DestinyPresentationScreenStyle.CategorySets, ); // CategorySets leaf nodes render as self-contained set cards if (aParentIsCategorySetStyle && !node.childPresentationNodes?.length) { return ( } complete={completed} > ); } // True when children are set cards, meaning this node should render them in a grid // and not use the alwaysExpanded header style itself. const childrenAreCategorySetLeaves = aParentIsCategorySetStyle && Boolean(node.childPresentationNodes?.length) && node.childPresentationNodes?.every((c) => !c.childPresentationNodes?.length); const alwaysExpanded = // if we're not in triumphs !isInTriumphs && // and 4 levels deep (e.g. collections > weapons > kinetic > auto rifles) !childrenAreCategorySetLeaves && thisAndParents.length >= 4; const onlyChild = // if this is a child of a child parents.length > 0 && // and has no siblings defs.PresentationNode.get(parent).children.presentationNodes.length === 1; /** whether this node's children are currently shown */ const childrenExpanded = isRootNode || onlyChild || path.includes(presentationNodeHash) || alwaysExpanded; const title = ( ); const titleClassName = clsx({ [styles.completed]: completed, }); const nodeProgress = ; return (
{alwaysExpanded ? (

{title} {nodeProgress}

) : ( !onlyChild && !isRootNode && ( ) )} <CollapsedSection collapsed={!childrenExpanded} headerId={headerId} contentId={contentId}> <div className={clsx({ [styles.categorySetGrid]: childrenAreCategorySetLeaves })}> {node.childPresentationNodes?.map((subNode) => ( <PresentationNode key={subNode.hash} node={subNode} ownedItemHashes={ownedItemHashes} path={path} parents={thisAndParents} onNodePathSelected={onNodePathSelected} isInTriumphs={isInTriumphs} /> ))} </div> {visible > 0 && ( <PresentationNodeLeaf node={node} ownedItemHashes={ownedItemHashes} redactedRecordsRevealed={redactedRecordsRevealed} sortRecordProgression={sortRecordProgression} /> )} </CollapsedSection> </div> ); } /** * Scrolls the given presentation node into view if it is not already. Assign * the returned headerRef to the header of the presentation node. */ function useScrollNodeIntoView(path: number[], presentationNodeHash: number) { const headerRef = useRef<HTMLDivElement>(null); const lastPath = useRef<number[]>(emptyArray()); useEffect(() => { if ( headerRef.current && path.at(-1) === presentationNodeHash && !deepEqual(lastPath.current, path) ) { const clientRect = headerRef.current.getBoundingClientRect(); if (clientRect.top < 50) { scrollToPosition({ top: window.scrollY + clientRect.top - 50, left: 0, behavior: 'smooth', }); } } lastPath.current = path; }, [path, presentationNodeHash]); return headerRef; } /** * The little progress bar in the header of a presentation node that shows how much has been unlocked. */ function PresentationNodeProgress({ acquired, visible }: { acquired: number; visible: number }) { return ( <div className={styles.nodeProgress}> <div> {acquired} / {visible} </div> <div className={styles.nodeProgressBar}> <div className={styles.nodeProgressBarAmount} style={{ width: percent(acquired / visible) }} /> </div> </div> ); } function PresentationNodeTitle({ displayProperties, overrideName, titleInfo, }: { displayProperties: { name: string; icon: string }; overrideName?: string; titleInfo?: DimTitle; }) { return ( <> {displayProperties.icon && ( <BungieImage src={displayProperties.icon} className={clsx(styles.nodeImg, { [styles.incompleteTitleIcon]: titleInfo && !titleInfo.isCompleted, })} /> )} {overrideName || displayProperties.name} {titleInfo && titleInfo.gildedNum > 0 && ( <> <span className={clsx(styles.isGilded, { [styles.gildedThisSeason]: titleInfo.isGildedForCurrentSeason, })} > {String.fromCodePoint(FontGlyphs.gilded_title)} {titleInfo.gildedNum > 1 && ( <span className={styles.gildedNum}>{titleInfo.gildedNum}</span> )} </span> </> )} </> ); } ================================================ FILE: src/app/records/PresentationNodeLeaf.tsx ================================================ import { compareBy } from 'app/utils/comparators'; import { VendorItemDisplay } from 'app/vendors/VendorItemComponent'; import { DestinyCollectibleState, DestinyRecordState } from 'bungie-api-ts/destiny2'; import Collectible from './Collectible'; import CollectiblesGrid from './CollectiblesGrid'; import Craftable from './Craftable'; import Metrics from './Metrics'; import { RecordGrid } from './Record'; import { DimCollectible, DimMetric, DimPresentationNodeLeaf, DimRecord, } from './presentation-nodes'; /** * Displays "leaf node" contents for presentation nodes (collectibles, triumphs, metrics) */ export default function PresentationNodeLeaf({ node, ownedItemHashes, redactedRecordsRevealed, sortRecordProgression, }: { node: DimPresentationNodeLeaf; ownedItemHashes?: Set<number>; redactedRecordsRevealed: boolean; sortRecordProgression: boolean; }) { return ( <> {node.collectibles && node.collectibles.length > 0 && ( <CollectiblesGrid> {(sortRecordProgression ? sortCollectibles(node.collectibles) : node.collectibles).map( (collectible) => ( <Collectible key={collectible.key} collectible={collectible} owned={Boolean(ownedItemHashes?.has(collectible.item.hash))} /> ), )} </CollectiblesGrid> )} {node.records && node.records.length > 0 && ( <RecordGrid records={sortRecordProgression ? sortRecords(node.records) : node.records} redactedRecordsRevealed={redactedRecordsRevealed} /> )} {node.metrics && node.metrics.length > 0 && ( <Metrics metrics={sortRecordProgression ? sortMetrics(node.metrics) : node.metrics} /> )} {node.craftables && node.craftables.length > 0 && ( <CollectiblesGrid> {node.craftables.map((craftable) => ( <Craftable key={craftable.item.hash} craftable={craftable} /> ))} </CollectiblesGrid> )} {node.plugs && node.plugs.length > 0 && ( <CollectiblesGrid> {node.plugs.map(({ item, unlocked }) => ( <VendorItemDisplay key={item.index} item={item} unavailable={!unlocked} owned={false} /> ))} </CollectiblesGrid> )} </> ); } function sortRecords(records: DimRecord[]): DimRecord[] { return records.toSorted( compareBy((record) => { // Triumph is already completed so move it to back of list. if ( record.recordComponent.state & DestinyRecordState.RecordRedeemed || record.recordComponent.state & DestinyRecordState.CanEquipTitle || !record.recordComponent.state ) { return 1; } // check which key is used to track progress let objectives; if (record.recordComponent.intervalObjectives) { objectives = record.recordComponent.intervalObjectives; } else if (record.recordComponent.objectives) { objectives = record.recordComponent.objectives; } else { // its a legacy triumph so it has no objectives and is not completed return 0; } // Sum up the progress let totalProgress = 0; for (const x of objectives) { totalProgress += Math.min(1, x.progress! / x.completionValue); } return -(totalProgress / objectives.length); }), ); } function sortCollectibles(collectibles: DimCollectible[]): DimCollectible[] { return collectibles.toSorted( compareBy((collectible) => { if (collectible.state & DestinyCollectibleState.NotAcquired) { return -1; } return 0; }), ); } function sortMetrics(metrics: DimMetric[]): DimMetric[] { return metrics.toSorted( compareBy((metric) => { const objectives = metric.metricComponent.objectiveProgress; if (objectives.complete) { return 1; } return -(objectives.progress! / objectives.completionValue); }), ); } ================================================ FILE: src/app/records/PresentationNodeRoot.m.scss ================================================ .root { margin-bottom: 16px; } ================================================ FILE: src/app/records/PresentationNodeRoot.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'root': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/PresentationNodeRoot.tsx ================================================ import { createItemContextSelector, currentStoreSelector } from 'app/inventory/selectors'; import { useD2Definitions } from 'app/manifest/selectors'; import { ItemFilter } from 'app/search/filter-types'; import { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import { useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import PresentationNode from './PresentationNode'; import * as styles from './PresentationNodeRoot.m.scss'; import PresentationNodeSearchResults from './PresentationNodeSearchResults'; import { makeItemsForCatalystRecords } from './catalysts'; import { filterPresentationNodesToSearch, hideCompletedRecords, toPresentationNodeTree, } from './presentation-nodes'; interface Props { presentationNodeHash: number; openedPresentationHash?: number; ownedItemHashes?: Set<number>; profileResponse: DestinyProfileResponse; searchQuery?: string; isTriumphs?: boolean; overrideName?: string; completedRecordsHidden?: boolean; /** Whether to show extra plugsets */ showPlugSets?: boolean; searchFilter?: ItemFilter; } const plugSetCollections = [ // Emotes { hash: 2860926541, displayItem: 3960522253 }, // Projections { hash: 2540258701, displayItem: 2544954628 }, ]; /** * The root for an expandable presentation node tree. */ export default function PresentationNodeRoot({ presentationNodeHash, openedPresentationHash, profileResponse, ownedItemHashes, showPlugSets, searchQuery, searchFilter, isTriumphs, overrideName, completedRecordsHidden, }: Props) { const itemCreationContext = useSelector(createItemContextSelector); const defs = useD2Definitions()!; const [nodePath, setNodePath] = useState<number[]>([]); let fullNodePath = nodePath; if (nodePath.length === 0 && openedPresentationHash) { let currentHash = openedPresentationHash; fullNodePath = [currentHash]; let node = defs.PresentationNode.get(currentHash); while (node.parentNodeHashes.length) { nodePath.unshift(node.parentNodeHashes[0]); currentHash = node.parentNodeHashes[0]; node = defs.PresentationNode.get(currentHash); } fullNodePath.unshift(presentationNodeHash); } const currentStore = useSelector(currentStoreSelector); const unfilteredNodeTree = useMemo( () => toPresentationNodeTree( itemCreationContext, presentationNodeHash, showPlugSets ? plugSetCollections : [], currentStore?.genderHash, ), [itemCreationContext, presentationNodeHash, showPlugSets, currentStore?.genderHash], ); const nodeTree = useMemo( () => unfilteredNodeTree && completedRecordsHidden ? hideCompletedRecords(unfilteredNodeTree) : unfilteredNodeTree, [completedRecordsHidden, unfilteredNodeTree], ); if (!nodeTree) { return null; } if (searchQuery && searchFilter) { const catalystItemsByRecordHash = makeItemsForCatalystRecords(itemCreationContext); const searchResults = filterPresentationNodesToSearch( nodeTree, searchQuery.toLowerCase(), searchFilter, undefined, defs, catalystItemsByRecordHash, ); return ( <PresentationNodeSearchResults searchResults={searchResults} ownedItemHashes={ownedItemHashes} profileResponse={profileResponse} /> ); } return ( <div className={styles.root}> <PresentationNode node={nodeTree} ownedItemHashes={ownedItemHashes} path={fullNodePath} onNodePathSelected={setNodePath} parents={[]} isRootNode={true} isInTriumphs={isTriumphs} overrideName={overrideName} /> </div> ); } ================================================ FILE: src/app/records/PresentationNodeSearchResults.m.scss ================================================ .path { composes: flexRow from '../dim-ui/common.m.scss'; list-style: none; padding: 0; margin: 16px 0 0 0; > li { text-transform: uppercase; letter-spacing: 2px; font-size: 14px; &::after { content: '>'; margin: 0 4px; } &:last-child::after { content: ''; } } } ================================================ FILE: src/app/records/PresentationNodeSearchResults.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'path': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/PresentationNodeSearchResults.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import { useSelector } from 'react-redux'; import PresentationNodeLeaf from './PresentationNodeLeaf'; import PresentationNodeRoot from './PresentationNodeRoot'; import * as styles from './PresentationNodeSearchResults.m.scss'; import { DimPresentationNodeSearchResult } from './presentation-nodes'; export default function PresentationNodeSearchResults({ searchResults, ownedItemHashes, profileResponse, }: { searchResults: DimPresentationNodeSearchResult[]; ownedItemHashes?: Set<number>; profileResponse: DestinyProfileResponse; }) { return ( <div> {searchResults.map((sr) => ( <PresentationNodeSearchResult key={sr.path.map((p) => p.hash).join('.')} sr={sr} ownedItemHashes={ownedItemHashes} profileResponse={profileResponse} /> ))} </div> ); } function PresentationNodeSearchResult({ sr, ownedItemHashes, profileResponse, }: { sr: DimPresentationNodeSearchResult; ownedItemHashes?: Set<number>; profileResponse: DestinyProfileResponse; }) { // TODO: make each node in path linkable const redactedRecordsRevealed = useSelector(settingSelector('redactedRecordsRevealed')); const sortRecordProgression = useSelector(settingSelector('sortRecordProgression')); const childNodes = !sr.collectibles && !sr.records && !sr.metrics && !sr.craftables && !sr.plugs && (() => { const node = sr.path.at(-1)!; return node.childPresentationNodes ? ( <PresentationNodeRoot presentationNodeHash={node.hash} ownedItemHashes={ownedItemHashes} profileResponse={profileResponse} /> ) : ( <PresentationNodeLeaf node={node} ownedItemHashes={ownedItemHashes} redactedRecordsRevealed={redactedRecordsRevealed} sortRecordProgression={sortRecordProgression} /> ); })(); return ( <div key={sr.path.map((p) => p.hash).join('.')}> <ul className={styles.path}> {sr.path.map((p, index) => index > 0 && <li key={p.hash}>{p.name}</li>)} </ul> <div> {childNodes} <PresentationNodeLeaf node={sr} ownedItemHashes={ownedItemHashes} redactedRecordsRevealed={redactedRecordsRevealed} sortRecordProgression={sortRecordProgression} /> </div> </div> ); } ================================================ FILE: src/app/records/Record.m.scss ================================================ @use 'sass:color'; @use '../variables.scss' as *; .recordsGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 8px; margin: 8px 0; @include phone-portrait { margin: 8px; grid-template-columns: none; } } .triumphRecord { position: relative; background: rgb(255, 255, 255, 0.05); border: 1px solid #666; padding: 12px; display: flex; flex-direction: row; align-items: flex-start; box-sizing: border-box; user-select: text; gap: 8px; @include phone-portrait { width: 100%; } h3 { margin: 0; font-size: 14px; font-weight: normal; } p { margin: 0 0 8px 0; color: var(--theme-text-secondary); white-space: pre-wrap; } p.gildingText { margin: 8px 0 0 0; color: var(--theme-text); } p + p.gildingText { margin: 0; } } .obscured h3 { color: #a1a2a2; } .unlocked { border-color: #56ecff; } .tracked { position: relative; border-color: #bdfc7f; outline: 1px solid #bdfc7f; background: linear-gradient( color.scale(#bdfc7f, $alpha: -100%) 0%, color.scale(#bdfc7f, $alpha: -90%) 100% ); } .multistep { padding-bottom: 18px; } .redeemed { border-color: var(--theme-record-redeemed); color: var(--theme-record-redeemed); background: transparent; p, span { color: var(--theme-record-redeemed); opacity: 0.8; } } .trackedInDim { position: relative; border-color: #f37423; outline: 1px solid #f37423; background: transparent; } .gildingTriumph { background-image: linear-gradient( to left bottom, transparent 25%, color.scale(#a1a2a2, $alpha: -80%) 1px, transparent calc(25% + 1px) ), linear-gradient( to left top, transparent 25%, color.scale(#a1a2a2, $alpha: -80%) 1px, transparent calc(25% + 1px) ); // To increase the number of chevrons in the background // decrease the first number of `background-size`, and vice versa for // decreasing. background-size: 60px 100%; // The following offset and repeat settings // prevent the two gradients making up the chevron pattern // (which both use reduced opacity colors) from overlapping // at the 0 point, which prevents that one point from being // more saturated than the rest of the lines. // There is no visible gap. background-position: 0 0, 0 2px; background-repeat: repeat-x; } .redeemed.gildingTriumph { border-color: $gilded-triumph-border-color; background-image: linear-gradient( to left bottom, transparent 25%, color.scale($gilded-triumph-border-color, $alpha: -80%) 1px, transparent calc(25% + 1px) ), linear-gradient( to left top, transparent 25%, color.scale($gilded-triumph-border-color, $alpha: -80%) 1px, transparent calc(25% + 1px) ); } .glow { display: none; width: 100%; height: 100%; position: absolute; top: 0; left: 0; } .trackedInDim .glow { display: block; background: linear-gradient(transparent 0%, var(--theme-accent-primary) 500%); } .redeemed.gildingTriumph .glow { display: block; background: linear-gradient( color.scale($gilded-triumph-border-color, $alpha: -100%) 80%, color.scale($gilded-triumph-border-color, $alpha: -90%) 100% ); } .redeemed .gildingText { opacity: 0.5; } // The "track triumph" icon that appears when you hover over a record. You can // click it to track the triumph. .dimTrackedIcon { position: absolute; display: none; right: calc(50px + var(--item-size) / 3.1); top: -9px; width: fit-content; height: fit-content; padding: 8px; transition: transform 300ms ease-in-out; transform-origin: center 8px; opacity: 0.7; @include interactive($hover: true) { transform: scale(1.5); } @media (hover: none) { display: block; opacity: 0.7; } img { width: calc(var(--item-size) / 3.1) !important; height: auto !important; } .trackedInDim & { display: block; opacity: 1; top: -10px; } // When the record is hovered, show the icon .triumphRecord:hover & { display: block; opacity: 0.7; } } .icon { display: block; width: 40px; height: auto; flex-shrink: 0; background: var(--theme-icon-tile); } .item { composes: resetButton from '../dim-ui/common.m.scss'; } .info { flex: 1; position: relative; } .recordLore { img { margin-right: 4px; vertical-align: bottom; } } .objectives { margin-top: 8px; } .score { float: right; color: var(--theme-text-secondary); margin-left: 4px; :global(.catalysts) & { display: none; } .currentScore { color: var(--theme-text); font-weight: bold; } .redeemed & { display: none; } } .trackedIcon { position: absolute; display: block; width: calc(var(--item-size) / 3.1) !important; height: auto !important; right: 50px; top: -2px; } .interval { height: 100%; background-color: rgb(255, 255, 255, 0.1); } .intervalUnlocked { background-color: white; } .intervalRedeemed { background-color: $xp; } .intervalContainer { position: absolute; width: 100%; height: 6px; left: 0; bottom: 0; display: flex; justify-content: space-between; &.complete { background-color: $xp; } .redeemed & { background-color: var(--theme-record-redeemed) !important; } } ================================================ FILE: src/app/records/Record.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'complete': string; 'currentScore': string; 'dimTrackedIcon': string; 'gildingText': string; 'gildingTriumph': string; 'glow': string; 'icon': string; 'info': string; 'interval': string; 'intervalContainer': string; 'intervalRedeemed': string; 'intervalUnlocked': string; 'item': string; 'multistep': string; 'objectives': string; 'obscured': string; 'recordLore': string; 'recordsGrid': string; 'redeemed': string; 'score': string; 'tracked': string; 'trackedIcon': string; 'trackedInDim': string; 'triumphRecord': string; 'unlocked': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/Record.tsx ================================================ import { trackTriumph } from 'app/dim-api/basic-actions'; import { trackedTriumphsSelector } from 'app/dim-api/selectors'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { t } from 'app/i18next-t'; import ItemPopupTrigger from 'app/inventory/ItemPopupTrigger'; import { createItemContextSelector } from 'app/inventory/selectors'; import { isBooleanObjective } from 'app/inventory/store/objectives'; import { useD2Definitions } from 'app/manifest/selectors'; import { Reward } from 'app/progress/Reward'; import { percent } from 'app/shell/formatters'; import { RootState } from 'app/store/types'; import { sumBy } from 'app/utils/collections'; import { HashLookup } from 'app/utils/util-types'; import { DestinyItemQuantity, DestinyObjectiveProgress, DestinyRecordComponent, DestinyRecordDefinition, DestinyRecordState, } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import catalystIcons from 'data/d2/catalyst-triumph-icons.json'; import dimTrackedIcon from 'images/dimTrackedIcon.svg'; import osteoStrigaCatalyst from 'images/osteo-striga-catalyst.jpg'; import trackedIcon from 'images/trackedIcon.svg'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import ishtarIcon from '../../images/ishtar-collective.svg'; import BungieImage from '../dim-ui/BungieImage'; import ExternalLink from '../dim-ui/ExternalLink'; import Objective from '../progress/Objective'; import * as styles from './Record.m.scss'; import { makeItemForCatalystRecord } from './catalysts'; import { DimRecord } from './presentation-nodes'; interface RecordInterval { objective: DestinyObjectiveProgress; score: number; percentCompleted: number; isRedeemed: boolean; rewards: DestinyItemQuantity[]; } const catalystIconsTable = catalystIcons as HashLookup<string>; function Record({ record, redactedRecordsRevealed, }: { record: DimRecord; redactedRecordsRevealed: boolean; }) { const defs = useD2Definitions()!; const { recordDef, trackedInGame, recordComponent } = record; const state = recordComponent.state; const recordHash = recordDef.hash; const dispatch = useDispatch(); const acquired = Boolean(state & DestinyRecordState.RecordRedeemed); const unlocked = !acquired && !(state & DestinyRecordState.ObjectiveNotCompleted); const obscured = !redactedRecordsRevealed && !unlocked && !acquired && Boolean(state & DestinyRecordState.Obscured); const trackedInDim = useSelector((state: RootState) => trackedTriumphsSelector(state).includes(recordHash), ); const loreLink = !obscured && recordDef.loreHash !== undefined && `http://www.ishtar-collective.net/entries/${recordDef.loreHash}`; const recordShouldGlow = (recordDef.forTitleGilding && acquired) || trackedInDim; const name = obscured ? recordDef.stateInfo.obscuredName : recordDef.displayProperties.name; const description = obscured ? recordDef.stateInfo.obscuredDescription : recordDef.displayProperties.description; const OSTEO_STRIGA_RECORD_HASH = 494981303; const isCatalyst = recordHash in catalystIconsTable; const recordIcon = recordHash === OSTEO_STRIGA_RECORD_HASH ? `~${osteoStrigaCatalyst}` : isCatalyst ? catalystIconsTable[recordHash] : recordDef.displayProperties.icon; const itemCreationContext = useSelector(createItemContextSelector); const catalystTarget = isCatalyst && makeItemForCatalystRecord(recordHash, itemCreationContext); const intervals = getIntervals(recordDef, recordComponent); const intervalBarStyle = { width: `calc((100% / ${intervals.length}) - 2px)`, }; const allIntervalsCompleted = intervals.every((i) => i.percentCompleted >= 1.0); const intervalProgressBar = !obscured && intervals.length > 0 && ( <div className={clsx(styles.intervalContainer, { [styles.complete]: allIntervalsCompleted, })} > {!allIntervalsCompleted && intervals.map((i) => { const redeemed = i.isRedeemed; const unlocked = i.percentCompleted >= 1.0; return ( <div key={i.objective.objectiveHash} className={clsx(styles.interval, { [styles.intervalRedeemed]: redeemed, [styles.intervalRedeemed]: unlocked && !redeemed, })} style={intervalBarStyle} > {!(redeemed || unlocked) && ( <div className={clsx(styles.interval, styles.intervalUnlocked)} style={{ width: percent(i.percentCompleted) }} /> )} </div> ); })} </div> ); let scoreValue = <>{t('Progress.RecordValue', { value: recordDef.completionInfo.ScoreValue })}</>; if (intervals.length > 1) { const currentScore = sumBy( intervals.slice(0, recordComponent.intervalsRedeemedCount), (i) => i.score, ); const totalScore = sumBy(intervals, (i) => i.score); scoreValue = ( <> <span className={styles.currentScore}>{currentScore}</span> /{' '} {t('Progress.RecordValue', { value: totalScore })} </> ); } const objectives = intervals.length > 0 ? [ intervals[Math.min(recordComponent.intervalsRedeemedCount, intervals.length - 1)] .objective, ] : recordComponent.objectives; const rewards = intervals.length > 0 ? intervals[Math.min(recordComponent.intervalsRedeemedCount, intervals.length - 1)].rewards : recordDef.rewardItems; const showObjectives = !obscured && objectives && ((!obscured && objectives.length > 1) || (objectives.length === 1 && !isBooleanObjective( defs.Objective.get(objectives[0].objectiveHash), objectives[0].progress ?? 0, objectives[0].completionValue, ))); // TODO: show track badge greyed out / on hover // TODO: track on click const toggleTracked = (e: React.MouseEvent) => { e.stopPropagation(); dispatch(trackTriumph({ recordHash, tracked: !trackedInDim })); }; return ( <div className={clsx(styles.triumphRecord, { [styles.redeemed]: acquired, [styles.unlocked]: unlocked, [styles.gildingTriumph]: recordDef.forTitleGilding, [styles.obscured]: obscured, [styles.tracked]: trackedInGame, [styles.trackedInDim]: trackedInDim, [styles.multistep]: intervals.length > 0, })} > {recordShouldGlow && <div className={styles.glow} />} {catalystTarget && recordIcon ? ( <ItemPopupTrigger item={catalystTarget}> {(ref, onClick) => ( <div className={styles.item} role="button" ref={ref} onClick={onClick}> <BungieImage className={styles.icon} src={recordIcon} /> </div> )} </ItemPopupTrigger> ) : ( recordIcon && <BungieImage className={styles.icon} src={recordIcon} /> )} <div className={styles.info}> {!obscured && recordDef.completionInfo && <div className={styles.score}>{scoreValue}</div>} <h3>{name}</h3> {description && ( <p> <RichDestinyText text={description} /> </p> )} {showObjectives && ( <div className={styles.objectives}> {objectives.map((objective) => ( <Objective key={objective.objectiveHash} objective={objective} /> ))} </div> )} {loreLink && ( <div className={styles.recordLore}> <ExternalLink href={loreLink}> <img src={ishtarIcon} height="16" width="16" /> </ExternalLink> <ExternalLink href={loreLink}>{t('MovePopup.ReadLore')}</ExternalLink> </div> )} {rewards && !acquired && !obscured && rewards.map((reward) => <Reward key={reward.itemHash} reward={reward} />)} {recordDef.forTitleGilding && !obscured && ( <p className={styles.gildingText}>{t('Triumphs.GildingTriumph')}</p> )} {trackedInGame && <img className={styles.trackedIcon} src={trackedIcon} />} </div> {(!acquired || trackedInDim) && ( <div role="button" onClick={toggleTracked} className={styles.dimTrackedIcon}> <img src={dimTrackedIcon} /> </div> )} {intervalProgressBar} </div> ); } function getIntervals( definition: DestinyRecordDefinition, record: DestinyRecordComponent, ): RecordInterval[] { const intervalDefinitions = definition?.intervalInfo?.intervalObjectives || []; const intervalObjectives = record?.intervalObjectives || []; const intervalRewards = definition?.intervalInfo?.intervalRewards || []; if (intervalDefinitions.length !== intervalObjectives.length) { return []; } const intervals: RecordInterval[] = []; let isPrevIntervalComplete = true; let prevIntervalProgress = 0; for (let i = 0; i < intervalDefinitions.length; i++) { const def = intervalDefinitions[i]; const data = intervalObjectives[i]; const rewards = intervalRewards[i].intervalRewardItems; intervals.push({ objective: data, score: def.intervalScoreValue, percentCompleted: isPrevIntervalComplete ? data.complete ? 1 : Math.max( 0, ((data.progress || 0) - prevIntervalProgress) / (data.completionValue - prevIntervalProgress), ) : 0, isRedeemed: record.intervalsRedeemedCount >= i + 1, rewards, }); isPrevIntervalComplete = data.complete; prevIntervalProgress = data.completionValue; } return intervals; } /** A grid of records as seen in triumph presentation nodes or Tracked Triumphs. */ export function RecordGrid({ records, redactedRecordsRevealed, }: { records: DimRecord[]; redactedRecordsRevealed: boolean; }) { // TODO: was there really a problem with duplicate records? const seenRecords = new Set<number>(); return ( <div className={styles.recordsGrid}> {records.map((record) => { if (seenRecords.has(record.recordDef.hash)) { return null; } seenRecords.add(record.recordDef.hash); return ( <Record key={record.recordDef.hash} record={record} redactedRecordsRevealed={redactedRecordsRevealed} /> ); })} </div> ); } ================================================ FILE: src/app/records/Records.m.scss ================================================ @use '../variables' as *; .page { user-select: text; } .presentationNodeOptions { display: flex; flex-direction: column; gap: 8px; @include phone-portrait { padding: 8px 10px; } } ================================================ FILE: src/app/records/Records.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'page': string; 'presentationNodeOptions': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/Records.tsx ================================================ import CheckButton from 'app/dim-ui/CheckButton'; import CollapsibleTitle from 'app/dim-ui/CollapsibleTitle'; import PageWithMenu from 'app/dim-ui/PageWithMenu'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { useLoadStores } from 'app/inventory/store/hooks'; import { destiny2CoreSettingsSelector, useD2Definitions } from 'app/manifest/selectors'; import { TrackedTriumphs } from 'app/progress/TrackedTriumphs'; import { searchFilterSelector } from 'app/search/items/item-search-filter'; import { useSetting } from 'app/settings/hooks'; import { querySelector } from 'app/shell/selectors'; import { compact, filterMap } from 'app/utils/collections'; import { usePageTitle } from 'app/utils/hooks'; import { useSelector } from 'react-redux'; import { useSearchParams } from 'react-router'; import { DestinyAccount } from '../accounts/destiny-account'; import ErrorBoundary from '../dim-ui/ErrorBoundary'; import { bucketsSelector, ownedItemsSelector, profileResponseSelector, } from '../inventory/selectors'; import { UNIVERSAL_ORNAMENTS_NODE } from '../search/d2-known-values'; import PresentationNodeRoot from './PresentationNodeRoot'; import * as styles from './Records.m.scss'; import UniversalOrnaments from './universal-ornaments/UniversalOrnaments'; interface Props { account: DestinyAccount; } /** * The records screen shows account-wide things like Triumphs and Collections. */ export default function Records({ account }: Props) { useLoadStores(account); const [searchParams] = useSearchParams(); usePageTitle(t('Records.Title')); const presentationNodeHash = searchParams.has('presentationNodeHash') ? parseInt(searchParams.get('presentationNodeHash')!, 10) : undefined; const buckets = useSelector(bucketsSelector); const ownedItemHashes = useSelector(ownedItemsSelector); const profileResponse = useSelector(profileResponseSelector); const searchQuery = useSelector(querySelector); const searchFilter = useSelector(searchFilterSelector); const destiny2CoreSettings = useSelector(destiny2CoreSettingsSelector); const [completedRecordsHidden, setCompletedRecordsHidden] = useSetting('completedRecordsHidden'); const [redactedRecordsRevealed, setRedactedRecordsRevealed] = useSetting('redactedRecordsRevealed'); const [sortRecordProgression, setSortRecordProgression] = useSetting('sortRecordProgression'); const defs = useD2Definitions(); if (!profileResponse || !defs || !buckets) { return <ShowPageLoading message={t('Loading.Profile')} />; } // Some root nodes come from the profile const badgesRootNodeHash = profileResponse?.profileCollectibles?.data?.collectionBadgesRootNodeHash; const metricsRootNodeHash = profileResponse?.metrics?.data?.metricsRootNodeHash; const collectionsRootHash = profileResponse?.profileCollectibles?.data?.collectionCategoriesRootNodeHash; const recordsRootHash = profileResponse?.profileRecords?.data?.recordCategoriesRootNodeHash; const sealsRootHash = profileResponse?.profileRecords?.data?.recordSealsRootNodeHash; const seasonalChallengesHash = destiny2CoreSettings?.seasonalChallengesPresentationNodeHash || 0; const profileHashes = compact([ seasonalChallengesHash, recordsRootHash, sealsRootHash, collectionsRootHash, badgesRootNodeHash, metricsRootNodeHash, ]); // Some nodes have bad titles, we manually fix them const overrideTitles: { [nodeHash: number]: string } = {}; if (collectionsRootHash) { overrideTitles[collectionsRootHash] = t('Vendors.Collections'); } if (metricsRootNodeHash) { overrideTitles[metricsRootNodeHash] = t('Progress.StatTrackers'); } // We discover the rest of the root nodes from the Bungie.net core settings const otherHashes = destiny2CoreSettings ? filterMap(Object.entries(destiny2CoreSettings), ([key, value]) => key.includes('RootNode') && key !== 'craftingRootNodeHash' && typeof value === 'number' ? value : undefined, ) : []; const universalOrnamentsName = defs.PresentationNode.get(UNIVERSAL_ORNAMENTS_NODE)?.displayProperties.name ?? '???'; // We put the hashes we know about from profile first const nodeHashes = [...new Set([...profileHashes, ...otherHashes])]; const presentationNodes = nodeHashes.map((h) => defs.PresentationNode.get(h)).filter(Boolean); const menuItems = [ { id: 'trackedTriumphs', title: t('Progress.TrackedTriumphs') }, ...presentationNodes.filter(Boolean).map((nodeDef) => ({ id: `p_${nodeDef.hash}`, title: overrideTitles[nodeDef.hash] || nodeDef.displayProperties.name, })), { id: 'universalOrnaments', title: universalOrnamentsName }, ]; return ( <PageWithMenu className="d2-vendors"> <PageWithMenu.Menu> {menuItems.map((menuItem) => ( <PageWithMenu.MenuButton key={menuItem.id} anchor={menuItem.id}> <span>{menuItem.title}</span> </PageWithMenu.MenuButton> ))} <div className={styles.presentationNodeOptions}> <CheckButton name="hide-completed" checked={completedRecordsHidden} onChange={setCompletedRecordsHidden} > {t('Triumphs.HideCompleted')} </CheckButton> <CheckButton name="reveal-redacted" checked={redactedRecordsRevealed} onChange={setRedactedRecordsRevealed} > {t('Triumphs.RevealRedacted')} </CheckButton> <CheckButton name="sort-progression" checked={sortRecordProgression} onChange={setSortRecordProgression} > {t('Triumphs.SortRecords')} </CheckButton> </div> </PageWithMenu.Menu> <PageWithMenu.Contents className={styles.page}> <section id="trackedTriumphs"> <CollapsibleTitle title={t('Progress.TrackedTriumphs')} sectionId="trackedTriumphs"> <TrackedTriumphs searchQuery={searchQuery} /> </CollapsibleTitle> </section> {presentationNodes.map((nodeDef) => ( <section key={nodeDef.hash} id={`p_${nodeDef.hash}`}> <CollapsibleTitle title={overrideTitles[nodeDef.hash] || nodeDef.displayProperties.name} sectionId={`p_${nodeDef.hash}`} > <PresentationNodeRoot presentationNodeHash={nodeDef.hash} profileResponse={profileResponse} ownedItemHashes={ownedItemHashes.accountWideOwned} openedPresentationHash={presentationNodeHash} searchQuery={searchQuery} searchFilter={searchFilter} overrideName={overrideTitles[nodeDef.hash]} isTriumphs={nodeDef.hash === recordsRootHash} showPlugSets={nodeDef.hash === collectionsRootHash} completedRecordsHidden={completedRecordsHidden} /> </CollapsibleTitle> </section> ))} <section id="universalOrnaments"> <CollapsibleTitle title={universalOrnamentsName} sectionId="universalOrnaments"> <ErrorBoundary name={universalOrnamentsName}> <UniversalOrnaments searchQuery={searchQuery} searchFilter={searchFilter} /> </ErrorBoundary> </CollapsibleTitle> </section> </PageWithMenu.Contents> </PageWithMenu> ); } ================================================ FILE: src/app/records/SetCard.m.scss ================================================ .card { display: flex; flex-direction: column; padding: 8px; box-sizing: border-box; position: relative; background: rgb(255, 255, 255, 0.05); &.complete { background: transparent; .title { color: var(--theme-record-redeemed); } &::before { content: ''; position: absolute; inset: 0; background: var(--theme-record-redeemed); opacity: 0.15; pointer-events: none; } } } .title { margin: 0 0 5px 0; font-size: 14px; font-weight: normal; text-transform: uppercase; } ================================================ FILE: src/app/records/SetCard.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'card': string; 'complete': string; 'title': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/SetCard.tsx ================================================ import clsx from 'clsx'; import * as styles from './SetCard.m.scss'; /** * A card for displaying a set of items (armor sets, ornament sets, etc.) * with a title and optional completed state. */ export default function SetCard({ title, complete = false, children, }: { title: React.ReactNode; complete?: boolean; children: React.ReactNode; }) { return ( <div className={clsx(styles.card, { [styles.complete]: complete }, 'set-card')}> <h4 className={styles.title}>{title}</h4> {children} </div> ); } ================================================ FILE: src/app/records/_set-card.scss ================================================ /// Shared grid layout for set card displays (armor sets, ornament sets, etc.) @mixin set-card-grid { display: grid; // 5 items + 4 gaps (4px each) + card padding (8px each side) grid-template-columns: repeat( auto-fill, minmax(min(calc(var(--item-size) * 5 + 32px), 100%), 1fr) ); gap: 8px; margin: 8px 4px; } ================================================ FILE: src/app/records/catalysts.ts ================================================ import { ItemCreationContext, makeFakeItem } from 'app/inventory/store/d2-item-factory'; import { invert } from 'app/utils/collections'; import exoticToCatalystRecordHash from 'data/d2/exotic-to-catalyst-record.json'; export function makeItemsForCatalystRecords(itemCreationContext: ItemCreationContext) { return new Map( Object.entries(exoticToCatalystRecordHash).map(([itemHash, recordHash]) => [ recordHash!, makeFakeItem(itemCreationContext, parseInt(itemHash, 10))!, ]), ); } const catalystRecordHashToExoticHash = invert(exoticToCatalystRecordHash, Number); export function makeItemForCatalystRecord( recordHash: number, itemCreationContext: ItemCreationContext, ) { const exoticHash = catalystRecordHashToExoticHash[recordHash]; if (exoticHash) { return makeFakeItem(itemCreationContext, exoticHash)!; } } ================================================ FILE: src/app/records/collectible-matching.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { ARMOR_NODE } from 'app/search/d2-known-values'; import { invert } from 'app/utils/collections'; import { DestinyClass, DestinyCollectibleDefinition, DestinyInventoryItemDefinition, DestinyPresentationNodeDefinition, } from 'bungie-api-ts/destiny2'; import focusingItemOutputs from 'data/d2/focusing-item-outputs.json'; import extraItemCollectibles from 'data/d2/unreferenced-collections-items.json'; import { keyBy, once } from 'es-toolkit'; import memoizeOne from 'memoize-one'; // For some items, the most recent versions don't have a collectible. d2ai gives us the most // recent item hash for given collectibles, so the inverted map gives us the collectible // for some items. const extraItemsToCollectibles = invert(extraItemCollectibles, Number); function collectPresentationNodes( defs: D2ManifestDefinitions, nodeHash: number, list: DestinyPresentationNodeDefinition[], ) { const def = defs.PresentationNode.get(nodeHash); if (def && !def.redacted) { if (def.children.collectibles.length) { list.push(def); } for (const childNode of def.children.presentationNodes) { collectPresentationNodes(defs, childNode.presentationNodeHash, list); } } return list; } /** * Finds the most likely collectible for a given item definition. Additionally pass in the classType * to be able to match armor ornaments that don't indicate a classType by themselves. */ export const createCollectibleFinder = memoizeOne((defs: D2ManifestDefinitions) => { const cache: { [itemHash: number]: DestinyCollectibleDefinition | null } = {}; const armorCollectiblesByClassType = once(() => { // The Titan/Hunter/Warlock armor presentation nodes, in that order (matches enum order) const classPresentationNodes = defs.PresentationNode.get(ARMOR_NODE).children.presentationNodes; return Object.fromEntries( [DestinyClass.Titan, DestinyClass.Hunter, DestinyClass.Warlock].map((classType) => { const classNode = classPresentationNodes[classType]; const relevantPresentationNodes = collectPresentationNodes( defs, classNode.presentationNodeHash, [], ); const collectibles = relevantPresentationNodes .flatMap((node) => node.children?.collectibles ?? []) .map((c) => defs.Collectible.get(c.collectibleHash)); // Most of the time, collectibles will have the same name const collectiblesByName = keyBy(collectibles, (c) => c.displayProperties.name); // Sometimes the collectible name and icon will be different, but reference an item // where the icon matches our icon (e.g. Y1 Trials / Prophecy: Bond Judgment vs. Judgement's Wrap) const collectiblesByReverseItemIcon = keyBy( collectibles, (c) => defs.InventoryItem.get(c.itemHash)?.displayProperties.icon, ); return [classType, { collectiblesByName, collectiblesByReverseItemIcon }] as const; }), ); }); return ( itemDef: DestinyInventoryItemDefinition, knownClassType?: DestinyClass, ): DestinyCollectibleDefinition | undefined => { const cacheEntry = cache[itemDef.hash]; if (cacheEntry !== undefined) { return cacheEntry ?? undefined; } const collectible = (() => { // If this is a fake focusing item, the item we're actually interested in is the output const itemHash = focusingItemOutputs[itemDef.hash] ?? itemDef.hash; const outputItemDef = defs.InventoryItem.get(itemHash); if (!outputItemDef) { return undefined; } // If this has a collectible hash, use that if (outputItemDef.collectibleHash) { return defs.Collectible.get(outputItemDef.collectibleHash); } // For some items, d2ai knows what the collectible is if (extraItemsToCollectibles[itemHash]) { return defs.Collectible.get(extraItemsToCollectibles[itemHash]); } // Otherwise we try some fuzzy matching with the collectibles. // This currently only handles armor, and needs a classType to // get the correct armor presentation node. This could potentially // be extended to weapons too, but we've not had many problems with weapon // collectibles yet and especially in the case of universal ornaments // the ornaments themselves don't have a good ICH or classType const classType = knownClassType ?? outputItemDef.classType; if (!outputItemDef.redacted && classType !== DestinyClass.Unknown) { const { collectiblesByName, collectiblesByReverseItemIcon } = armorCollectiblesByClassType()[classType]; return ( collectiblesByName[outputItemDef.displayProperties.name] ?? collectiblesByReverseItemIcon[outputItemDef.displayProperties.icon] ); } })(); cache[itemDef.hash] = collectible ?? null; return collectible; }; }); ================================================ FILE: src/app/records/extra-collectibles.d.ts ================================================ declare module 'data/d2/unreferenced-collections-items.json' { const x: { readonly [collectibleHash: number]: number }; export default x; } ================================================ FILE: src/app/records/plugset-helpers.ts ================================================ import { stubTrue } from 'app/utils/functions'; import { DestinyItemPlug, DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import universalOrnamentPlugSetHashes from 'data/d2/universal-ornament-plugset-hashes.json'; /** * Get all plugs from the specified plugset. This includes whether the plugs are unlocked or not. * This returns unlocked plugs for a specific character or account-wide. */ function itemsForCharacterOrProfilePlugSet( profileResponse: DestinyProfileResponse, plugSetHash: number, characterId: string, ) { return (profileResponse.profilePlugSets.data?.plugs[plugSetHash] ?? []).concat( profileResponse.characterPlugSets.data?.[characterId]?.plugs[plugSetHash] ?? [], ); } const HARMONIC_RESISTANCE = 1293710444; // InventoryItem "Harmonic Resistance" export function filterUnlockedPlugs( plugSetHash: number, plugSetItems: DestinyItemPlug[], outUnlockedPlugs: Set<number>, predicate: (plug: DestinyItemPlug) => boolean = stubTrue, ) { const useCanInsert = universalOrnamentPlugSetHashes.includes(plugSetHash); for (const plugSetItem of plugSetItems) { if ( ((useCanInsert ? plugSetItem.canInsert : plugSetItem.enabled) || // Harmonic Resistance may report as disabled when the user // has Strand equipped since in that case, it provides... (plugSetItem.plugItemHash === HARMONIC_RESISTANCE && plugSetItem.enableFailIndexes.length === 1 && // ..."No Current Benefit" plugSetItem.enableFailIndexes[0] === 2)) && predicate(plugSetItem) ) { outUnlockedPlugs.add(plugSetItem.plugItemHash); } } } /** * The set of plug item hashes that are unlocked in the given plugset by the given character. * TODO: would be great to precalculate/memoize this by character ID and profileResponse */ export function unlockedItemsForCharacterOrProfilePlugSet( profileResponse: DestinyProfileResponse, plugSetHash: number, characterId: string, ): Set<number> { const unlockedPlugs = new Set<number>(); const plugSetItems = itemsForCharacterOrProfilePlugSet(profileResponse, plugSetHash, characterId); filterUnlockedPlugs(plugSetHash, plugSetItems, unlockedPlugs); return unlockedPlugs; } ================================================ FILE: src/app/records/presentation-nodes.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { ItemCreationContext, makeFakeItem } from 'app/inventory/store/d2-item-factory'; import { ItemFilter } from 'app/search/filter-types'; import { compact, count, filterMap } from 'app/utils/collections'; import extraItemCollectibles from 'data/d2/unreferenced-collections-items.json'; import { DimTitle } from 'app/inventory/store-types'; import { getTitleInfo } from 'app/inventory/store/d2-store-factory'; import { compareBy } from 'app/utils/comparators'; import { DestinyCollectibleDefinition, DestinyCollectibleState, DestinyCraftableComponent, DestinyDisplayPropertiesDefinition, DestinyMetricComponent, DestinyMetricDefinition, DestinyPresentationNodeCollectibleChildEntry, DestinyPresentationNodeComponent, DestinyPresentationNodeCraftableChildEntry, DestinyPresentationNodeDefinition, DestinyPresentationNodeMetricChildEntry, DestinyPresentationNodeRecordChildEntry, DestinyPresentationNodeState, DestinyProfileResponse, DestinyRecordComponent, DestinyRecordDefinition, DestinyRecordState, DestinyScope, DestinyTraitDefinition, } from 'bungie-api-ts/destiny2'; import { TraitHashes } from 'data/d2/generated-enums'; import { minBy } from 'es-toolkit'; import { unlockedItemsForCharacterOrProfilePlugSet } from './plugset-helpers'; export interface DimPresentationNodeLeaf { records?: DimRecord[]; collectibles?: DimCollectible[]; metrics?: DimMetric[]; craftables?: DimCraftable[]; plugs?: DimCollectiblePlug[]; } export interface DimPresentationNode extends DimPresentationNodeLeaf { /** * The node definition may be missing if it's one of the fake nodes for PlugSets. * The required properties `hash`, `name` and `icon` are derived from the def * or generated with fake info. */ nodeDef: DestinyPresentationNodeDefinition | undefined; nodeComponent: DestinyPresentationNodeComponent | undefined; /** May or may not be an actual hash */ hash: number; name: string; icon: string; visible: number; acquired: number; childPresentationNodes?: DimPresentationNode[]; /** * For seals, the title info. */ titleInfo?: DimTitle; } export interface DimRecord { recordComponent: DestinyRecordComponent; recordDef: DestinyRecordDefinition; trackedInGame: boolean; } export interface DimMetric { metricComponent: DestinyMetricComponent; metricDef: DestinyMetricDefinition; } export interface DimCollectible { state: DestinyCollectibleState; collectibleDef: DestinyCollectibleDefinition; item: DimItem; key: string; /** * true if this was artificially created by DIM. * some items are missing in collectibles, and we can fix that, * but they shouldn't be counted toward completion meters * or they'll seem wrong compared to in-game collections */ fake: boolean; } export interface DimCraftable { // to-do: determine what interesting information we can share about a craftable item: DimItem; canCraftThis: boolean; canCraftAllPlugs: boolean; } export interface DimCollectiblePlug { item: DimItem; unlocked: boolean; } export interface DimPresentationNodeSearchResult extends DimPresentationNodeLeaf { /** The sequence of nodes from outside to inside ending in the leaf node that contains our matching records/collectibles/metrics */ path: DimPresentationNode[]; } /** Process the live data into DIM types that collect everything in one place and can be filtered/searched. */ export function toPresentationNodeTree( itemCreationContext: ItemCreationContext, node: number, plugSetCollections?: { hash: number; displayItem: number }[], genderHash?: number, ): DimPresentationNode | null { const { defs, buckets, profileResponse } = itemCreationContext; const presentationNodeDef = defs.PresentationNode.get(node); if (presentationNodeDef.redacted) { 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, profileResponse.profileRecords.data, genderHash, ) : undefined; const commonNodeProperties = { nodeDef: presentationNodeDef, hash: presentationNodeDef.hash, name: titleInfo?.title || presentationNodeDef.displayProperties.name, icon: presentationNodeDef.displayProperties.icon, titleInfo, nodeComponent, }; if (presentationNodeDef.children.collectibles?.length) { const collectibles = toCollectibles( itemCreationContext, presentationNodeDef.children.collectibles, ); const visible = count(collectibles, (c) => !c.fake); const acquired = count( collectibles, (c) => !c.fake && !(c.state & DestinyCollectibleState.NotAcquired), ); // add an entry for self and return return { ...commonNodeProperties, visible, acquired, collectibles, }; } else if (presentationNodeDef.children.records?.length) { const records = toRecords(defs, profileResponse, presentationNodeDef.children.records); const visible = records.length; const acquired = count(records, (r) => Boolean(r.recordComponent.state & DestinyRecordState.RecordRedeemed), ); // add an entry for self and return return { ...commonNodeProperties, visible, acquired, records, }; } else if (buckets && presentationNodeDef.children.craftables?.length) { const craftables = toCraftables(itemCreationContext, presentationNodeDef.children.craftables); const visible = craftables.length; const acquired = count(craftables, (c) => c.canCraftThis); // add an entry for self and return return { ...commonNodeProperties, visible, acquired, craftables, }; } else if (presentationNodeDef.children.metrics?.length) { const metrics = toMetrics(defs, profileResponse, presentationNodeDef.children.metrics); // TODO: class based on displayStyle const visible = metrics.length; const acquired = count(metrics, (m) => Boolean(m.metricComponent.objectiveProgress.complete)); return { ...commonNodeProperties, visible, acquired, metrics, }; } else { // call for all children, then add 'em up const children: DimPresentationNode[] = []; let acquired = 0; let visible = 0; for (const presentationNode of presentationNodeDef.children.presentationNodes) { const subnode = toPresentationNodeTree( itemCreationContext, presentationNode.presentationNodeHash, undefined, genderHash, ); if (subnode) { acquired += subnode.acquired; visible += subnode.visible; children.push(subnode); } } if (plugSetCollections) { for (const collection of plugSetCollections) { // Explicitly do not include counts in parent counts, since it would differ from // the numbers shown in-game children.push(buildPlugSetPresentationNode(itemCreationContext, collection)); } } return { ...commonNodeProperties, visible, acquired, childPresentationNodes: children, }; } } function buildPlugSetPresentationNode( itemCreationContext: ItemCreationContext, { hash, displayItem }: { hash: number; displayItem: number }, ): DimPresentationNode { const plugSetDef = itemCreationContext.defs.PlugSet.get(hash); const item = itemCreationContext.defs.InventoryItem.get(displayItem); const unlockedItems = unlockedItemsForCharacterOrProfilePlugSet( itemCreationContext.profileResponse, hash, '', ); const plugSetItems = filterMap(plugSetDef.reusablePlugItems, (i) => makeFakeItem(itemCreationContext, i.plugItemHash), ); const plugEntries = plugSetItems.map((item) => ({ item, unlocked: unlockedItems.has(item.hash), })); const acquired = count(plugEntries, (i) => i.unlocked); const subnode: DimPresentationNode = { nodeDef: undefined, hash: -hash, name: item.displayProperties.name, icon: item.displayProperties.icon, visible: plugSetItems.length, acquired, plugs: plugEntries, nodeComponent: undefined, }; return subnode; } function dropEmptyNodes(node: DimPresentationNode | undefined): DimPresentationNode | undefined { if (!node) { return undefined; } const children = node.collectibles ?? node.craftables ?? node.records ?? node.metrics ?? node.plugs ?? node.childPresentationNodes; if (children?.length) { return node; } else { return undefined; } } export function hideCompletedRecords(node: DimPresentationNode): DimPresentationNode { if (node.childPresentationNodes) { return { ...node, childPresentationNodes: filterMap(node.childPresentationNodes, (node) => dropEmptyNodes(hideCompletedRecords(node)), ), }; } if (node.records) { return { ...node, records: node.records.filter( (r) => !(r.recordComponent.state & DestinyRecordState.RecordRedeemed), ), }; } return node; } // TODO: how to flatten this down to individual category trees // TODO: how to handle simple searches plus bigger queries // TODO: this uses the entire search field as one big string search. no "and". no fun. export function filterPresentationNodesToSearch( node: DimPresentationNode, searchQuery: string, filterItems: ItemFilter, path: DimPresentationNode[] = [], defs: D2ManifestDefinitions, catalystItemsByRecordHash: Map<number, DimItem>, ): DimPresentationNodeSearchResult[] { // If the node itself matches if (searchNode(node, searchQuery)) { // Return this whole node return [{ path: [...path, node] }]; } if (node.childPresentationNodes) { // TODO: build up the tree? return node.childPresentationNodes.flatMap((c) => filterPresentationNodesToSearch( c, searchQuery, filterItems, [...path, node], defs, catalystItemsByRecordHash, ), ); } if (node.collectibles) { const collectibles = node.collectibles.filter((c) => filterItems(c.item)); return collectibles.length ? [ { path: [...path, node], collectibles, }, ] : []; } if (node.records) { const records = node.records.filter( (r) => searchDisplayProperties(r.recordDef.displayProperties, searchQuery) || searchRewards(r.recordDef, searchQuery, defs) || searchCatalysts(r.recordDef.hash, filterItems, catalystItemsByRecordHash), ); return records.length ? [ { path: [...path, node], records, }, ] : []; } if (node.metrics) { const metrics = node.metrics.filter((r) => searchDisplayProperties(r.metricDef.displayProperties, searchQuery), ); return metrics.length ? [ { path: [...path, node], metrics, }, ] : []; } if (node.craftables) { const craftables = node.craftables.filter((c) => filterItems(c.item)); return craftables.length ? [ { path: [...path, node], craftables, }, ] : []; } if (node.plugs) { const plugs = node.plugs.filter((p) => filterItems(p.item)); return plugs.length ? [ { path: [...path, node], plugs, }, ] : []; } return []; } function searchNode(node: DimPresentationNode, searchQuery: string) { return ( (node.nodeDef && searchDisplayProperties(node.nodeDef.displayProperties, searchQuery)) || node.titleInfo?.title.toLowerCase().includes(searchQuery) || node.name.toLowerCase().includes(searchQuery) ); } export function searchDisplayProperties( displayProperties: DestinyDisplayPropertiesDefinition, searchQuery: string, ) { return ( displayProperties.name.toLowerCase().includes(searchQuery) || displayProperties.description.toLowerCase().includes(searchQuery) ); } function searchRewards( record: DestinyRecordDefinition, searchQuery: string, defs: D2ManifestDefinitions, ) { return record.rewardItems.some((ri) => searchDisplayProperties(defs.InventoryItem.get(ri.itemHash).displayProperties, searchQuery), ); } function toCollectibles( itemCreationContext: ItemCreationContext, collectibleChildren: DestinyPresentationNodeCollectibleChildEntry[], ): DimCollectible[] { const { defs, profileResponse } = itemCreationContext; return compact( collectibleChildren.flatMap(({ collectibleHash }) => { const fakeItemHash = extraItemCollectibles[collectibleHash]; const collectibleDef = defs.Collectible.get(collectibleHash); if (!collectibleDef) { return null; } const itemHashes = compact([collectibleDef.itemHash, fakeItemHash]); return itemHashes.map((itemHash) => { const state = getCollectibleState(collectibleDef, profileResponse); if ( state === undefined || state & DestinyCollectibleState.Invisible || collectibleDef.redacted ) { return null; } const item = makeFakeItem(itemCreationContext, itemHash); if (!item) { return null; } item.missingSockets = false; return { state, collectibleDef, item, key: `${collectibleHash}-${itemHash}`, fake: fakeItemHash === itemHash, }; }); }), ); } function toRecords( defs: D2ManifestDefinitions, profileResponse: DestinyProfileResponse, recordHashes: DestinyPresentationNodeRecordChildEntry[], ): DimRecord[] { return filterMap(recordHashes, ({ recordHash }) => toRecord(defs, profileResponse, recordHash)); } export function toRecord( defs: D2ManifestDefinitions, profileResponse: DestinyProfileResponse, recordHash: number, mayBeMissing?: boolean, ): DimRecord | undefined { const recordDef = mayBeMissing ? defs.Record.getOptional(recordHash) : defs.Record.get(recordHash); if (!recordDef) { return undefined; } const record = getRecordComponent(recordDef, profileResponse); if (record === undefined || record.state & DestinyRecordState.Invisible || recordDef.redacted) { return undefined; } // Rename Immovable Refit -> Vexcalibur Catalyst const VEXCALIBUR_CATALYST_RECORD_HASH = 3787307395; if (recordHash === VEXCALIBUR_CATALYST_RECORD_HASH) { Object.assign(recordDef, { displayProperties: { ...recordDef.displayProperties, name: defs.Record.get(recordHash).stateInfo.obscuredName, }, }); } const CATALYST_PRESENTATION_NODES = [3788273704, 185103480, 2538646043]; if ( recordDef.parentNodeHashes?.length > 0 && recordDef.parentNodeHashes.some((hash) => CATALYST_PRESENTATION_NODES.includes(hash)) && recordDef.stateInfo?.obscuredDescription ) { const sourceText = `\n\n${t('Progress.CatalystSource', { source: recordDef.stateInfo.obscuredDescription })}`; if (!recordDef.displayProperties.description.includes(sourceText)) { Object.assign(recordDef, { displayProperties: { ...recordDef.displayProperties, description: `${recordDef.displayProperties.description}${sourceText}`, }, }); } } const trackedInGame = profileResponse?.profileRecords?.data?.trackedRecordHash === recordHash; return { recordComponent: record, recordDef, trackedInGame, }; } function toCraftables( itemCreationContext: ItemCreationContext, craftableChildren: DestinyPresentationNodeCraftableChildEntry[], ): DimCraftable[] { return filterMap(craftableChildren.toSorted(compareBy((c) => c.nodeDisplayPriority)), (c) => toCraftable(itemCreationContext, c.craftableItemHash), ); } function toCraftable( itemCreationContext: ItemCreationContext, itemHash: number, ): DimCraftable | undefined { const item = makeFakeItem(itemCreationContext, itemHash); if (!item) { return; } const info = getCraftableInfo(item.hash, itemCreationContext.profileResponse); if (!info?.visible) { return; } const canCraftThis = info.failedRequirementIndexes.length === 0; const canCraftAllPlugs = info.sockets.every((s) => s.plugs.every((p) => p.failedRequirementIndexes.length === 0), ); return { item, canCraftThis, canCraftAllPlugs }; } function toMetrics( defs: D2ManifestDefinitions, profileResponse: DestinyProfileResponse, metricHashes: DestinyPresentationNodeMetricChildEntry[], ): DimMetric[] { return filterMap(metricHashes, ({ metricHash }) => { const metricDef = defs.Metric.get(metricHash); if (!metricDef) { return undefined; } const metric = getMetricComponent(metricDef, profileResponse); if (!metric || metric.invisible || metricDef.redacted) { return undefined; } return { metricComponent: metric, metricDef, }; }); } function getRecordComponent( recordDef: DestinyRecordDefinition, profileResponse: DestinyProfileResponse, ): DestinyRecordComponent | undefined { return recordDef.scope === DestinyScope.Character ? profileResponse.characterRecords?.data ? Object.values(profileResponse.characterRecords.data)[0].records[recordDef.hash] : undefined : profileResponse.profileRecords?.data?.records[recordDef.hash]; } function getCraftableInfo(itemHash: number, profileResponse: DestinyProfileResponse) { if (!profileResponse.characterCraftables?.data) { return; } const allCharCraftables: (DestinyCraftableComponent | undefined)[] = Object.values( profileResponse.characterCraftables.data, ).map((d) => d.craftables[itemHash]); // try to find a character on whom this item is visible return allCharCraftables.find((c) => c?.visible === true) ?? allCharCraftables[0]; } export function getCollectibleState( collectibleDef: DestinyCollectibleDefinition, profileResponse: DestinyProfileResponse, ) { return collectibleDef.scope === DestinyScope.Character ? profileResponse.characterCollectibles?.data ? minBy( // Find the version of the collectible that's unlocked, if any Object.values(profileResponse.characterCollectibles.data) .map((c) => c.collectibles[collectibleDef.hash].state) .filter((s) => s !== undefined), (state) => state & DestinyCollectibleState.NotAcquired, ) : undefined : profileResponse.profileCollectibles?.data?.collectibles[collectibleDef.hash]?.state; } export function getMetricTimeScope( defs: D2ManifestDefinitions, metric: DestinyMetricDefinition, ): DestinyTraitDefinition { const traitHash = metric.traitHashes.find((h) => h !== TraitHashes.All); return defs.Trait.get(traitHash ?? TraitHashes.All); } function getMetricComponent( metricDef: DestinyMetricDefinition, profileResponse: DestinyProfileResponse, ): DestinyMetricComponent | undefined { return profileResponse.metrics?.data?.metrics[metricDef.hash]; } function searchCatalysts( recordHash: number, filterItems: ItemFilter, catalystItemsByRecordHash: Map<number, DimItem>, ): unknown { const exoticForCatalyst = catalystItemsByRecordHash.get(recordHash); return exoticForCatalyst && filterItems(exoticForCatalyst); } ================================================ FILE: src/app/records/selectors.ts ================================================ import { profileResponseSelector } from 'app/inventory/selectors'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { SHADER_NODE } from 'app/search/d2-known-values'; import { DestinyCollectibleState, DestinyPresentationNodeDefinition } from 'bungie-api-ts/destiny2'; import { createSelector } from 'reselect'; import { getCollectibleState } from './presentation-nodes'; // A set containing shaders that are displayed in collections for any character. export const collectionsVisibleShadersSelector = createSelector( d2ManifestSelector, profileResponseSelector, (defs, profileResponse) => { if (!defs || !profileResponse) { return undefined; } const getVisibleCollectibles = (node: DestinyPresentationNodeDefinition): number[] => { const visibleCollectibles: number[] = node.children.collectibles .map((c) => defs.Collectible.get(c.collectibleHash)) .filter((c) => { const state = getCollectibleState(c, profileResponse); return state !== undefined && !(state & DestinyCollectibleState.Invisible) && !c.redacted; }) .map((c) => c.itemHash); const visibleChildCollectibles = node.children.presentationNodes.flatMap((childNode) => getVisibleCollectibles(defs.PresentationNode.get(childNode.presentationNodeHash)), ); return visibleChildCollectibles.concat(visibleCollectibles); }; return new Set(getVisibleCollectibles(defs.PresentationNode.get(SHADER_NODE))); }, ); ================================================ FILE: src/app/records/universal-ornaments/UniversalOrnaments.m.scss ================================================ @use '../set-card'; .classType { margin-left: 10px; } .classIcon { height: 24px; vertical-align: middle; } .records { @include set-card.set-card-grid; } .ornaments { display: flex; flex-wrap: wrap; gap: 4px; } ================================================ FILE: src/app/records/universal-ornaments/UniversalOrnaments.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'classIcon': string; 'classType': string; 'ornaments': string; 'records': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/records/universal-ornaments/UniversalOrnaments.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import CollapsibleTitle from 'app/dim-ui/CollapsibleTitle'; import { DimItem } from 'app/inventory/item-types'; import { createItemContextSelector } from 'app/inventory/selectors'; import { useD2Definitions } from 'app/manifest/selectors'; import { ItemFilter } from 'app/search/filter-types'; import { objectValues } from 'app/utils/util-types'; import { VendorItemDisplay } from 'app/vendors/VendorItemComponent'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import SetCard from '../SetCard'; import * as styles from './UniversalOrnaments.m.scss'; import { OrnamentStatus, OrnamentsSet, buildSets, filterOrnamentSets, instantiateOrnamentSets, univeralOrnamentsVisibilitySelector, } from './universal-ornaments'; /** * Displays all unlocked and unlockable universal ornament, categorized into armor sets. */ export default function UniversalOrnaments({ searchQuery, searchFilter, }: { searchQuery: string; searchFilter: ItemFilter; }) { const defs = useD2Definitions(); const createItemContext = useSelector(createItemContextSelector); const unlocked = useSelector(univeralOrnamentsVisibilitySelector); const defData = defs && buildSets(defs); const populatedData = useMemo( () => defData && instantiateOrnamentSets(defData, createItemContext), [createItemContext, defData], ); const filteredData = useMemo( () => populatedData && filterOrnamentSets(populatedData, searchQuery, searchFilter), [populatedData, searchFilter, searchQuery], ); if (!filteredData) { return null; } return ( <div className={styles.classType}> {objectValues(filteredData).flatMap((sets) => ( <CollapsibleTitle key={sets.classType} title={ <> <BungieImage className={styles.classIcon} src={sets.icon} /> {sets.name} </> } sectionId={`ornaments-class-${sets.classType}`} defaultCollapsed={true} > <div className={styles.records}> {Object.values(sets.sets).map((set) => ( <Ornaments key={set.key} set={set} ownedItemHashes={unlocked} /> ))} </div> </CollapsibleTitle> ))} </div> ); } function Ornaments({ set, ownedItemHashes, }: { set: OrnamentsSet<DimItem>; ownedItemHashes: OrnamentStatus; }) { // If none of the ornaments for this set are visible in an in-game socket, we should // hide this, since it's likely some Eververse set if (set.ornaments.every((item) => !ownedItemHashes.visibleOrnaments.has(item.hash))) { return null; } const complete = set.ornaments.every((item) => ownedItemHashes.unlockedOrnaments.has(item.hash)); return ( <SetCard title={set.name} complete={complete}> <div className={styles.ornaments}> {set.ornaments.map((item) => { const acquired = ownedItemHashes.visibleOrnaments.has(item.hash); const owned = ownedItemHashes.unlockedOrnaments.has(item.hash); return ( <VendorItemDisplay key={item.hash} item={item} unavailable={!owned} acquired={acquired} owned={owned} extraData={{ acquired, owned }} /> ); })} </div> </SetCard> ); } ================================================ FILE: src/app/records/universal-ornaments/universal-ornaments.ts ================================================ import { D2Categories } from 'app/destiny2/d2-bucket-categories'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { profileResponseSelector } from 'app/inventory/selectors'; import { ItemCreationContext, makeFakeItem } from 'app/inventory/store/d2-item-factory'; import { ARMOR_NODE, DEFAULT_ORNAMENTS } from 'app/search/d2-known-values'; import { ItemFilter } from 'app/search/filter-types'; import { filterMap, mapValues } from 'app/utils/collections'; import { DestinyClass, DestinyCollectibleDefinition } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import auxOrnamentSets from 'data/d2/universal-ornament-aux-sets.json'; import universalOrnamentPlugSetHashes from 'data/d2/universal-ornament-plugset-hashes.json'; import memoizeOne from 'memoize-one'; import { createSelector } from 'reselect'; import { createCollectibleFinder } from '../collectible-matching'; export interface OrnamentsSet<T = number> { /** Name of the ornament set */ name: string; key: string; /** The set, in bucket order */ ornaments: T[]; } /** * Ornament sets by class type. */ export interface OrnamentsData<T = number> { [classType: number]: { classType: DestinyClass; /** Name of the class for convenience */ name: string; /** Class icon */ icon: string; /** All ornament sets for this class. */ sets: { [setKey: string]: OrnamentsSet<T> }; }; } export interface OrnamentStatus { /** Ornaments that are visible in the in-game ornament socket. If they're not already unlocked, they're available for unlocking. */ visibleOrnaments: Set<number>; /** Ornaments that are available for the in-game ornament socket. These are unlocked and can be equipped. */ unlockedOrnaments: Set<number>; } /** Retrieve info from the profile about which ornaments are unlocked, and which ones are visible. */ export const univeralOrnamentsVisibilitySelector = createSelector( profileResponseSelector, (profileResponse) => { const unlockedPlugs: OrnamentStatus = { visibleOrnaments: new Set(), unlockedOrnaments: new Set(), }; if (profileResponse?.profilePlugSets.data?.plugs) { for (const plugSetHash of universalOrnamentPlugSetHashes) { const plugs = profileResponse.profilePlugSets.data.plugs[plugSetHash]; if (!plugs) { continue; } for (const plugSetItem of plugs) { if (plugSetItem.canInsert) { unlockedPlugs.visibleOrnaments.add(plugSetItem.plugItemHash); unlockedPlugs.unlockedOrnaments.add(plugSetItem.plugItemHash); } else if (plugSetItem.enabled) { unlockedPlugs.visibleOrnaments.add(plugSetItem.plugItemHash); } } } } for (const characterId in profileResponse?.characterPlugSets.data) { for (const plugSetHash of universalOrnamentPlugSetHashes) { const plugs = profileResponse?.characterPlugSets.data[characterId].plugs[plugSetHash]; if (!plugs) { continue; } for (const plugSetItem of plugs) { if (plugSetItem.canInsert) { unlockedPlugs.visibleOrnaments.add(plugSetItem.plugItemHash); unlockedPlugs.unlockedOrnaments.add(plugSetItem.plugItemHash); } else if (plugSetItem.enabled) { unlockedPlugs.visibleOrnaments.add(plugSetItem.plugItemHash); } } } } return unlockedPlugs; }, ); /** * Returns universal ornament plug set hashes keyed by class and sorted in inventory slot order * (helmet -> class item) */ function identifyPlugSets(defs: D2ManifestDefinitions): { [classType: number]: number[] } { const sets: { [classType: number]: { [bucketTypeHash: number]: number } } = { [DestinyClass.Titan]: {}, [DestinyClass.Hunter]: {}, [DestinyClass.Warlock]: {}, }; nextPlugSet: for (const plugSetHash of universalOrnamentPlugSetHashes) { const plugSet = defs.PlugSet.get(plugSetHash); for (const entry of plugSet.reusablePlugItems) { const item = defs.InventoryItem.get(entry.plugItemHash); if ( !item.redacted && item.collectibleHash && item.inventory && D2Categories.Armor.includes(item.inventory.bucketTypeHash) && item.classType !== DestinyClass.Unknown ) { sets[item.classType][item.inventory.bucketTypeHash] = plugSetHash; continue nextPlugSet; } } } return mapValues(sets, (set) => D2Categories.Armor.map((bucketHash) => set[bucketHash])); } /** * Builds universal ornament sets based entirely on static definition data. This takes * all universal ornament plugs and applies a bunch of heuristics to map them to armor sets * using presentation node / collectible defs. */ export const buildSets = memoizeOne((defs: D2ManifestDefinitions): OrnamentsData => { const plugSetHashes = identifyPlugSets(defs); const collectibleFinder = createCollectibleFinder(defs); const data: OrnamentsData = { [DestinyClass.Titan]: { classType: DestinyClass.Titan, sets: {}, name: '', icon: '' }, [DestinyClass.Hunter]: { classType: DestinyClass.Hunter, sets: {}, name: '', icon: '' }, [DestinyClass.Warlock]: { classType: DestinyClass.Warlock, sets: {}, name: '', icon: '' }, }; const findCollectibleArmorParentNode = ( collectible: DestinyCollectibleDefinition | undefined, ) => { if (collectible) { return collectible.parentNodeHashes ?.map((nodeHash) => defs.PresentationNode.get(nodeHash)) .find((node) => node?.children.collectibles?.length <= 5); } }; // The Titan / Hunter / Warlock presentation nodes as children of the Armor node. NB hardcoded order here... const classPresentationNodes = defs.PresentationNode.get(ARMOR_NODE).children.presentationNodes; for (const classType of [ DestinyClass.Titan, DestinyClass.Hunter, DestinyClass.Warlock, ] as const) { const relevantPlugSetHashes = plugSetHashes[classType]; const classNode = classPresentationNodes[classType]; const classNodeDef = defs.PresentationNode.get(classNode.presentationNodeHash); data[classType].name = classNodeDef.displayProperties.name; data[classType].icon = classNodeDef.displayProperties.icon; const auxSets = Object.entries(auxOrnamentSets[classType]); const otherItems: number[] = []; for (const plugSetHash of relevantPlugSetHashes) { const plugSet = defs.PlugSet.get(plugSetHash); if (!plugSet) { continue; } for (const entry of plugSet.reusablePlugItems) { if (DEFAULT_ORNAMENTS.includes(entry.plugItemHash)) { continue; } const item = defs.InventoryItem.get(entry.plugItemHash); if (item) { // d2ai may have categorized this into its own armor set. // Add it using the d2ai-generated set key. const auxEntry = auxSets.find(([, set]) => set.some((hash) => hash === item.hash)); if (auxEntry) { const [key] = auxEntry; (data[classType].sets[key] ??= { key, name: '', ornaments: [], }).ornaments.push(item.hash); if (item.inventory?.bucketTypeHash === BucketHashes.ClassArmor) { // And use the class item as the set name as Bungie used this workaround // too for the 2023 Solstice sets data[classType].sets[key].name = item.displayProperties.name; } } else { const node = findCollectibleArmorParentNode(collectibleFinder(item, classType)); if (node) { // Some sets will be mapped to the same presentation node - find things that are categorically different // 1. whether the plug is a proper armor piece too, or just a modification // 2. season (via iconWatermark) // 3. whether the item has a collectibleHash const setKey = `${node.hash}-${ item.inventory?.bucketTypeHash === BucketHashes.Modifications }-${item.iconWatermark}-${Boolean(item.collectibleHash)}`; (data[classType].sets[setKey] ??= { key: setKey, name: node.displayProperties.name, ornaments: [], }).ornaments.push(item.hash); } else { otherItems.push(item.hash); } } } } } // Finally, put all remaining items (as of writing, Masquerader's helmet and two class items per class) // into their own section. data[classType].sets[-123] = { name: t('Records.UniversalOrnamentSetOther'), key: '-123', ornaments: otherItems, }; } return data; }); /** * Turn our def-based ornament hashes into DimItems based on live data. */ export function instantiateOrnamentSets( data: OrnamentsData, itemCreationContext: ItemCreationContext, ): OrnamentsData<DimItem> { return mapValues(data, (classData) => ({ ...classData, sets: Object.fromEntries( filterMap(Object.entries(classData.sets), ([key, set]) => { const items = filterMap(set.ornaments, (hash) => makeFakeItem(itemCreationContext, hash)); return [key, { ...set, ornaments: items }]; }), ), })); } /** * Filter ornaments down to sets matching the search filter. */ export function filterOrnamentSets( data: Readonly<OrnamentsData<DimItem>>, searchQuery: string, searchFilter: ItemFilter, ): OrnamentsData<DimItem> { return mapValues(data, (classData) => ({ ...classData, sets: Object.fromEntries( Object.entries(classData.sets).filter( ([_key, set]) => !searchQuery || set.name.toLowerCase().includes(searchQuery) || set.ornaments.some(searchFilter), ), ), })); } ================================================ FILE: src/app/register-service-worker.test.ts ================================================ import { isNewVersion } from './register-service-worker'; describe('isNewVersion', () => { it('should recognize two identical versions', async () => { expect(isNewVersion('6.44.0.1000100', '6.44.0.1000100')).toBe(false); }); it('should newer versions', async () => { expect(isNewVersion('6.45.0.1000100', '6.44.0.1000100')).toBe(true); }); it('should ignore older versions', async () => { expect(isNewVersion('6.40.0.1000100', '6.44.0.1000100')).toBe(false); }); }); ================================================ FILE: src/app/register-service-worker.ts ================================================ import { getClient } from '@sentry/browser'; import { toHttpStatusError } from './bungie-api/http-client'; import { sheetsOpen } from './dim-ui/sheets-open'; import { errorLog, infoLog, warnLog } from './utils/log'; import { Observable } from './utils/observable'; import { delay } from './utils/promises'; import { reportException } from './utils/sentry'; const TAG = 'SW'; /** * A function that will attempt to update the service worker in place. * It will return a promise for when the update is complete. * If service workers are not enabled or installed, this is a no-op. */ let updateServiceWorker = async () => true; /** * Whether there is new content available if you reload DIM. * * We only need to update when there's new content and we've already updated the service worker. */ export const dimNeedsUpdate$ = new Observable<boolean>(false); /** * Poll what the server thinks is current. * This is to handle cases where folks have DIM open for a long time. * It will attempt to update the service worker before reporting that DIM needs update. */ // TODO: Move this state into Redux? let currentVersion = $DIM_VERSION; (async () => { await delay(10 * 1000); setInterval( async () => { try { const serverVersion = await getServerVersion(); if (isNewVersion(serverVersion, currentVersion)) { const updated = await updateServiceWorker(); if (updated) { currentVersion = serverVersion; dimNeedsUpdate$.next(true); } } } catch (e) { errorLog(TAG, 'Failed to check version.json', e); } }, 15 * 60 * 1000, ); })(); /** * If Service Workers are supported, install our Service Worker and listen for updates. */ export default function registerServiceWorker() { if (!('serviceWorker' in navigator)) { return; } window.addEventListener('load', () => { navigator.serviceWorker .register(`${$PUBLIC_PATH}service-worker.js`, { scope: $PUBLIC_PATH }) .then((registration) => { // TODO: save off a handler that can call registration.update() to force update on refresh? registration.onupdatefound = () => { if ($featureFlags.debugSW) { infoLog(TAG, 'A new Service Worker version has been found...'); } const installingWorker = registration.installing!; installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // At this point, the old content will have been purged and // the fresh content will have been added to the cache. // It's the perfect time to display a "New content is // available; please refresh." message in your web app. infoLog(TAG, 'New content is available; please refresh. (from onupdatefound)'); // At this point, is it really cached?? dimNeedsUpdate$.next(true); let preventDevToolsReloadLoop = false; navigator.serviceWorker.addEventListener('controllerchange', () => { // Ensure refresh is only called once. // This works around a bug in "force update on reload". if (preventDevToolsReloadLoop) { return; } preventDevToolsReloadLoop = true; if ( // Loadout optimizer is all about state, don't reload it !window.location.pathname.endsWith('/optimizer') && // If a sheet is up, the user is doing something. We check sheetsOpen here, because it is not reactive! sheetsOpen.open <= 0 ) { window.location.reload(); } else { warnLog(TAG, 'Not reloading because user is in the middle of something'); } }); } else if ($featureFlags.debugSW) { // At this point, everything has been precached. // It's the perfect time to display a // "Content is cached for offline use." message. infoLog(TAG, 'Content is cached for offline use.'); } } else if ($featureFlags.debugSW) { infoLog(TAG, 'New Service Worker state: ', installingWorker.state); } }; }; updateServiceWorker = async () => { infoLog(TAG, 'Checking for service worker update.'); try { await registration.update(); } catch (err) { if ($featureFlags.debugSW) { errorLog(TAG, 'Unable to update service worker.', err); reportException('service-worker', err); } return false; } if (registration.waiting) { infoLog(TAG, 'New content is available; please refresh. (from update)'); // Disable Sentry error logging if this user is on an older version const sentryOptions = getClient()?.getOptions(); if (sentryOptions) { sentryOptions.enabled = false; } return true; } else { infoLog(TAG, 'Updated, but theres not a new worker waiting'); return false; } }; }) .catch((err) => { errorLog(TAG, 'Unable to register service worker.', err); reportException('service-worker', err); }); }); } /** * Fetch a file on the server that contains the currently uploaded version number. */ async function getServerVersion() { const response = await fetch('/version.json'); if (response.ok) { const data = (await response.json()) as { version?: string }; if (!data.version) { throw new Error('No version property'); } return data.version; } else { throw await toHttpStatusError(response); } } export function isNewVersion(version: string, currentVersion: string) { const parts = version.split('.'); const currentVersionParts = currentVersion.split('.'); let newerAvailable = false; let olderAvailable = false; for (let i = 0; i < parts.length && i < currentVersionParts.length; i++) { const versionSegment = parseInt(parts[i], 10); const currentVersionSegment = parseInt(currentVersionParts[i], 10); if (versionSegment > currentVersionSegment) { newerAvailable = true; break; } else if (versionSegment < currentVersionSegment) { olderAvailable = true; break; } } if (olderAvailable) { warnLog(TAG, 'Server version ', version, ' is older than current version ', currentVersion); } else if (newerAvailable) { infoLog(TAG, 'Found newer version on server, attempting to update'); } return newerAvailable; } /** * Attempt to update the service worker and reload DIM with the new version. */ export async function reloadDIM() { try { const registration = await navigator.serviceWorker.getRegistration(); if (!registration) { errorLog(TAG, 'No registration!'); window.location.reload(); return; } if (!registration.waiting) { // Just to ensure registration.waiting is available before // calling postMessage() errorLog(TAG, 'registration.waiting is null!'); const installingWorker = registration.installing; if (installingWorker) { infoLog(TAG, 'found an installing service worker'); installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { infoLog(TAG, 'installing service worker installed, skip waiting'); installingWorker.postMessage('skipWaiting'); } }; } else { window.location.reload(); } return; } infoLog(TAG, 'posting skip waiting'); registration.waiting.postMessage('skipWaiting'); // insurance! setTimeout(() => { window.location.reload(); }, 2000); } catch (e) { errorLog(TAG, 'Error checking registration:', e); window.location.reload(); } } ================================================ FILE: src/app/routes.ts ================================================ import { DestinyAccount } from './accounts/destiny-account'; /** * This file contains helpers for generating route paths, though generally our * paths are simple. */ export const accountRoute = (account: Pick<DestinyAccount, 'membershipId' | 'destinyVersion'>) => `/${account.membershipId}/d${account.destinyVersion}`; ================================================ FILE: src/app/safari-touch-fix.ts ================================================ import { isNativeDragAndDropSupported, isiOSBrowser } from './utils/browsers'; import { noop } from './utils/functions'; // This can likely be removed now, but definitely after we minimally support iOS 15 // https://github.com/timruffles/mobile-drag-drop/issues/77 // https://github.com/timruffles/mobile-drag-drop/issues/124 export function safariTouchFix() { if (!isiOSBrowser() || isNativeDragAndDropSupported()) { return; } // Test via a getter in the options object to see if the passive property is accessed let supportsPassive = false; try { const opts: AddEventListenerOptions = Object.defineProperty({}, 'passive', { get() { supportsPassive = true; return supportsPassive; }, }); window.addEventListener('testPassive', noop, opts); window.removeEventListener('testPassive', noop, opts); } catch {} supportsPassive ? window.addEventListener('touchmove', noop, { passive: false }) : window.addEventListener('touchmove', noop); } ================================================ FILE: src/app/search/FilterHelp.m.scss ================================================ @use '../variables.scss' as *; .filterView { padding: 0 10px 1em 10px !important; user-select: text; table { padding: 0; border-collapse: collapse; border-spacing: 0; background-color: rgb(255, 255, 255, 0.15); width: 100%; @include phone-portrait { table-layout: fixed; } tr:nth-child(2n) { background-color: rgb(0, 0, 0, 0.1); } tr:nth-child(2n + 1) { background-color: rgb(0, 0, 0, 0.3); } } th { text-align: left; } td, th { border: none; font-size: 13px; padding: 6px 9px; vertical-align: top; span, a { color: #eee; display: block; text-decoration: none; @include interactive($hover: true) { text-decoration: underline; } } } tr td:first-child { border-left: none; } kbd { background-color: #222; border-radius: 2px; padding: 0 2px; font-family: 'Open Sans', sans-serif; } } .search { margin: 8px 0 !important; } .entry { display: flex; margin-bottom: 4px; div { display: flex; } &:last-child { margin-bottom: 0; } .separator { opacity: 0.5; } } ================================================ FILE: src/app/search/FilterHelp.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'entry': string; 'filterView': string; 'search': string; 'separator': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/search/FilterHelp.tsx ================================================ import { SearchType } from '@destinyitemmanager/dim-api-types'; import StaticPage from 'app/dim-ui/StaticPage'; import { t } from 'app/i18next-t'; import { toggleSearchQueryComponent } from 'app/shell/actions'; import { RootState } from 'app/store/types'; import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import * as styles from './FilterHelp.m.scss'; import { SearchInput } from './SearchInput'; import { FilterDescription, filterDescriptionText } from './filter-description'; import { ItemFilterDefinition, ItemSearchConfig, SuggestionsContext, } from './items/item-filter-types'; import { searchConfigSelector, suggestionsContextSelector } from './items/item-search-filter'; import { LoadoutFilterDefinition, LoadoutSearchConfig, LoadoutSuggestionsContext, } from './loadouts/loadout-filter-types'; import { loadoutSearchConfigSelector, loadoutSuggestionsContextSelector, } from './loadouts/loadout-search-filter'; import { generateGroupedSuggestionsForFilter } from './suggestions-generation'; function keywordsString(keywords: string | string[]) { if (Array.isArray(keywords)) { return keywords.join(', '); } return keywords; } export default function FilterHelp({ searchType = SearchType.Item }: { searchType?: SearchType }) { const searchConfig = useSelector<RootState, ItemSearchConfig | LoadoutSearchConfig>( searchType === SearchType.Loadout ? loadoutSearchConfigSelector : searchConfigSelector, ).filtersMap; const suggestionContext = useSelector( searchType === SearchType.Loadout ? loadoutSuggestionsContextSelector : suggestionsContextSelector, ); const [search, setSearch] = useState(''); const searchLower = search.toLowerCase(); const filters = search ? searchConfig.allFilters.filter((filter) => { if (filter.deprecated) { return false; } const keywordsArr = Array.isArray(filter.keywords) ? filter.keywords : [filter.keywords]; if (keywordsArr.some((k) => k.includes(searchLower))) { return true; } const localDesc = filterDescriptionText(filter.description); return localDesc?.toLowerCase().includes(searchLower); }) : searchConfig.allFilters.filter((f) => !f.deprecated); return ( <StaticPage className={styles.filterView}> <div> <p> {t('Filter.Combine', { example: '(is:weapon and is:legendary) or (is:armor and stat:total:<55)', })}{' '} {t('Filter.Negate', { notexample: '-is:tagged', notexample2: 'not is:tagged' })}{' '} <a href="/search-history">{t('SearchHistory.Link')}</a> </p> <div className={styles.search}> <SearchInput query={search} onQueryChanged={setSearch} placeholder={t('Filter.SearchPrompt')} autoFocus /> </div> <table> <thead> <tr> <th>{t('Filter.Filter')}</th> <th>{t('Filter.Description')}</th> </tr> </thead> <tbody> {filters.map((filter) => ( <FilterExplanation key={keywordsString(filter.keywords)} filter={filter} suggestionContext={suggestionContext} /> ))} </tbody> </table> </div> </StaticPage> ); } function FilterExplanation({ filter, suggestionContext, }: { filter: LoadoutFilterDefinition | ItemFilterDefinition; suggestionContext: LoadoutSuggestionsContext | SuggestionsContext; }) { const dispatch = useDispatch(); let suggestions = Array.from( new Set([...generateGroupedSuggestionsForFilter(filter, suggestionContext, true)]), ); if (filter.format === 'freeform' || filter.format?.includes('freeform')) { suggestions = suggestions.slice(0, 5); } const applySuggestion = (e: React.MouseEvent<HTMLAnchorElement>, k: string) => { e.preventDefault(); dispatch(toggleSearchQueryComponent(k)); }; return ( <tr> <td> {suggestions.map((k, i) => ( <div key={i} className={styles.entry}> <a href="." onClick={(e) => applySuggestion(e, k.keyword)}> {k.ops ? `${k.keyword}` : k.keyword} </a> {k.ops?.map((op, j) => { const x = `${k.keyword}${op}`; return ( <div key={j}> <span className={styles.separator}>| </span> <a href="." onClick={(e) => applySuggestion(e, x)}> {op} </a> </div> ); })} </div> ))} </td> <td> <FilterDescription description={filter.description} /> </td> </tr> ); } ================================================ FILE: src/app/search/HighlightedText.tsx ================================================ export default function HighlightedText({ text, startIndex, endIndex, className, }: { text: string; startIndex: number; endIndex: number; className: string; }) { const start = text.slice(0, startIndex); const middle = text.slice(startIndex, endIndex); const end = text.slice(endIndex); if (!middle) { return <>{text}</>; } return ( <> {start} <span className={className}>{middle}</span> {end} </> ); } ================================================ FILE: src/app/search/MainSearchBarActions.m.scss ================================================ .count { color: #999; padding-left: 8px; @media (max-width: 1000px) { display: none; } } .resultButton { composes: filterBarButton from '../search/SearchBar.m.scss'; display: flex; align-items: center; > span:first-child { margin-right: 4px !important; } } ================================================ FILE: src/app/search/MainSearchBarActions.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'count': string; 'resultButton': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/search/MainSearchBarActions.tsx ================================================ import { t } from 'app/i18next-t'; import { toggleSearchResults } from 'app/shell/actions'; import { AppIcon, faList } from 'app/shell/icons'; import { querySelector, searchResultsOpenSelector, useIsPhonePortrait } from 'app/shell/selectors'; import { emptyArray } from 'app/utils/empty'; import { motion } from 'motion/react'; import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import * as styles from './MainSearchBarActions.m.scss'; import { searchButtonAnimateVariants } from './SearchBar'; import SearchResults from './SearchResults'; import { filteredItemsSelector, queryValidSelector } from './items/item-search-filter'; /** * The extra buttons that appear in the main search bar when there are matched items. */ export default function MainSearchBarActions() { const searchQuery = useSelector(querySelector); const queryValid = useSelector(queryValidSelector); const filteredItems = useSelector(filteredItemsSelector); const searchResultsOpen = useSelector(searchResultsOpenSelector); const dispatch = useDispatch(); const isPhonePortrait = useIsPhonePortrait(); const location = useLocation(); const onInventory = location.pathname.endsWith('inventory'); const onProgress = location.pathname.endsWith('progress'); const onRecords = location.pathname.endsWith('records'); const onVendors = location.pathname.endsWith('vendors'); // We don't have access to the selected store so we'd match multiple characters' worth. // Just suppress the count for now const showSearchResults = onInventory && !isPhonePortrait; const showSearchCount = Boolean( queryValid && searchQuery && !onProgress && !onRecords && !onVendors, ); const handleCloseSearchResults = useCallback( () => dispatch(toggleSearchResults(false)), [dispatch], ); return ( <> {showSearchCount && ( <motion.div key="count" variants={searchButtonAnimateVariants} exit="hidden" initial="hidden" animate="shown" > {showSearchResults ? ( <button type="button" className={styles.resultButton} title={t('Header.SearchResults')} onClick={() => dispatch(toggleSearchResults())} > <span className={styles.count}> {t('Header.FilterMatchCount', { count: filteredItems.length })} </span> <AppIcon icon={faList} /> </button> ) : ( <span className={styles.count}> {t('Header.FilterMatchCount', { count: filteredItems.length })} </span> )} </motion.div> )} {searchResultsOpen && ( <SearchResults items={queryValid ? filteredItems : emptyArray()} onClose={handleCloseSearchResults} /> )} </> ); } ================================================ FILE: src/app/search/MainSearchBarMenu.tsx ================================================ import useBulkNote from 'app/dim-ui/useBulkNote'; import ItemActionsDropdown from 'app/item-actions/ItemActionsDropdown'; import { querySelector } from 'app/shell/selectors'; import { motion } from 'motion/react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import { searchButtonAnimateVariants } from './SearchBar'; import { filteredItemsSelector, queryValidSelector } from './items/item-search-filter'; /** * The three-dots dropdown menu of actions for the search bar that act on searched items. */ export default function MainSearchBarMenu() { const location = useLocation(); const searchQuery = useSelector(querySelector); const queryValid = useSelector(queryValidSelector); const showSearchCount = Boolean(searchQuery && queryValid); const filteredItems = useSelector(filteredItemsSelector); const onInventory = location.pathname.endsWith('inventory'); const [promptDialog, bulkNote] = useBulkNote(); const showSearchActions = onInventory; if (!showSearchActions) { return null; } return ( <motion.div layout key="action" variants={searchButtonAnimateVariants} exit="hidden" initial="hidden" animate="shown" > {promptDialog} <ItemActionsDropdown filteredItems={filteredItems} searchActive={showSearchCount} searchQuery={searchQuery} fixed={true} bulkNote={bulkNote} /> </motion.div> ); } ================================================ FILE: src/app/search/SearchBar.m.scss ================================================ @use '../variables.scss' as *; .searchBar { position: relative; font-size: 13px; } .open { border-bottom-left-radius: 0 !important; border-bottom-right-radius: 0 !important; box-shadow: 0 1px 0 1px var(--theme-search-dropdown-border); } .menu { color: var(--theme-text); margin: 0; border-top: 0; background: var(--theme-search-dropdown-bg); position: absolute; z-index: 1; list-style: none; padding: 0; left: 0; right: 0; // Add a top offset for the 1px inset box-shadow/border // This avoids an unwanted dividing line between the search box + dropdown when open top: calc($search-bar-height - 1px); border-bottom-left-radius: $theme-corner-radius-search; border-bottom-right-radius: $theme-corner-radius-search; box-shadow: inset 0 0 0 1px var(--theme-search-dropdown-border); overflow: hidden; overscroll-behavior: none; @include phone-portrait { top: 100%; // Fallback to allow either fully or semi-transparent search box borders // Add a margin and change to outset shadows for clearer outlining on mobile // Fill the 1px margin with re-stacked fills/borders to accurately colour-match margin: 0 1px; box-shadow: 0 0 0 1px var(--theme-search-dropdown-border), 0 0 0 1px var(--theme-sheet-search-bg), 0 0 0 1px var(--theme-mobile-background), 0 1px 4px 1px rgb(0, 0, 0, 0.5); max-height: calc(var(--viewport-height) - var(--header-height)) !important; } &:empty { display: none !important; } } .invalid { color: var(--theme-text-secondary) !important; } .openButton { composes: resetButton from '../dim-ui/common.m.scss'; display: inline-block; color: #999; font-size: 12px !important; } .menuItem { composes: flexRow from '../dim-ui/common.m.scss'; align-items: flex-start; padding: 6px 9px 6px 9px; gap: 6px; @include interactive($hover: true) { cursor: pointer; } @include phone-portrait { padding: 10px 10px 10px 9px; } } .menuItemIcon { font-size: 12px !important; margin-top: 4px; color: #999; } .armoryItemIcon { margin-right: 6px; margin-top: 5px; width: 24px; height: 24px; } .openInArmoryLabel { font-style: italic; opacity: 0.5; margin-left: 2px; margin-right: 4px; } .deleteIcon { composes: resetButton from '../dim-ui/common.m.scss'; font-size: 10px !important; margin-top: 4px; color: #999; padding: 0 2px; visibility: hidden; @media (hover: none) { visibility: visible; } } .namedQueryBody { display: block; font-size: 10.5px; opacity: 0.5; } .highlightedItem { background-color: var(--theme-accent-primary); color: var(--theme-text-invert) !important; .menuItemIcon, .openInArmoryLabel { color: var(--theme-text-invert); } .deleteIcon { color: #333; visibility: visible; } .namedQueryBody, .openInArmoryLabel { opacity: 1; } } .menuItemQuery { white-space: normal; margin: 0 auto 0 0; padding: 1px 0; overflow: hidden; max-height: 4em; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; } /* Highlighted spans of text in the autocompleter */ .textHighlight { display: inline-block; white-space: pre; border-bottom: 1px dotted #ddd; margin-bottom: -1px; .highlightedItem & { border-bottom-color: #222; } } .keyHelp { margin-top: 2px; .highlightedItem & { color: var(--theme-text-invert); border-color: var(--theme-text-invert); } } .saveSearchButton > :global(.app-icon) { color: var(--theme-accent-primary) !important; } .filterHelp { max-width: 800px; margin: 0 auto; } .filterBarButton { margin: 0 6px; padding: 0; appearance: none; display: inline-block; background: transparent; border: 0; cursor: pointer; @include interactive($hover: true) { > span, > :global(.app-icon), button > :global(.app-icon) { color: var(--theme-accent-primary); } } &:focus-visible { outline: 1px solid var(--theme-accent-primary); } > :global(.app-icon), > button > :global(.app-icon) { font-size: 14px !important; color: #999; // Increase touch target size padding: 4px; margin: -4px; margin-left: 4px; &:first-child { margin-left: -4px; } :global(.mobile-search-link) & { font-size: 18px !important; } } > span, > a { margin: 0 !important; } } ================================================ FILE: src/app/search/SearchBar.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'armoryItemIcon': string; 'deleteIcon': string; 'filterBarButton': string; 'filterHelp': string; 'highlightedItem': string; 'invalid': string; 'keyHelp': string; 'menu': string; 'menuItem': string; 'menuItemIcon': string; 'menuItemQuery': string; 'namedQueryBody': string; 'open': string; 'openButton': string; 'openInArmoryLabel': string; 'saveSearchButton': string; 'searchBar': string; 'textHighlight': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/search/SearchBar.tsx ================================================ import { SearchType } from '@destinyitemmanager/dim-api-types'; import ArmorySheet from 'app/armory/ArmorySheet'; import { saveSearch, searchDeleted, searchUsed } from 'app/dim-api/basic-actions'; import { languageSelector, recentSearchesSelector } from 'app/dim-api/selectors'; import BungieImage from 'app/dim-ui/BungieImage'; import KeyHelp from 'app/dim-ui/KeyHelp'; import { Loading } from 'app/dim-ui/Loading'; import Sheet from 'app/dim-ui/Sheet'; import UserGuideLink from 'app/dim-ui/UserGuideLink'; import { useFixOverscrollBehavior } from 'app/dim-ui/useFixOverscrollBehavior'; import { t } from 'app/i18next-t'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { toggleSearchResults } from 'app/shell/actions'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { isiOSBrowser } from 'app/utils/browsers'; import { Portal } from 'app/utils/temp-container'; import clsx from 'clsx'; import { UseComboboxState, UseComboboxStateChangeOptions, useCombobox } from 'downshift'; import { debounce } from 'es-toolkit'; import { AnimatePresence, LayoutGroup, Variants, motion } from 'motion/react'; import React, { Suspense, lazy, memo, useCallback, useDeferredValue, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import { AppIcon, closeIcon, disabledIcon, expandDownIcon, expandUpIcon, faClock, helpIcon, searchIcon, starIcon, starOutlineIcon, unTrackedIcon, } from '../shell/icons'; import HighlightedText from './HighlightedText'; import * as styles from './SearchBar.m.scss'; import { buildArmoryIndex } from './armory-search'; import createAutocompleter, { SearchItem, SearchItemType } from './autocomplete'; import { searchConfigSelector, validateQuerySelector } from './items/item-search-filter'; import { loadoutSearchConfigSelector, validateLoadoutQuerySelector, } from './loadouts/loadout-search-filter'; import { canonicalizeQuery, parseQuery } from './query-parser'; import './search-filter.scss'; export const searchButtonAnimateVariants: Variants = { hidden: { scale: 0 }, shown: { scale: 1 }, }; const searchItemIcons: { [key in SearchItemType]: string } = { [SearchItemType.Recent]: faClock, [SearchItemType.Saved]: starIcon, [SearchItemType.Suggested]: unTrackedIcon, // TODO: choose a real icon [SearchItemType.Autocomplete]: searchIcon, // TODO: choose a real icon [SearchItemType.Help]: helpIcon, [SearchItemType.ArmoryEntry]: helpIcon, }; const armoryIndexSelector = createSelector(d2ManifestSelector, languageSelector, buildArmoryIndex); const autoCompleterSelector = createSelector( searchConfigSelector, armoryIndexSelector, createAutocompleter, ); const loadoutAutoCompleterSelector = createSelector( loadoutSearchConfigSelector, () => undefined, createAutocompleter, ); const LazyFilterHelp = lazy(() => import(/* webpackChunkName: "filter-help" */ './FilterHelp')); const RowContents = memo(({ item }: { item: SearchItem }) => { function highlight(text: string, section: string) { return item.highlightRange?.section === section ? ( <HighlightedText text={text} startIndex={item.highlightRange.range[0]} endIndex={item.highlightRange.range[1]} className={styles.textHighlight} /> ) : ( text ); } switch (item.type) { case SearchItemType.Help: return <>{t('Header.FilterHelpMenuItem')}</>; case SearchItemType.ArmoryEntry: return ( <> {item.armoryItem.name} <span className={styles.openInArmoryLabel}>{` - ${t('Armory.OpenInArmory')}`}</span> <span className={styles.namedQueryBody}> {`${item.armoryItem.seasonName} (${t('Armory.Season', { season: item.armoryItem.season, year: item.armoryItem.year ?? '?', })})`} </span> </> ); default: return ( <> {item.query.header && highlight(item.query.header, 'header')} <span className={clsx({ [styles.namedQueryBody]: item.query.header !== undefined, })} > {highlight(item.query.body, 'body')} </span> </> ); } }); const Row = memo( ({ highlighted, item, isPhonePortrait, isTabAutocompleteItem, onClick, }: { highlighted: boolean; item: SearchItem; isPhonePortrait: boolean; isTabAutocompleteItem: boolean; onClick: (e: React.MouseEvent, item: SearchItem) => void; }) => ( <> {item.type === SearchItemType.ArmoryEntry ? ( <BungieImage className={styles.armoryItemIcon} src={item.armoryItem.icon} /> ) : ( <AppIcon className={styles.menuItemIcon} icon={searchItemIcons[item.type]} /> )} <p className={styles.menuItemQuery}> <RowContents item={item} /> </p> {!isPhonePortrait && isTabAutocompleteItem && ( <KeyHelp className={styles.keyHelp} combo="tab" /> )} {!isPhonePortrait && highlighted && <KeyHelp className={styles.keyHelp} combo="enter" />} {(item.type === SearchItemType.Recent || item.type === SearchItemType.Saved) && ( <button type="button" className={styles.deleteIcon} onClick={(e) => onClick(e, item)} title={t('Header.DeleteSearch')} > <AppIcon icon={closeIcon} /> </button> )} </> ), ); // TODO: break filter autocomplete into its own object/helpers... with tests /** An interface for interacting with the search filter through a ref */ export interface SearchFilterRef { /** Switch focus to the filter field */ focusFilterInput: () => void; /** Clear the filter field */ clearFilter: () => void; } const resultItemHeight = 32; /** * A reusable, autocompleting item search input. This is an uncontrolled input that * announces its query has changed only after some delay. This is the new version of the component * that offers a browser-style autocompleting search bar with history. * * TODO: Should this be the main search bar only, or should it also work for item picker, etc? */ function SearchBar({ searchQueryVersion, searchQuery, mainSearchBar, placeholder, children, onQueryChanged, instant, onClear, className, menu, searchType = SearchType.Item, ref, }: { /** Placeholder text when nothing has been typed */ placeholder: string; /** Is this the main search bar in the header? It behaves somewhat differently. */ mainSearchBar?: boolean; /** A fake property that can be used to force the "live" query to be replaced with the one from props */ searchQueryVersion?: number; /** The search query to fill in the input. This is used only initially, or when searchQueryVersion changes */ searchQuery?: string; /** Children are used as optional extra action buttons only when there is a query. */ children?: React.ReactNode; /** An optional menu of actions that can be executed on the search. Always shown. */ menu?: React.ReactNode; /** Whether this search bar applies to loadouts rather than items. */ searchType?: SearchType; instant?: boolean; className?: string; /** Fired whenever the query changes (already debounced) */ onQueryChanged: (query: string) => void; /** Fired whenever the query has been cleared */ onClear?: () => void; ref?: React.Ref<SearchFilterRef>; }) { const dispatch = useThunkDispatch(); const isPhonePortrait = useIsPhonePortrait(); const recentSearches = useSelector(recentSearchesSelector(searchType)); const autocompleter = useSelector( searchType === SearchType.Loadout ? loadoutAutoCompleterSelector : autoCompleterSelector, ); const validateQuery = useSelector( searchType === SearchType.Loadout ? validateLoadoutQuerySelector : validateQuerySelector, ); // On iOS at least, focusing the keyboard pushes the content off the screen const autoFocus = !mainSearchBar && !isPhonePortrait && !isiOSBrowser(); const [liveQueryLive, setLiveQuery] = useState(searchQuery ?? ''); const [filterHelpOpen, setFilterHelpOpen] = useState(false); const [armoryItemHash, setArmoryItemHash] = useState<number | undefined>(undefined); const [menuMaxHeight, setMenuMaxHeight] = useState<undefined | number>(); const inputElement = useRef<HTMLInputElement>(null); // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedUpdateQuery = useCallback( instant ? onQueryChanged : debounce((query: string) => { onQueryChanged(query); }, 500), [onQueryChanged], ); const liveQuery = useDeferredValue(liveQueryLive); const { valid, saveable } = validateQuery(liveQuery); const lastBlurQuery = useRef<string>(undefined); const onBlur = () => { if (valid && liveQuery && liveQuery !== lastBlurQuery.current) { // save this to the recent searches only on blur // we use the ref to only fire if the query changed since the last blur dispatch(searchUsed({ query: liveQuery, type: searchType })); lastBlurQuery.current = liveQuery; } }; // Is the current search saved? const canonical = liveQuery ? canonicalizeQuery(parseQuery(liveQuery)) : ''; const saved = canonical ? recentSearches.find((s) => s.query === canonical)?.saved : false; const toggleSaved = () => { // TODO: keep track of the last search, if you search for something more narrow immediately after then replace? dispatch(saveSearch({ query: liveQuery, saved: !saved, type: searchType })); }; // Try to fill up the screen with search results const maxResults = isPhonePortrait ? 7 // TODO: do this dynamically on mobile too, but the timing of when the virtual keyboard shows up is a nightmare : menuMaxHeight ? Math.floor((0.7 * menuMaxHeight) / resultItemHeight) : 10; const caretPosition = inputElement.current?.selectionStart || liveQuery.length; const items = useMemo( () => autocompleter( liveQuery, caretPosition, recentSearches, /* includeArmory */ Boolean(mainSearchBar), maxResults, ), [autocompleter, caretPosition, liveQuery, mainSearchBar, recentSearches, maxResults], ); // useCombobox from Downshift manages the state of the dropdown const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, getLabelProps, highlightedIndex, getItemProps, setInputValue, reset: clearFilter, } = useCombobox<SearchItem>({ items, stateReducer, initialInputValue: liveQuery, initialIsOpen: isPhonePortrait && mainSearchBar, defaultHighlightedIndex: liveQuery ? 0 : -1, itemToString: (i) => i?.query.fullText || '', onInputValueChange: ({ inputValue, type }) => { setLiveQuery(inputValue || ''); debouncedUpdateQuery(inputValue || ''); if (type === useCombobox.stateChangeTypes.FunctionReset) { onClear?.(); } }, }); // special click handling for filter helper function stateReducer( state: UseComboboxState<SearchItem>, actionAndChanges: UseComboboxStateChangeOptions<SearchItem>, ) { const { type, changes } = actionAndChanges; switch (type) { case useCombobox.stateChangeTypes.ItemClick: case useCombobox.stateChangeTypes.InputKeyDownEnter: if (!changes.selectedItem) { return changes; } switch (changes.selectedItem.type) { case SearchItemType.Help: setFilterHelpOpen(true); break; case SearchItemType.ArmoryEntry: setArmoryItemHash(changes.selectedItem.armoryItem.hash); break; default: // exit early if non FilterHelper item was selected return changes; } // helper click, open FilterHelper and modify state return { ...changes, selectedItem: state.selectedItem, // keep the last selected item (i.e. the edit field stays unchanged) }; default: return changes; // no handling for other types } } // Reset live query when search version changes useEffect(() => { if (searchQuery !== undefined && (searchQueryVersion || 0) > 0) { setInputValue(searchQuery); } // This should only happen when the query version changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQueryVersion]); // Determine a maximum height for the results menu useEffect(() => { if (inputElement.current && window.visualViewport) { const { y, height } = inputElement.current.getBoundingClientRect(); const { height: viewportHeight } = window.visualViewport; // pixels remaining in viewport minus offset minus 10px for padding const pxAvailable = viewportHeight - y - height - 10; // constrain to size that would allow only whole items to be seen setMenuMaxHeight(Math.floor(pxAvailable / resultItemHeight) * resultItemHeight); } }, [isOpen]); const deleteSearch = useCallback( (e: React.MouseEvent, item: SearchItem) => { e.stopPropagation(); dispatch(searchDeleted({ query: item.query.fullText, type: searchType })); }, [dispatch, searchType], ); // Add some methods for refs to use useImperativeHandle( ref, () => ({ focusFilterInput: () => { inputElement.current?.focus(); }, clearFilter, }), [clearFilter], ); // Implement tab completion on the tab key. If the highlighted item is an autocomplete suggestion, // accept it. Otherwise, we scan from the beginning to find the first autocomplete suggestion and // accept that. If there's nothing to accept, the tab key does its normal thing, which is to switch // focus. The tabAutocompleteItem is computed as part of render so we can offer keyboard help. const tabAutocompleteItem = highlightedIndex > 0 && items[highlightedIndex]?.type === SearchItemType.Autocomplete ? items[highlightedIndex] : items.find((s) => s.type === SearchItemType.Autocomplete && s.query.fullText !== liveQuery); const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Tab' && !e.altKey && !e.ctrlKey && tabAutocompleteItem && isOpen) { e.preventDefault(); if (inputElement.current) { // Use execCommand to make the insertion as if the user typed it, so it can be undone with Ctrl-Z inputElement.current.setSelectionRange(0, inputElement.current.value.length); document.execCommand('insertText', false, tabAutocompleteItem.query.fullText); if (tabAutocompleteItem.highlightRange) { const cursorPos = tabAutocompleteItem.highlightRange.range[1]; inputElement.current.setSelectionRange(cursorPos, cursorPos); } } } else if (e.key === 'Home' || e.key === 'End') { // Disable the use of Home/End to select items in the menu // https://github.com/downshift-js/downshift/issues/1162 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (e.nativeEvent as any).preventDownshiftDefault = true; } else if ( (e.key === 'Delete' || e.key === 'Backspace') && e.shiftKey && highlightedIndex >= 0 && items[highlightedIndex]?.query && items[highlightedIndex]?.type === SearchItemType.Recent ) { e.preventDefault(); dispatch(searchDeleted({ query: items[highlightedIndex].query.fullText, type: searchType })); } else if (e.key === 'Enter' && !isOpen && liveQuery) { // Show search results on "Enter" with a closed menu dispatch(toggleSearchResults()); } }; const menuRef = useRef<HTMLUListElement>(null); useFixOverscrollBehavior(menuRef); const autocompleteMenu = useMemo( () => ( <ul {...getMenuProps({ ref: menuRef })} className={styles.menu} style={{ maxHeight: menuMaxHeight, }} > {isOpen && items.map((item, index) => ( <li className={clsx(styles.menuItem, { [styles.highlightedItem]: highlightedIndex === index, })} key={`${item.type}${item.query.fullText}${ item.type === SearchItemType.ArmoryEntry && item.armoryItem.hash }`} {...getItemProps({ item, index })} > <Row highlighted={highlightedIndex === index} item={item} isPhonePortrait={isPhonePortrait} isTabAutocompleteItem={item === tabAutocompleteItem} onClick={deleteSearch} /> </li> ))} </ul> ), [ deleteSearch, getItemProps, getMenuProps, highlightedIndex, isOpen, isPhonePortrait, items, tabAutocompleteItem, menuMaxHeight, ], ); return ( <> <div className={clsx(className, 'search-filter', styles.searchBar, { [styles.open]: isOpen })} role="search" > <AppIcon {...getLabelProps({ icon: searchIcon, className: 'search-bar-icon' })} /> <input {...getInputProps({ onBlur, onKeyDown, ref: inputElement, className: clsx({ [styles.invalid]: !valid }), autoComplete: 'off', autoCorrect: 'off', autoCapitalize: 'off', spellCheck: false, autoFocus, placeholder, type: 'text', name: 'filter', 'aria-label': placeholder, })} enterKeyHint="search" /> <LayoutGroup> <AnimatePresence> {children} {liveQuery.length > 0 && valid && (saveable || saved) && !isPhonePortrait && ( <motion.button variants={searchButtonAnimateVariants} exit="hidden" initial="hidden" animate="shown" key="save" type="button" className={clsx(styles.filterBarButton, styles.saveSearchButton)} onClick={toggleSaved} title={t('Header.SaveSearch')} > <AppIcon icon={saved ? starIcon : starOutlineIcon} /> </motion.button> )} {(liveQuery.length > 0 || (isPhonePortrait && mainSearchBar)) && ( <motion.button variants={searchButtonAnimateVariants} exit="hidden" initial="hidden" animate="shown" key="clear" type="button" className={styles.filterBarButton} onClick={clearFilter} title={t('Header.Clear')} > <AppIcon icon={disabledIcon} /> </motion.button> )} {menu} <motion.button layout key="menu" {...getToggleButtonProps({ type: 'button', className: clsx(styles.filterBarButton, styles.openButton), 'aria-label': 'toggle menu', })} > <AppIcon icon={isOpen ? expandUpIcon : expandDownIcon} /> </motion.button> </AnimatePresence> </LayoutGroup> {filterHelpOpen && ( <Suspense fallback={ <Portal> <Loading message={t('Loading.FilterHelp')} /> </Portal> } > {/* Because FilterHelp suspends, the entire sheet will suspend while it is loaded. * This stops us having issues with incorrect frozen initial heights as it will * get locked to the fallback height if we don't do this. */} <Sheet onClose={() => setFilterHelpOpen(false)} header={ <> <h1>{t('Header.Filters')}</h1> <UserGuideLink topic="Item-Search" /> </> } freezeInitialHeight sheetClassName={styles.filterHelp} > <LazyFilterHelp searchType={searchType} /> </Sheet> </Suspense> )} {autocompleteMenu} </div> {armoryItemHash !== undefined && ( <ArmorySheet itemHash={armoryItemHash} onClose={() => setArmoryItemHash(undefined)} /> )} </> ); } export default memo(SearchBar); ================================================ FILE: src/app/search/SearchFilter.tsx ================================================ import { SearchType } from '@destinyitemmanager/dim-api-types'; import { t } from 'app/i18next-t'; import { exampleLOSearch } from 'app/loadout-builder/example-search'; import { querySelector, searchQueryVersionSelector, useIsPhonePortrait } from 'app/shell/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import React, { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import { setSearchQuery } from '../shell/actions'; import MainSearchBarActions from './MainSearchBarActions'; import MainSearchBarMenu from './MainSearchBarMenu'; import SearchBar, { SearchFilterRef } from './SearchBar'; /** * The main search filter that's in the header. */ export default function SearchFilter({ onClear, ref, }: { onClear?: () => void; ref: React.Ref<SearchFilterRef>; }) { const searchQuery = useSelector(querySelector); const searchQueryVersion = useSelector(searchQueryVersionSelector); const isPhonePortrait = useIsPhonePortrait(); const onClearFilter = useCallback(() => { onClear?.(); }, [onClear]); const location = useLocation(); const dispatch = useThunkDispatch(); const onQueryChanged = useCallback( (query: string) => dispatch(setSearchQuery(query, false)), [dispatch], ); // We don't have access to the selected store so we'd match multiple characters' worth. // Just suppress the count for now const onRecords = location.pathname.endsWith('records'); const onProgress = location.pathname.endsWith('progress'); const onOptimizer = location.pathname.endsWith('optimizer'); const onLoadouts = location.pathname.endsWith('loadouts'); const placeholder = useMemo( () => onRecords ? t('Header.FilterHelpRecords') : onProgress ? t('Header.FilterHelpProgress') : onOptimizer ? t('Header.FilterHelpOptimizer', { example: exampleLOSearch, }) : onLoadouts ? t('Header.FilterHelpLoadouts') : isPhonePortrait ? t('Header.FilterHelpBrief') : t('Header.FilterHelp', { example: 'is:dupe, is:maxpower, -is:blue' }), [isPhonePortrait, onRecords, onProgress, onOptimizer, onLoadouts], ); const extras = useMemo(() => <MainSearchBarActions key="actions" />, []); const menu = useMemo(() => <MainSearchBarMenu key="actions-menu" />, []); return ( <SearchBar ref={ref} onQueryChanged={onQueryChanged} placeholder={placeholder} onClear={onClearFilter} searchQueryVersion={searchQueryVersion} searchQuery={searchQuery} mainSearchBar={true} menu={menu} searchType={onLoadouts ? SearchType.Loadout : SearchType.Item} > {!onLoadouts && extras} </SearchBar> ); } ================================================ FILE: src/app/search/SearchHistory.m.scss ================================================ .searchHistory { list-style: none; table { border-spacing: 0; width: 100%; } th { text-align: left; border-bottom: 1px solid white; white-space: nowrap; } td, th { vertical-align: top; padding: 16px 8px; } td { user-select: text; } tr:nth-child(2n) { td { background-color: rgb(0, 0, 0, 0.2); } } } .tabs { max-width: fit-content; margin-top: 8px; } .iconButton { composes: resetButton from '../dim-ui/common.m.scss'; margin-left: 6px; color: var(--theme-text); padding: 0 2px; } .date { white-space: nowrap; } .instructions { margin: 1em; button { float: right; } } .sorter { margin-left: 2px; } ================================================ FILE: src/app/search/SearchHistory.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'date': string; 'iconButton': string; 'instructions': string; 'searchHistory': string; 'sorter': string; 'tabs': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/search/SearchHistory.tsx ================================================ import { Search, SearchType } from '@destinyitemmanager/dim-api-types'; import { saveSearch, searchDeleted } from 'app/dim-api/basic-actions'; import { recentSearchesSelector } from 'app/dim-api/selectors'; import RadioButtons, { Option } from 'app/dim-ui/RadioButtons'; import { ColumnSort, SortDirection, useTableColumnSorts } from 'app/dim-ui/table-columns'; import { t } from 'app/i18next-t'; import { AppIcon, closeIcon, faCaretDown, faCaretUp, starIcon, starOutlineIcon, } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { Comparator, chainComparator, compareBy, reverseComparator } from 'app/utils/comparators'; import { useShiftHeld } from 'app/utils/hooks'; import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import * as styles from './SearchHistory.m.scss'; function comparatorFor(id: string): Comparator<Search> { switch (id) { case 'last_used': return compareBy((s) => s.lastUsage); case 'starred': return compareBy((s) => s.saved); case 'times_used': return compareBy((s) => s.usageCount); case 'query': return compareBy((s) => s.query); default: throw new Error(`internal error, unhandled column ${id}`); } } export default function SearchHistory() { const dispatch = useThunkDispatch(); const [searchType, setSearchType] = useState(SearchType.Item); const recentSearches = useSelector(recentSearchesSelector(searchType)); const [columnSorts, toggleColumnSort] = useTableColumnSorts([ { columnId: 'starred', sort: SortDirection.DESC }, { columnId: 'last_used', sort: SortDirection.DESC }, ]); const shiftHeld = useShiftHeld(); const deleteSearch = (e: React.MouseEvent, item: Search) => { e.stopPropagation(); dispatch(searchDeleted({ query: item.query, type: item.type })); }; const toggleSaved = (item: Search) => { dispatch(saveSearch({ query: item.query, saved: !item.saved, type: item.type })); }; const onDeleteAll = () => { for (const s of recentSearches.filter((s) => !s.saved)) { dispatch(searchDeleted({ query: s.query, type: s.type })); } }; const onToggleSort = (columnId: string, defaultDirection: SortDirection) => toggleColumnSort(columnId, shiftHeld, defaultDirection); const headers: [string, React.ReactNode, SortDirection][] = [ ['last_used', t('SearchHistory.Date'), SortDirection.DESC], ['times_used', t('SearchHistory.UsageCount'), SortDirection.DESC], ['starred', <AppIcon key="star" icon={starIcon} />, SortDirection.DESC], ['query', t('SearchHistory.Query'), SortDirection.ASC], ]; const searchComparator = chainComparator( ...columnSorts.map((sort) => sort.sort === SortDirection.DESC ? reverseComparator(comparatorFor(sort.columnId)) : comparatorFor(sort.columnId), ), ); const radioOptions: Option<SearchType>[] = [ { label: t('SearchHistory.Item'), value: SearchType.Item }, { label: t('SearchHistory.Loadout'), value: SearchType.Loadout }, ]; // TODO: Tabs return ( <div className={styles.searchHistory}> <p className={styles.instructions}> {t('SearchHistory.Description')} <button type="button" className="dim-button" onClick={onDeleteAll}> {t('SearchHistory.DeleteAll')} </button> <RadioButtons className={styles.tabs} options={radioOptions} value={searchType} onChange={setSearchType} /> </p> <table> <thead> <tr> <th /> {headers.map(([id, contents, defaultDirection]) => ( <ColumnHeader columnSorts={columnSorts} defaultDirection={defaultDirection} toggleColumnSort={onToggleSort} columnId={id} key={id} > {contents} </ColumnHeader> ))} </tr> </thead> <tbody> {recentSearches .filter((s) => s.usageCount > 0) .sort(searchComparator) .map((search) => ( <tr key={search.query}> <td> <button type="button" onClick={(e) => deleteSearch(e, search)} title={t('Header.DeleteSearch')} className={styles.iconButton} > <AppIcon icon={closeIcon} /> </button> </td> <td className={styles.date}>{new Date(search.lastUsage).toLocaleString()}</td> <td>{search.usageCount}</td> <td> <button type="button" className={styles.iconButton} onClick={() => toggleSaved(search)} title={t('Header.SaveSearch')} > <AppIcon icon={search.saved ? starIcon : starOutlineIcon} /> </button> </td> <td>{search.query}</td> </tr> ))} </tbody> </table> </div> ); } function ColumnHeader({ columnId, children, defaultDirection, columnSorts, toggleColumnSort, }: { columnId: string; children: React.ReactNode; defaultDirection: SortDirection; columnSorts: ColumnSort[]; toggleColumnSort: (columnId: string, direction: SortDirection) => () => void; }) { const sort = columnSorts.find((c) => c.columnId === columnId); return ( <th onClick={toggleColumnSort(columnId, defaultDirection)}> {children} {sort && ( <AppIcon className={styles.sorter} icon={sort.sort === SortDirection.DESC ? faCaretDown : faCaretUp} /> )} </th> ); } ================================================ FILE: src/app/search/SearchInput.tsx ================================================ import { searchIcon } from 'app/shell/icons'; import AppIcon from 'app/shell/icons/AppIcon'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { isiOSBrowser } from 'app/utils/browsers'; /** * A styled text input without fancy features like autocompletion or de-bouncing. */ export function SearchInput({ onQueryChanged, placeholder, autoFocus, query, }: { onQueryChanged: (newValue: string) => void; placeholder?: string; autoFocus?: boolean; query?: string; }) { const isPhonePortrait = useIsPhonePortrait(); // On iOS at least, focusing the keyboard pushes the content off the screen const nativeAutoFocus = !isPhonePortrait && !isiOSBrowser(); return ( <div className="search-filter" role="search"> <AppIcon icon={searchIcon} className="search-bar-icon" /> <input autoComplete="off" autoCorrect="off" autoCapitalize="off" autoFocus={autoFocus && nativeAutoFocus} placeholder={placeholder} type="text" name="filter" value={query} onChange={(e) => onQueryChanged(e.currentTarget.value)} /> </div> ); } ================================================ FILE: src/app/search/SearchResults.m.scss ================================================ @use '../variables.scss' as *; .searchResults { max-height: calc(var(--viewport-height) - var(--header-height) - 16px); } .header { margin: 0; } .contents { min-height: calc(#{$badge-height} + var(--item-size) + 4px) !important; margin: 10px; } ================================================ FILE: src/app/search/SearchResults.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'contents': string; 'header': string; 'searchResults': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/search/SearchResults.tsx ================================================ import ClickOutsideRoot from 'app/dim-ui/ClickOutsideRoot'; import { t } from 'app/i18next-t'; import ConnectedInventoryItem from 'app/inventory/ConnectedInventoryItem'; import DraggableInventoryItem from 'app/inventory/DraggableInventoryItem'; import ItemPopupTrigger from 'app/inventory/ItemPopupTrigger'; import { moveItemToCurrentStore } from 'app/inventory/move-item'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import clsx from 'clsx'; import { memo, useCallback } from 'react'; import { useSelector } from 'react-redux'; import Sheet from '../dim-ui/Sheet'; import '../inventory-page/StoreBucket.scss'; import { DimItem } from '../inventory/item-types'; import { itemSorterSelector } from '../settings/item-sort'; import * as styles from './SearchResults.m.scss'; /** * This displays all the items that match the given search - it is shown by default when a search is active * on mobile, and as a sheet when you hit "enter" on desktop. */ export default memo(function SearchResults({ items, onClose, }: { items: DimItem[]; onClose: () => void; }) { const sortItems = useSelector(itemSorterSelector); const header = ( <div> <h1 className={styles.header}>{t('Header.FilterMatchCount', { count: items.length })}</h1> </div> ); // TODO: actions footer? // TODO: categories? return ( <Sheet onClose={onClose} header={header} sheetClassName={clsx('item-picker', styles.searchResults)} allowClickThrough={true} > <ClickOutsideRoot> <div className={clsx('sub-bucket', styles.contents)}> {sortItems(items).map((item) => ( <SearchResultItem key={item.index} item={item} /> ))} </div> </ClickOutsideRoot> </Sheet> ); }); function SearchResultItem({ item }: { item: DimItem }) { const dispatch = useThunkDispatch(); const doubleClicked = useCallback( (e: React.MouseEvent) => dispatch(moveItemToCurrentStore(item, e)), [dispatch, item], ); return ( <DraggableInventoryItem item={item}> <ItemPopupTrigger item={item} key={item.index}> {(ref, onClick) => ( <ConnectedInventoryItem item={item} ref={ref} onClick={onClick} onDoubleClick={doubleClicked} /> )} </ItemPopupTrigger> </DraggableInventoryItem> ); } ================================================ FILE: src/app/search/__snapshots__/autocomplete.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`autocompleteTermSuggestions autocomplete within query for plain string match {jotu} - {jötunn} 1`] = ` [ { "highlightRange": { "range": [ 0, 13, ], "section": "body", }, "query": { "body": "name:"jötunn"", "fullText": "name:"jötunn"", "helpText": undefined, }, "type": 3, }, ] `; exports[`filterComplete autocomplete terms for |is:b| 1`] = ` [ "is:bow", "is:blue", ] `; exports[`filterComplete autocomplete terms for |jun| 1`] = ` [ "tag:junk", ] `; exports[`filterComplete autocomplete terms for |ote| 1`] = ` [ "is:hasnotes", "is:emotes", "notes:", ] `; exports[`filterComplete autocomplete terms for |sni| 1`] = ` [ "is:sniperrifle", ] `; exports[`filterComplete autocomplete terms for |stat:| 1`] = ` [ "stat:rpm:", "stat:rof:", "stat:charge:", "stat:impact:", "stat:handling:", "stat:ventspeed:", "stat:heatgen:", "stat:cooling:", "stat:range:", "stat:stability:", "stat:reload:", "stat:magazine:", "stat:aimassist:", "stat:equipspeed:", "stat:shieldduration:", "stat:velocity:", "stat:blastradius:", "stat:recoildirection:", "stat:drawtime:", "stat:zoom:", "stat:airborne:", "stat:accuracy:", "stat:ammogen:", "stat:persistence:", "stat:swingspeed:", "stat:guardefficiency:", "stat:guardresistance:", "stat:chargerate:", "stat:guardendurance:", "stat:ammocapacity:", "stat:weapons:", "stat:health:", "stat:class:", "stat:grenade:", "stat:super:", "stat:melee:", "stat:mobility:", "stat:resilience:", "stat:recovery:", "stat:discipline:", "stat:intellect:", "stat:strength:", "stat:total:", "stat:any:", "stat:highest:", "stat:secondhighest:", "stat:thirdhighest:", "stat:fourthhighest:", "stat:fifthhighest:", "stat:sixthhighest:", "stat:rpm:<", "stat:rpm:>", "stat:rof:<", "stat:rof:>", "stat:charge:<", "stat:charge:>", "stat:impact:<", "stat:impact:>", "stat:handling:<", "stat:handling:>", "stat:ventspeed:<", "stat:ventspeed:>", "stat:heatgen:<", "stat:heatgen:>", "stat:cooling:<", "stat:cooling:>", "stat:range:<", "stat:range:>", "stat:stability:<", "stat:stability:>", "stat:reload:<", "stat:reload:>", "stat:magazine:<", "stat:magazine:>", "stat:aimassist:<", "stat:aimassist:>", "stat:equipspeed:<", "stat:equipspeed:>", "stat:shieldduration:<", "stat:shieldduration:>", "stat:velocity:<", "stat:velocity:>", "stat:blastradius:<", "stat:blastradius:>", "stat:recoildirection:<", "stat:recoildirection:>", "stat:drawtime:<", "stat:drawtime:>", "stat:zoom:<", "stat:zoom:>", "stat:airborne:<", "stat:airborne:>", "stat:accuracy:<", "stat:accuracy:>", "stat:ammogen:<", "stat:ammogen:>", "stat:persistence:<", "stat:persistence:>", "stat:swingspeed:<", "stat:swingspeed:>", "stat:guardefficiency:<", "stat:guardefficiency:>", "stat:guardresistance:<", "stat:guardresistance:>", "stat:chargerate:<", "stat:chargerate:>", "stat:guardendurance:<", "stat:guardendurance:>", "stat:ammocapacity:<", "stat:ammocapacity:>", "stat:weapons:<", "stat:weapons:>", "stat:health:<", "stat:health:>", "stat:class:<", "stat:class:>", "stat:grenade:<", "stat:grenade:>", "stat:super:<", "stat:super:>", "stat:melee:<", "stat:melee:>", "stat:mobility:<", "stat:mobility:>", "stat:resilience:<", "stat:resilience:>", "stat:recovery:<", "stat:recovery:>", "stat:discipline:<", "stat:discipline:>", "stat:intellect:<", "stat:intellect:>", "stat:strength:<", "stat:strength:>", "stat:total:<", "stat:total:>", "stat:any:<", "stat:any:>", "stat:highest:<", "stat:highest:>", "stat:secondhighest:<", "stat:secondhighest:>", "stat:thirdhighest:<", "stat:thirdhighest:>", "stat:fourthhighest:<", "stat:fourthhighest:>", "stat:fifthhighest:<", "stat:fifthhighest:>", "stat:sixthhighest:<", "stat:sixthhighest:>", "stat:rpm:<=", "stat:rpm:>=", "stat:rof:<=", "stat:rof:>=", "stat:charge:<=", "stat:charge:>=", "stat:impact:<=", "stat:impact:>=", "stat:handling:<=", "stat:handling:>=", "stat:ventspeed:<=", "stat:ventspeed:>=", "stat:heatgen:<=", "stat:heatgen:>=", "stat:cooling:<=", "stat:cooling:>=", "stat:range:<=", "stat:range:>=", "stat:stability:<=", "stat:stability:>=", "stat:reload:<=", "stat:reload:>=", "stat:magazine:<=", "stat:magazine:>=", "stat:aimassist:<=", "stat:aimassist:>=", "stat:equipspeed:<=", "stat:equipspeed:>=", "stat:shieldduration:<=", "stat:shieldduration:>=", "stat:velocity:<=", "stat:velocity:>=", "stat:blastradius:<=", "stat:blastradius:>=", "stat:recoildirection:<=", "stat:recoildirection:>=", "stat:drawtime:<=", "stat:drawtime:>=", "stat:zoom:<=", "stat:zoom:>=", "stat:airborne:<=", "stat:airborne:>=", "stat:accuracy:<=", "stat:accuracy:>=", "stat:ammogen:<=", "stat:ammogen:>=", "stat:persistence:<=", "stat:persistence:>=", "stat:swingspeed:<=", "stat:swingspeed:>=", "stat:guardefficiency:<=", "stat:guardefficiency:>=", "stat:guardresistance:<=", "stat:guardresistance:>=", "stat:chargerate:<=", "stat:chargerate:>=", "stat:guardendurance:<=", "stat:guardendurance:>=", "stat:ammocapacity:<=", "stat:ammocapacity:>=", "stat:weapons:<=", "stat:weapons:>=", "stat:health:<=", "stat:health:>=", "stat:class:<=", "stat:class:>=", "stat:grenade:<=", "stat:grenade:>=", "stat:super:<=", "stat:super:>=", "stat:melee:<=", "stat:melee:>=", "stat:mobility:<=", "stat:mobility:>=", "stat:resilience:<=", "stat:resilience:>=", "stat:recovery:<=", "stat:recovery:>=", "stat:discipline:<=", "stat:discipline:>=", "stat:intellect:<=", "stat:intellect:>=", "stat:strength:<=", "stat:strength:>=", "stat:total:<=", "stat:total:>=", "stat:any:<=", "stat:any:>=", "stat:highest:<=", "stat:highest:>=", "stat:secondhighest:<=", "stat:secondhighest:>=", "stat:thirdhighest:<=", "stat:thirdhighest:>=", "stat:fourthhighest:<=", "stat:fourthhighest:>=", "stat:fifthhighest:<=", "stat:fifthhighest:>=", "stat:sixthhighest:<=", "stat:sixthhighest:>=", ] `; exports[`filterComplete autocomplete terms for |stat:mob| 1`] = ` [ "stat:mobility:", ] `; exports[`filterComplete autocomplete terms for |stat| 1`] = ` [ "stat:", "dupe:stats", "dupe:statlower", "basestat:", "maxstatloadout:", "maxstatvalue:", "maxbasestatvalue:", "primarystat:", "secondarystat:", "tertiarystat:", "tunedstat:", "dupe:archetype+tertiarystat", "dupe:nonzerostats", "dupe:setbonus+statlower", "dupe:customstatlower", "source:heliostat", "maxstatloadout:weapons", "maxstatloadout:health", "maxstatloadout:class", "maxstatloadout:grenade", "maxstatloadout:super", "maxstatloadout:melee", "maxstatloadout:mobility", "maxstatloadout:resilience", "maxstatloadout:recovery", "maxstatloadout:discipline", "maxstatloadout:intellect", "maxstatloadout:strength", "maxstatloadout:total", "maxstatvalue:weapons", "maxstatvalue:health", "maxstatvalue:class", "maxstatvalue:grenade", "maxstatvalue:super", "maxstatvalue:melee", "maxstatvalue:mobility", "maxstatvalue:resilience", "maxstatvalue:recovery", "maxstatvalue:discipline", "maxstatvalue:intellect", "maxstatvalue:strength", "maxstatvalue:total", "maxstatvalue:any", "maxbasestatvalue:weapons", "maxbasestatvalue:health", "maxbasestatvalue:class", "maxbasestatvalue:grenade", "maxbasestatvalue:super", "maxbasestatvalue:melee", "maxbasestatvalue:mobility", "maxbasestatvalue:resilience", "maxbasestatvalue:recovery", "maxbasestatvalue:discipline", "maxbasestatvalue:intellect", "maxbasestatvalue:strength", "maxbasestatvalue:total", "maxbasestatvalue:any", "primarystat:weapons", "secondarystat:weapons", "tertiarystat:weapons", "primarystat:health", "secondarystat:health", "tertiarystat:health", "primarystat:class", "secondarystat:class", "tertiarystat:class", "primarystat:grenade", "secondarystat:grenade", "tertiarystat:grenade", "primarystat:super", "secondarystat:super", "tertiarystat:super", "primarystat:melee", "secondarystat:melee", "tertiarystat:melee", "tunedstat:weapons", "tunedstat:health", "tunedstat:class", "tunedstat:grenade", "tunedstat:super", "tunedstat:melee", "tunedstat:primary", "tunedstat:secondary", "tunedstat:tertiary", "tunedstat:unfocused", "stat:rpm:", "stat:rof:", "stat:charge:", "stat:impact:", "stat:handling:", "stat:ventspeed:", "stat:heatgen:", "stat:cooling:", "stat:range:", "stat:stability:", "stat:reload:", "stat:magazine:", "stat:aimassist:", "stat:equipspeed:", "stat:shieldduration:", "stat:velocity:", "stat:blastradius:", "stat:recoildirection:", "stat:drawtime:", "stat:zoom:", "stat:airborne:", "stat:accuracy:", "stat:ammogen:", "stat:persistence:", "stat:swingspeed:", "stat:guardefficiency:", "stat:guardresistance:", "stat:chargerate:", "stat:guardendurance:", "stat:ammocapacity:", "stat:weapons:", "stat:health:", "stat:class:", "stat:grenade:", "stat:super:", "stat:melee:", "stat:mobility:", "stat:resilience:", "stat:recovery:", "stat:discipline:", "stat:intellect:", "stat:strength:", "stat:total:", "stat:any:", "stat:highest:", "stat:secondhighest:", "stat:thirdhighest:", "stat:fourthhighest:", "stat:fifthhighest:", "stat:sixthhighest:", "basestat:weapons:", "basestat:health:", "basestat:class:", "basestat:grenade:", "basestat:super:", "basestat:melee:", "basestat:mobility:", "basestat:resilience:", "basestat:recovery:", "basestat:discipline:", "basestat:intellect:", "basestat:strength:", "basestat:total:", "basestat:any:", "basestat:highest:", "basestat:secondhighest:", "basestat:thirdhighest:", "basestat:fourthhighest:", "basestat:fifthhighest:", "basestat:sixthhighest:", ] `; exports[`filterSortRecentSearches check saved search highlighting for query || 1`] = ` [ { "query": { "body": "is:patternunlocked -is:crafted", "fullText": "is:patternunlocked -is:crafted", "header": undefined, }, "type": 1, }, { "query": { "body": "is:patternunlocked -is:crafted", "fullText": "/* random-roll craftable guns */ is:patternunlocked -is:crafted", "header": "random-roll craftable guns", }, "type": 1, }, ] `; exports[`filterSortRecentSearches check saved search highlighting for query |craft| 1`] = ` [ { "highlightRange": { "range": [ 23, 28, ], "section": "body", }, "query": { "body": "is:patternunlocked -is:crafted", "fullText": "is:patternunlocked -is:crafted", "header": undefined, }, "type": 1, }, { "highlightRange": { "range": [ 12, 17, ], "section": "header", }, "query": { "body": "is:patternunlocked -is:crafted", "fullText": "/* random-roll craftable guns */ is:patternunlocked -is:crafted", "header": "random-roll craftable guns", }, "type": 1, }, ] `; exports[`filterSortRecentSearches check saved search highlighting for query |craftable| 1`] = ` [ { "highlightRange": { "range": [ 12, 21, ], "section": "header", }, "query": { "body": "is:patternunlocked -is:crafted", "fullText": "/* random-roll craftable guns */ is:patternunlocked -is:crafted", "header": "random-roll craftable guns", }, "type": 1, }, ] `; exports[`filterSortRecentSearches check saved search highlighting for query |crafted| 1`] = ` [ { "highlightRange": { "range": [ 23, 30, ], "section": "body", }, "query": { "body": "is:patternunlocked -is:crafted", "fullText": "is:patternunlocked -is:crafted", "header": undefined, }, "type": 1, }, { "highlightRange": { "range": [ 23, 30, ], "section": "body", }, "query": { "body": "is:patternunlocked -is:crafted", "fullText": "/* random-roll craftable guns */ is:patternunlocked -is:crafted", "header": "random-roll craftable guns", }, "type": 1, }, ] `; exports[`filterSortRecentSearches filter/sort recent searches for query || 1`] = ` [ "recent saved", "yearold saved", "0 days old, 9 uses", "0 days old, 8 uses", "dayold highuse", "0 days old, 7 uses", "1 days old, 9 uses", "1 days old, 8 uses", "0 days old, 6 uses", "1 days old, 7 uses", "2 days old, 9 uses", "2 days old, 8 uses", "1 days old, 6 uses", "2 days old, 7 uses", "3 days old, 9 uses", "0 days old, 5 uses", "3 days old, 8 uses", "2 days old, 6 uses", "3 days old, 7 uses", "4 days old, 9 uses", "1 days old, 5 uses", "4 days old, 8 uses", "3 days old, 6 uses", "4 days old, 7 uses", "5 days old, 9 uses", "2 days old, 5 uses", "5 days old, 8 uses", "4 days old, 6 uses", "0 days old, 4 uses", "5 days old, 7 uses", "6 days old, 9 uses", "3 days old, 5 uses", "6 days old, 8 uses", "5 days old, 6 uses", "1 days old, 4 uses", "6 days old, 7 uses", "7 days old, 9 uses", "4 days old, 5 uses", "7 days old, 8 uses", "6 days old, 6 uses", "2 days old, 4 uses", "7 days old, 7 uses", "8 days old, 9 uses", "5 days old, 5 uses", "8 days old, 8 uses", "7 days old, 6 uses", "3 days old, 4 uses", "8 days old, 7 uses", "9 days old, 9 uses", "6 days old, 5 uses", "9 days old, 8 uses", "8 days old, 6 uses", "4 days old, 4 uses", "9 days old, 7 uses", "10 days old, 9 uses", "0 days old, 3 uses", "7 days old, 5 uses", "10 days old, 8 uses", "9 days old, 6 uses", "5 days old, 4 uses", "10 days old, 7 uses", "11 days old, 9 uses", "1 days old, 3 uses", "8 days old, 5 uses", "11 days old, 8 uses", "10 days old, 6 uses", "6 days old, 4 uses", "11 days old, 7 uses", "12 days old, 9 uses", "2 days old, 3 uses", "9 days old, 5 uses", "12 days old, 8 uses", "11 days old, 6 uses", "7 days old, 4 uses", "12 days old, 7 uses", "13 days old, 9 uses", "3 days old, 3 uses", "10 days old, 5 uses", "13 days old, 8 uses", "12 days old, 6 uses", "8 days old, 4 uses", "13 days old, 7 uses", "14 days old, 9 uses", "4 days old, 3 uses", "11 days old, 5 uses", "14 days old, 8 uses", "13 days old, 6 uses", "9 days old, 4 uses", "14 days old, 7 uses", "15 days old, 9 uses", "5 days old, 3 uses", "12 days old, 5 uses", "15 days old, 8 uses", "14 days old, 6 uses", "10 days old, 4 uses", "15 days old, 7 uses", "16 days old, 9 uses", "6 days old, 3 uses", "13 days old, 5 uses", "16 days old, 8 uses", "15 days old, 6 uses", "11 days old, 4 uses", "16 days old, 7 uses", "17 days old, 9 uses", "7 days old, 3 uses", "14 days old, 5 uses", "17 days old, 8 uses", "0 days old, 2 uses", "16 days old, 6 uses", "12 days old, 4 uses", "17 days old, 7 uses", "18 days old, 9 uses", "8 days old, 3 uses", "15 days old, 5 uses", "18 days old, 8 uses", "1 days old, 2 uses", "17 days old, 6 uses", "13 days old, 4 uses", "18 days old, 7 uses", "19 days old, 9 uses", "9 days old, 3 uses", "16 days old, 5 uses", "19 days old, 8 uses", "2 days old, 2 uses", "18 days old, 6 uses", "14 days old, 4 uses", "19 days old, 7 uses", "20 days old, 9 uses", "10 days old, 3 uses", "17 days old, 5 uses", "20 days old, 8 uses", "3 days old, 2 uses", "19 days old, 6 uses", "15 days old, 4 uses", "20 days old, 7 uses", "21 days old, 9 uses", "11 days old, 3 uses", "18 days old, 5 uses", "21 days old, 8 uses", "4 days old, 2 uses", "20 days old, 6 uses", "16 days old, 4 uses", "21 days old, 7 uses", "22 days old, 9 uses", "12 days old, 3 uses", "19 days old, 5 uses", "22 days old, 8 uses", "5 days old, 2 uses", "21 days old, 6 uses", "17 days old, 4 uses", "22 days old, 7 uses", "23 days old, 9 uses", "13 days old, 3 uses", "20 days old, 5 uses", "23 days old, 8 uses", "6 days old, 2 uses", "22 days old, 6 uses", "18 days old, 4 uses", "23 days old, 7 uses", "24 days old, 9 uses", "14 days old, 3 uses", "21 days old, 5 uses", "24 days old, 8 uses", "7 days old, 2 uses", "23 days old, 6 uses", "19 days old, 4 uses", "24 days old, 7 uses", "25 days old, 9 uses", "15 days old, 3 uses", "22 days old, 5 uses", "25 days old, 8 uses", "8 days old, 2 uses", "24 days old, 6 uses", "20 days old, 4 uses", "25 days old, 7 uses", "26 days old, 9 uses", "16 days old, 3 uses", "23 days old, 5 uses", "26 days old, 8 uses", "9 days old, 2 uses", "25 days old, 6 uses", "21 days old, 4 uses", "26 days old, 7 uses", "27 days old, 9 uses", "17 days old, 3 uses", "24 days old, 5 uses", "27 days old, 8 uses", "10 days old, 2 uses", "26 days old, 6 uses", "22 days old, 4 uses", "27 days old, 7 uses", "28 days old, 9 uses", "18 days old, 3 uses", "25 days old, 5 uses", "28 days old, 8 uses", "11 days old, 2 uses", "27 days old, 6 uses", "23 days old, 4 uses", "28 days old, 7 uses", "29 days old, 9 uses", "19 days old, 3 uses", "26 days old, 5 uses", "29 days old, 8 uses", "12 days old, 2 uses", "28 days old, 6 uses", "24 days old, 4 uses", "29 days old, 7 uses", "20 days old, 3 uses", "27 days old, 5 uses", "13 days old, 2 uses", "29 days old, 6 uses", "25 days old, 4 uses", "21 days old, 3 uses", "28 days old, 5 uses", "14 days old, 2 uses", "26 days old, 4 uses", "22 days old, 3 uses", "29 days old, 5 uses", "15 days old, 2 uses", "27 days old, 4 uses", "0 days old, 1 uses", "23 days old, 3 uses", "16 days old, 2 uses", "28 days old, 4 uses", "1 days old, 1 uses", "24 days old, 3 uses", "17 days old, 2 uses", "29 days old, 4 uses", "2 days old, 1 uses", "25 days old, 3 uses", "18 days old, 2 uses", "3 days old, 1 uses", "26 days old, 3 uses", "19 days old, 2 uses", "4 days old, 1 uses", "27 days old, 3 uses", "20 days old, 2 uses", "5 days old, 1 uses", "28 days old, 3 uses", "21 days old, 2 uses", "6 days old, 1 uses", "29 days old, 3 uses", "22 days old, 2 uses", "7 days old, 1 uses", "23 days old, 2 uses", "8 days old, 1 uses", "24 days old, 2 uses", "9 days old, 1 uses", "25 days old, 2 uses", "10 days old, 1 uses", "26 days old, 2 uses", "11 days old, 1 uses", "27 days old, 2 uses", "12 days old, 1 uses", "28 days old, 2 uses", "13 days old, 1 uses", "29 days old, 2 uses", "14 days old, 1 uses", "15 days old, 1 uses", "16 days old, 1 uses", "17 days old, 1 uses", "18 days old, 1 uses", "19 days old, 1 uses", "20 days old, 1 uses", "21 days old, 1 uses", "22 days old, 1 uses", "23 days old, 1 uses", "24 days old, 1 uses", "25 days old, 1 uses", "26 days old, 1 uses", "27 days old, 1 uses", "28 days old, 1 uses", "29 days old, 1 uses", "yearold highuse", "yearold unsaved", "dim api autosuggest", ] `; exports[`filterSortRecentSearches filter/sort recent searches for query |high| 1`] = ` [ "dayold highuse", "yearold highuse", ] `; ================================================ FILE: src/app/search/__snapshots__/query-parser.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`parse | |: ast 1`] = ` { "length": 0, "op": "noop", "startIndex": 0, } `; exports[`parse | |: lexer 1`] = `[]`; exports[`parse | (\\n (\\n (is:weapon -is:maxpower powerlimit:1060 or tag:junk or is:blue)\\n or\\n (\\n (is:armor -is:exotic -is:classitem)\\n \\n -(is:titan (basestat:recovery:>=18 or basestat:total:>=63))\\n -(is:hunter ((basestat:recovery:>=13 basestat:mobility:>=18) or basestat:recovery:>15 or basestat:total:>=63))\\n -(is:warlock ((basestat:recovery:>=18 discipline:>=17) or basestat:total:>=63))\\n \\n -(\\n ((basestat:mobility:>=18 basestat:resilience:>=13) or\\n (basestat:mobility:>=18 basestat:recovery:>=13) or\\n (basestat:mobility:>=18 basestat:discipline:>=13) or\\n (basestat:mobility:>=18 basestat:intellect:>=13) or\\n (basestat:mobility:>=18 basestat:strength:>=13) or\\n (basestat:resilience:>=18 basestat:mobility:>=13) or\\n (basestat:resilience:>=18 basestat:recovery:>=13) or\\n (basestat:resilience:>=18 basestat:discipline:>=13) or\\n (basestat:resilience:>=18 basestat:intellect:>=13) or\\n (basestat:resilience:>=18 basestat:strength:>=13) or\\n (basestat:recovery:>=18 basestat:mobility:>=13) or\\n (basestat:recovery:>=18 basestat:resilience:>=13) or\\n (basestat:recovery:>=18 basestat:discipline:>=13) or\\n (basestat:recovery:>=18 basestat:intellect:>=13) or\\n (basestat:recovery:>=18 basestat:strength:>=13) or\\n (basestat:discipline:>=18 basestat:mobility:>=13) or\\n (basestat:discipline:>=18 basestat:resilience:>=13) or\\n (basestat:discipline:>=18 basestat:recovery:>=13) or\\n (basestat:discipline:>=18 basestat:intellect:>=13) or\\n (basestat:discipline:>=18 basestat:strength:>=13) or\\n (basestat:intellect:>=18 basestat:mobility:>=13) or\\n (basestat:intellect:>=18 basestat:resilience:>=13) or\\n (basestat:intellect:>=18 basestat:recovery:>=13) or\\n (basestat:intellect:>=18 basestat:discipline:>=13) or\\n (basestat:intellect:>=18 basestat:strength:>=13) or\\n (basestat:strength:>=18 basestat:mobility:>=13) or\\n (basestat:strength:>=18 basestat:resilience:>=13) or\\n (basestat:strength:>=18 basestat:recovery:>=13) or\\n (basestat:strength:>=18 basestat:discipline:>=13) or\\n (basestat:strength:>=18 basestat:intellect:>=13))\\n )\\n \\n -(\\n ((basestat:mobility:>=13 basestat:resilience:>=13 basestat:recovery:>=13) or\\n (basestat:mobility:>=13 basestat:resilience:>=13 basestat:discipline:>=13) or\\n (basestat:mobility:>=13 basestat:resilience:>=13 basestat:intellect:>=13) or\\n (basestat:mobility:>=13 basestat:resilience:>=13 basestat:strength:>=13) or\\n (basestat:mobility:>=13 basestat:recovery:>=13 basestat:discipline:>=13) or\\n (basestat:mobility:>=13 basestat:recovery:>=13 basestat:intellect:>=13) or\\n (basestat:mobility:>=13 basestat:recovery:>=13 basestat:strength:>=13) or\\n (basestat:mobility:>=13 basestat:discipline:>=13 basestat:intellect:>=13) or\\n (basestat:mobility:>=13 basestat:discipline:>=13 basestat:strength:>=13) or\\n (basestat:mobility:>=13 basestat:intellect:>=13 basestat:strength:>=13) or\\n (basestat:resilience:>=13 basestat:recovery:>=13 basestat:discipline:>=13) or\\n (basestat:resilience:>=13 basestat:recovery:>=13 basestat:intellect:>=13) or\\n (basestat:resilience:>=13 basestat:recovery:>=13 basestat:strength:>=13) or\\n (basestat:resilience:>=13 basestat:discipline:>=13 basestat:intellect:>=13) or\\n (basestat:resilience:>=13 basestat:discipline:>=13 basestat:strength:>=13) or\\n (basestat:resilience:>=13 basestat:intellect:>=13 basestat:strength:>=13) or\\n (basestat:recovery:>=13 basestat:discipline:>=13 basestat:intellect:>=13) or\\n (basestat:recovery:>=13 basestat:discipline:>=13 basestat:strength:>=13) or\\n (basestat:recovery:>=13 basestat:intellect:>=13 basestat:strength:>=13) or\\n (basestat:discipline:>=13 basestat:intellect:>=13 basestat:strength:>=13))\\n )\\n \\n -(basestat:mobility:>=8 basestat:resilience:>=8 basestat:recovery:>=8 basestat:discipline:>=8 basestat:intellect:>=8 basestat:strength:>=8)\\n )\\n \\n or\\n (is:classitem ((is:dupelower -is:modded) or (is:sunset))) \\n or\\n (is:armor -powerlimit:>1060) \\n or\\n (is:armor is:blue)\\n )\\n -tag:keep -tag:archive -tag:favorite -tag:infuse -is:maxpower -power:>=1260 -is:inloadout -is:masterwork\\n )|: ast 1`] = ` { "length": 3660, "op": "and", "operands": [ { "length": 3546, "op": "or", "operands": [ { "length": 58, "op": "and", "operands": [ { "args": "weapon", "length": 9, "op": "filter", "startIndex": 15, "type": "is", }, { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 11, "op": "filter", "startIndex": 26, "type": "is", }, "startIndex": 25, }, { "length": 34, "op": "or", "operands": [ { "args": "1060", "length": 15, "op": "filter", "startIndex": 38, "type": "powerlimit", }, { "args": "junk", "length": 8, "op": "filter", "startIndex": 57, "type": "tag", }, { "args": "blue", "length": 7, "op": "filter", "startIndex": 69, "type": "is", }, ], "startIndex": 38, }, ], "startIndex": 14, }, { "length": 3361, "op": "and", "operands": [ { "args": "armor", "length": 8, "op": "filter", "startIndex": 96, "type": "is", }, { "length": 10, "op": "not", "operand": { "args": "exotic", "length": 9, "op": "filter", "startIndex": 106, "type": "is", }, "startIndex": 105, }, { "length": 13, "op": "not", "operand": { "args": "classitem", "length": 12, "op": "filter", "startIndex": 117, "type": "is", }, "startIndex": 116, }, { "length": 59, "op": "not", "operand": { "length": 58, "op": "and", "operands": [ { "args": "titan", "length": 8, "op": "filter", "startIndex": 140, "type": "is", }, { "length": 47, "op": "or", "operands": [ { "args": "recovery:>=18", "length": 22, "op": "filter", "startIndex": 150, "type": "basestat", }, { "args": "total:>=63", "length": 19, "op": "filter", "startIndex": 176, "type": "basestat", }, ], "startIndex": 149, }, ], "startIndex": 139, }, "startIndex": 138, }, { "length": 106, "op": "not", "operand": { "length": 105, "op": "and", "operands": [ { "args": "hunter", "length": 9, "op": "filter", "startIndex": 204, "type": "is", }, { "length": 93, "op": "or", "operands": [ { "length": 47, "op": "and", "operands": [ { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 216, "type": "basestat", }, { "args": "mobility:>=18", "length": 22, "op": "filter", "startIndex": 239, "type": "basestat", }, ], "startIndex": 215, }, { "args": "recovery:>15", "length": 21, "op": "filter", "startIndex": 266, "type": "basestat", }, { "args": "total:>=63", "length": 19, "op": "filter", "startIndex": 291, "type": "basestat", }, ], "startIndex": 214, }, ], "startIndex": 203, }, "startIndex": 202, }, { "length": 79, "op": "not", "operand": { "length": 78, "op": "and", "operands": [ { "args": "warlock", "length": 10, "op": "filter", "startIndex": 319, "type": "is", }, { "length": 65, "op": "or", "operands": [ { "length": 40, "op": "and", "operands": [ { "args": "recovery:>=18", "length": 22, "op": "filter", "startIndex": 332, "type": "basestat", }, { "args": ">=17", "length": 15, "op": "filter", "startIndex": 355, "type": "discipline", }, ], "startIndex": 331, }, { "args": "total:>=63", "length": 19, "op": "filter", "startIndex": 375, "type": "basestat", }, ], "startIndex": 330, }, ], "startIndex": 318, }, "startIndex": 317, }, { "length": 1483, "op": "not", "operand": { "length": 1482, "op": "or", "operands": [ { "length": 49, "op": "and", "operands": [ { "args": "mobility:>=18", "length": 22, "op": "filter", "startIndex": 413, "type": "basestat", }, { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 436, "type": "basestat", }, ], "startIndex": 412, }, { "length": 47, "op": "and", "operands": [ { "args": "mobility:>=18", "length": 22, "op": "filter", "startIndex": 470, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 493, "type": "basestat", }, ], "startIndex": 469, }, { "length": 49, "op": "and", "operands": [ { "args": "mobility:>=18", "length": 22, "op": "filter", "startIndex": 525, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 548, "type": "basestat", }, ], "startIndex": 524, }, { "length": 48, "op": "and", "operands": [ { "args": "mobility:>=18", "length": 22, "op": "filter", "startIndex": 582, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 605, "type": "basestat", }, ], "startIndex": 581, }, { "length": 47, "op": "and", "operands": [ { "args": "mobility:>=18", "length": 22, "op": "filter", "startIndex": 638, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 661, "type": "basestat", }, ], "startIndex": 637, }, { "length": 49, "op": "and", "operands": [ { "args": "resilience:>=18", "length": 24, "op": "filter", "startIndex": 693, "type": "basestat", }, { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 718, "type": "basestat", }, ], "startIndex": 692, }, { "length": 49, "op": "and", "operands": [ { "args": "resilience:>=18", "length": 24, "op": "filter", "startIndex": 750, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 775, "type": "basestat", }, ], "startIndex": 749, }, { "length": 51, "op": "and", "operands": [ { "args": "resilience:>=18", "length": 24, "op": "filter", "startIndex": 807, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 832, "type": "basestat", }, ], "startIndex": 806, }, { "length": 50, "op": "and", "operands": [ { "args": "resilience:>=18", "length": 24, "op": "filter", "startIndex": 866, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 891, "type": "basestat", }, ], "startIndex": 865, }, { "length": 49, "op": "and", "operands": [ { "args": "resilience:>=18", "length": 24, "op": "filter", "startIndex": 924, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 949, "type": "basestat", }, ], "startIndex": 923, }, { "length": 47, "op": "and", "operands": [ { "args": "recovery:>=18", "length": 22, "op": "filter", "startIndex": 981, "type": "basestat", }, { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 1004, "type": "basestat", }, ], "startIndex": 980, }, { "length": 49, "op": "and", "operands": [ { "args": "recovery:>=18", "length": 22, "op": "filter", "startIndex": 1036, "type": "basestat", }, { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 1059, "type": "basestat", }, ], "startIndex": 1035, }, { "length": 49, "op": "and", "operands": [ { "args": "recovery:>=18", "length": 22, "op": "filter", "startIndex": 1093, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 1116, "type": "basestat", }, ], "startIndex": 1092, }, { "length": 48, "op": "and", "operands": [ { "args": "recovery:>=18", "length": 22, "op": "filter", "startIndex": 1150, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 1173, "type": "basestat", }, ], "startIndex": 1149, }, { "length": 47, "op": "and", "operands": [ { "args": "recovery:>=18", "length": 22, "op": "filter", "startIndex": 1206, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 1229, "type": "basestat", }, ], "startIndex": 1205, }, { "length": 49, "op": "and", "operands": [ { "args": "discipline:>=18", "length": 24, "op": "filter", "startIndex": 1261, "type": "basestat", }, { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 1286, "type": "basestat", }, ], "startIndex": 1260, }, { "length": 51, "op": "and", "operands": [ { "args": "discipline:>=18", "length": 24, "op": "filter", "startIndex": 1318, "type": "basestat", }, { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 1343, "type": "basestat", }, ], "startIndex": 1317, }, { "length": 49, "op": "and", "operands": [ { "args": "discipline:>=18", "length": 24, "op": "filter", "startIndex": 1377, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 1402, "type": "basestat", }, ], "startIndex": 1376, }, { "length": 50, "op": "and", "operands": [ { "args": "discipline:>=18", "length": 24, "op": "filter", "startIndex": 1434, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 1459, "type": "basestat", }, ], "startIndex": 1433, }, { "length": 49, "op": "and", "operands": [ { "args": "discipline:>=18", "length": 24, "op": "filter", "startIndex": 1492, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 1517, "type": "basestat", }, ], "startIndex": 1491, }, { "length": 48, "op": "and", "operands": [ { "args": "intellect:>=18", "length": 23, "op": "filter", "startIndex": 1549, "type": "basestat", }, { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 1573, "type": "basestat", }, ], "startIndex": 1548, }, { "length": 50, "op": "and", "operands": [ { "args": "intellect:>=18", "length": 23, "op": "filter", "startIndex": 1605, "type": "basestat", }, { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 1629, "type": "basestat", }, ], "startIndex": 1604, }, { "length": 48, "op": "and", "operands": [ { "args": "intellect:>=18", "length": 23, "op": "filter", "startIndex": 1663, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 1687, "type": "basestat", }, ], "startIndex": 1662, }, { "length": 50, "op": "and", "operands": [ { "args": "intellect:>=18", "length": 23, "op": "filter", "startIndex": 1719, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 1743, "type": "basestat", }, ], "startIndex": 1718, }, { "length": 48, "op": "and", "operands": [ { "args": "intellect:>=18", "length": 23, "op": "filter", "startIndex": 1777, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 1801, "type": "basestat", }, ], "startIndex": 1776, }, { "length": 47, "op": "and", "operands": [ { "args": "strength:>=18", "length": 22, "op": "filter", "startIndex": 1833, "type": "basestat", }, { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 1856, "type": "basestat", }, ], "startIndex": 1832, }, { "length": 49, "op": "and", "operands": [ { "args": "strength:>=18", "length": 22, "op": "filter", "startIndex": 1888, "type": "basestat", }, { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 1911, "type": "basestat", }, ], "startIndex": 1887, }, { "length": 47, "op": "and", "operands": [ { "args": "strength:>=18", "length": 22, "op": "filter", "startIndex": 1945, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 1968, "type": "basestat", }, ], "startIndex": 1944, }, { "length": 49, "op": "and", "operands": [ { "args": "strength:>=18", "length": 22, "op": "filter", "startIndex": 2000, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 2023, "type": "basestat", }, ], "startIndex": 1999, }, { "length": 48, "op": "and", "operands": [ { "args": "strength:>=18", "length": 22, "op": "filter", "startIndex": 2057, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 2080, "type": "basestat", }, ], "startIndex": 2056, }, ], "startIndex": 405, }, "startIndex": 404, }, { "length": 1453, "op": "not", "operand": { "length": 1452, "op": "or", "operands": [ { "length": 71, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2128, "type": "basestat", }, { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 2151, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 2176, "type": "basestat", }, ], "startIndex": 2127, }, { "length": 73, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2208, "type": "basestat", }, { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 2231, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 2256, "type": "basestat", }, ], "startIndex": 2207, }, { "length": 72, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2290, "type": "basestat", }, { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 2313, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 2338, "type": "basestat", }, ], "startIndex": 2289, }, { "length": 71, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2371, "type": "basestat", }, { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 2394, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 2419, "type": "basestat", }, ], "startIndex": 2370, }, { "length": 71, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2451, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 2474, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 2497, "type": "basestat", }, ], "startIndex": 2450, }, { "length": 70, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2531, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 2554, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 2577, "type": "basestat", }, ], "startIndex": 2530, }, { "length": 69, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2610, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 2633, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 2656, "type": "basestat", }, ], "startIndex": 2609, }, { "length": 72, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2688, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 2711, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 2736, "type": "basestat", }, ], "startIndex": 2687, }, { "length": 71, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2769, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 2792, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 2817, "type": "basestat", }, ], "startIndex": 2768, }, { "length": 70, "op": "and", "operands": [ { "args": "mobility:>=13", "length": 22, "op": "filter", "startIndex": 2849, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 2872, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 2896, "type": "basestat", }, ], "startIndex": 2848, }, { "length": 73, "op": "and", "operands": [ { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 2928, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 2953, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 2976, "type": "basestat", }, ], "startIndex": 2927, }, { "length": 72, "op": "and", "operands": [ { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 3010, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 3035, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 3058, "type": "basestat", }, ], "startIndex": 3009, }, { "length": 71, "op": "and", "operands": [ { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 3091, "type": "basestat", }, { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 3116, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 3139, "type": "basestat", }, ], "startIndex": 3090, }, { "length": 74, "op": "and", "operands": [ { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 3171, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 3196, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 3221, "type": "basestat", }, ], "startIndex": 3170, }, { "length": 73, "op": "and", "operands": [ { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 3254, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 3279, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 3304, "type": "basestat", }, ], "startIndex": 3253, }, { "length": 72, "op": "and", "operands": [ { "args": "resilience:>=13", "length": 24, "op": "filter", "startIndex": 3336, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 3361, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 3385, "type": "basestat", }, ], "startIndex": 3335, }, { "length": 72, "op": "and", "operands": [ { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 3417, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 3440, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 3465, "type": "basestat", }, ], "startIndex": 3416, }, { "length": 71, "op": "and", "operands": [ { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 3498, "type": "basestat", }, { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 3521, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 3546, "type": "basestat", }, ], "startIndex": 3497, }, { "length": 70, "op": "and", "operands": [ { "args": "recovery:>=13", "length": 22, "op": "filter", "startIndex": 3578, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 3601, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 3625, "type": "basestat", }, ], "startIndex": 3577, }, { "length": 72, "op": "and", "operands": [ { "args": "discipline:>=13", "length": 24, "op": "filter", "startIndex": 3657, "type": "basestat", }, { "args": "intellect:>=13", "length": 23, "op": "filter", "startIndex": 3682, "type": "basestat", }, { "args": "strength:>=13", "length": 22, "op": "filter", "startIndex": 3706, "type": "basestat", }, ], "startIndex": 3656, }, ], "startIndex": 2120, }, "startIndex": 2119, }, { "length": 135, "op": "not", "operand": { "length": 134, "op": "and", "operands": [ { "args": "mobility:>=8", "length": 21, "op": "filter", "startIndex": 3746, "type": "basestat", }, { "args": "resilience:>=8", "length": 23, "op": "filter", "startIndex": 3768, "type": "basestat", }, { "args": "recovery:>=8", "length": 21, "op": "filter", "startIndex": 3792, "type": "basestat", }, { "args": "discipline:>=8", "length": 23, "op": "filter", "startIndex": 3814, "type": "basestat", }, { "args": "intellect:>=8", "length": 22, "op": "filter", "startIndex": 3838, "type": "basestat", }, { "args": "strength:>=8", "length": 21, "op": "filter", "startIndex": 3861, "type": "basestat", }, ], "startIndex": 3745, }, "startIndex": 3744, }, ], "startIndex": 89, }, { "length": 57, "op": "and", "operands": [ { "args": "classitem", "length": 12, "op": "filter", "startIndex": 3905, "type": "is", }, { "length": 42, "op": "or", "operands": [ { "length": 25, "op": "and", "operands": [ { "args": "dupelower", "length": 12, "op": "filter", "startIndex": 3920, "type": "is", }, { "length": 10, "op": "not", "operand": { "args": "modded", "length": 9, "op": "filter", "startIndex": 3934, "type": "is", }, "startIndex": 3933, }, ], "startIndex": 3919, }, { "args": "sunset", "length": 11, "op": "filter", "startIndex": 3948, "type": "is", }, ], "startIndex": 3918, }, ], "startIndex": 3904, }, { "length": 28, "op": "and", "operands": [ { "args": "armor", "length": 8, "op": "filter", "startIndex": 3975, "type": "is", }, { "length": 17, "op": "not", "operand": { "args": ">1060", "length": 16, "op": "filter", "startIndex": 3985, "type": "powerlimit", }, "startIndex": 3984, }, ], "startIndex": 3974, }, { "length": 18, "op": "and", "operands": [ { "args": "armor", "length": 8, "op": "filter", "startIndex": 4016, "type": "is", }, { "args": "blue", "length": 7, "op": "filter", "startIndex": 4025, "type": "is", }, ], "startIndex": 4015, }, ], "startIndex": 8, }, { "length": 9, "op": "not", "operand": { "args": "keep", "length": 8, "op": "filter", "startIndex": 4045, "type": "tag", }, "startIndex": 4044, }, { "length": 12, "op": "not", "operand": { "args": "archive", "length": 11, "op": "filter", "startIndex": 4055, "type": "tag", }, "startIndex": 4054, }, { "length": 13, "op": "not", "operand": { "args": "favorite", "length": 12, "op": "filter", "startIndex": 4068, "type": "tag", }, "startIndex": 4067, }, { "length": 11, "op": "not", "operand": { "args": "infuse", "length": 10, "op": "filter", "startIndex": 4082, "type": "tag", }, "startIndex": 4081, }, { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 11, "op": "filter", "startIndex": 4094, "type": "is", }, "startIndex": 4093, }, { "length": 13, "op": "not", "operand": { "args": ">=1260", "length": 12, "op": "filter", "startIndex": 4107, "type": "power", }, "startIndex": 4106, }, { "length": 13, "op": "not", "operand": { "args": "inloadout", "length": 12, "op": "filter", "startIndex": 4121, "type": "is", }, "startIndex": 4120, }, { "length": 14, "op": "not", "operand": { "args": "masterwork", "length": 13, "op": "filter", "startIndex": 4135, "type": "is", }, "startIndex": 4134, }, ], "startIndex": 2, } `; exports[`parse | (\\n (\\n (is:weapon -is:maxpower powerlimit:1060 or tag:junk or is:blue)\\n or\\n (\\n (is:armor -is:exotic -is:classitem)\\n \\n -(is:titan (basestat:recovery:>=18 or basestat:total:>=63))\\n -(is:hunter ((basestat:recovery:>=13 basestat:mobility:>=18) or basestat:recovery:>15 or basestat:total:>=63))\\n -(is:warlock ((basestat:recovery:>=18 discipline:>=17) or basestat:total:>=63))\\n \\n -(\\n ((basestat:mobility:>=18 basestat:resilience:>=13) or\\n (basestat:mobility:>=18 basestat:recovery:>=13) or\\n (basestat:mobility:>=18 basestat:discipline:>=13) or\\n (basestat:mobility:>=18 basestat:intellect:>=13) or\\n (basestat:mobility:>=18 basestat:strength:>=13) or\\n (basestat:resilience:>=18 basestat:mobility:>=13) or\\n (basestat:resilience:>=18 basestat:recovery:>=13) or\\n (basestat:resilience:>=18 basestat:discipline:>=13) or\\n (basestat:resilience:>=18 basestat:intellect:>=13) or\\n (basestat:resilience:>=18 basestat:strength:>=13) or\\n (basestat:recovery:>=18 basestat:mobility:>=13) or\\n (basestat:recovery:>=18 basestat:resilience:>=13) or\\n (basestat:recovery:>=18 basestat:discipline:>=13) or\\n (basestat:recovery:>=18 basestat:intellect:>=13) or\\n (basestat:recovery:>=18 basestat:strength:>=13) or\\n (basestat:discipline:>=18 basestat:mobility:>=13) or\\n (basestat:discipline:>=18 basestat:resilience:>=13) or\\n (basestat:discipline:>=18 basestat:recovery:>=13) or\\n (basestat:discipline:>=18 basestat:intellect:>=13) or\\n (basestat:discipline:>=18 basestat:strength:>=13) or\\n (basestat:intellect:>=18 basestat:mobility:>=13) or\\n (basestat:intellect:>=18 basestat:resilience:>=13) or\\n (basestat:intellect:>=18 basestat:recovery:>=13) or\\n (basestat:intellect:>=18 basestat:discipline:>=13) or\\n (basestat:intellect:>=18 basestat:strength:>=13) or\\n (basestat:strength:>=18 basestat:mobility:>=13) or\\n (basestat:strength:>=18 basestat:resilience:>=13) or\\n (basestat:strength:>=18 basestat:recovery:>=13) or\\n (basestat:strength:>=18 basestat:discipline:>=13) or\\n (basestat:strength:>=18 basestat:intellect:>=13))\\n )\\n \\n -(\\n ((basestat:mobility:>=13 basestat:resilience:>=13 basestat:recovery:>=13) or\\n (basestat:mobility:>=13 basestat:resilience:>=13 basestat:discipline:>=13) or\\n (basestat:mobility:>=13 basestat:resilience:>=13 basestat:intellect:>=13) or\\n (basestat:mobility:>=13 basestat:resilience:>=13 basestat:strength:>=13) or\\n (basestat:mobility:>=13 basestat:recovery:>=13 basestat:discipline:>=13) or\\n (basestat:mobility:>=13 basestat:recovery:>=13 basestat:intellect:>=13) or\\n (basestat:mobility:>=13 basestat:recovery:>=13 basestat:strength:>=13) or\\n (basestat:mobility:>=13 basestat:discipline:>=13 basestat:intellect:>=13) or\\n (basestat:mobility:>=13 basestat:discipline:>=13 basestat:strength:>=13) or\\n (basestat:mobility:>=13 basestat:intellect:>=13 basestat:strength:>=13) or\\n (basestat:resilience:>=13 basestat:recovery:>=13 basestat:discipline:>=13) or\\n (basestat:resilience:>=13 basestat:recovery:>=13 basestat:intellect:>=13) or\\n (basestat:resilience:>=13 basestat:recovery:>=13 basestat:strength:>=13) or\\n (basestat:resilience:>=13 basestat:discipline:>=13 basestat:intellect:>=13) or\\n (basestat:resilience:>=13 basestat:discipline:>=13 basestat:strength:>=13) or\\n (basestat:resilience:>=13 basestat:intellect:>=13 basestat:strength:>=13) or\\n (basestat:recovery:>=13 basestat:discipline:>=13 basestat:intellect:>=13) or\\n (basestat:recovery:>=13 basestat:discipline:>=13 basestat:strength:>=13) or\\n (basestat:recovery:>=13 basestat:intellect:>=13 basestat:strength:>=13) or\\n (basestat:discipline:>=13 basestat:intellect:>=13 basestat:strength:>=13))\\n )\\n \\n -(basestat:mobility:>=8 basestat:resilience:>=8 basestat:recovery:>=8 basestat:discipline:>=8 basestat:intellect:>=8 basestat:strength:>=8)\\n )\\n \\n or\\n (is:classitem ((is:dupelower -is:modded) or (is:sunset))) \\n or\\n (is:armor -powerlimit:>1060) \\n or\\n (is:armor is:blue)\\n )\\n -tag:keep -tag:archive -tag:favorite -tag:infuse -is:maxpower -power:>=1260 -is:inloadout -is:masterwork\\n )|: lexer 1`] = ` [ [ "(", ], [ "(", ], [ "(", ], [ "filter", "is", "weapon", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "maxpower", ], [ "implicit_and", ], [ "filter", "powerlimit", "1060", ], [ "or", ], [ "filter", "tag", "junk", ], [ "or", ], [ "filter", "is", "blue", ], [ ")", ], [ "or", ], [ "(", ], [ "(", ], [ "filter", "is", "armor", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "exotic", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "classitem", ], [ ")", ], [ "implicit_and", ], [ "not", ], [ "(", ], [ "filter", "is", "titan", ], [ "implicit_and", ], [ "(", ], [ "filter", "basestat", "recovery:>=18", ], [ "or", ], [ "filter", "basestat", "total:>=63", ], [ ")", ], [ ")", ], [ "implicit_and", ], [ "not", ], [ "(", ], [ "filter", "is", "hunter", ], [ "implicit_and", ], [ "(", ], [ "(", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "mobility:>=18", ], [ ")", ], [ "or", ], [ "filter", "basestat", "recovery:>15", ], [ "or", ], [ "filter", "basestat", "total:>=63", ], [ ")", ], [ ")", ], [ "implicit_and", ], [ "not", ], [ "(", ], [ "filter", "is", "warlock", ], [ "implicit_and", ], [ "(", ], [ "(", ], [ "filter", "basestat", "recovery:>=18", ], [ "implicit_and", ], [ "filter", "discipline", ">=17", ], [ ")", ], [ "or", ], [ "filter", "basestat", "total:>=63", ], [ ")", ], [ ")", ], [ "implicit_and", ], [ "not", ], [ "(", ], [ "(", ], [ "(", ], [ "filter", "basestat", "mobility:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "mobility:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "recovery:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "mobility:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "recovery:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "recovery:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "recovery:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "recovery:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "discipline:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "mobility:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "discipline:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "discipline:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "discipline:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "discipline:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "intellect:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "mobility:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "intellect:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "intellect:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "intellect:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "intellect:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "strength:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "mobility:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "strength:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "strength:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "strength:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "strength:>=18", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ ")", ], [ ")", ], [ "implicit_and", ], [ "not", ], [ "(", ], [ "(", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "mobility:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "resilience:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "recovery:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "basestat", "discipline:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=13", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=13", ], [ ")", ], [ ")", ], [ ")", ], [ "implicit_and", ], [ "not", ], [ "(", ], [ "filter", "basestat", "mobility:>=8", ], [ "implicit_and", ], [ "filter", "basestat", "resilience:>=8", ], [ "implicit_and", ], [ "filter", "basestat", "recovery:>=8", ], [ "implicit_and", ], [ "filter", "basestat", "discipline:>=8", ], [ "implicit_and", ], [ "filter", "basestat", "intellect:>=8", ], [ "implicit_and", ], [ "filter", "basestat", "strength:>=8", ], [ ")", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "is", "classitem", ], [ "implicit_and", ], [ "(", ], [ "(", ], [ "filter", "is", "dupelower", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "modded", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "is", "sunset", ], [ ")", ], [ ")", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "is", "armor", ], [ "implicit_and", ], [ "not", ], [ "filter", "powerlimit", ">1060", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "is", "armor", ], [ "implicit_and", ], [ "filter", "is", "blue", ], [ ")", ], [ ")", ], [ "implicit_and", ], [ "not", ], [ "filter", "tag", "keep", ], [ "implicit_and", ], [ "not", ], [ "filter", "tag", "archive", ], [ "implicit_and", ], [ "not", ], [ "filter", "tag", "favorite", ], [ "implicit_and", ], [ "not", ], [ "filter", "tag", "infuse", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "maxpower", ], [ "implicit_and", ], [ "not", ], [ "filter", "power", ">=1260", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "inloadout", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "masterwork", ], [ ")", ], ] `; exports[`parse | /* My cool search */\\n is:armor|: ast 1`] = ` { "args": "armor", "comment": "my cool search", "length": 8, "op": "filter", "startIndex": 24, "type": "is", } `; exports[`parse | /* My cool search */\\n is:armor|: lexer 1`] = ` [ [ "comment", "my cool search", ], [ "filter", "is", "armor", ], ] `; exports[`parse |"grenade launcher reserves"|: ast 1`] = ` { "args": "grenade launcher reserves", "length": 27, "op": "filter", "startIndex": 0, "type": "keyword", } `; exports[`parse |"grenade launcher reserves"|: lexer 1`] = ` [ [ "filter", "keyword", "grenade launcher reserves", ], ] `; exports[`parse |"수집가"|: ast 1`] = ` { "args": "수집가", "length": 5, "op": "filter", "startIndex": 0, "type": "keyword", } `; exports[`parse |"수집가"|: lexer 1`] = ` [ [ "filter", "keyword", "수집가", ], ] `; exports[`parse |( power:>1000 and -modslot:arrival ) |: ast 1`] = ` { "length": 36, "op": "and", "operands": [ { "args": ">1000", "length": 11, "op": "filter", "startIndex": 2, "type": "power", }, { "length": 16, "op": "not", "operand": { "args": "arrival", "length": 15, "op": "filter", "startIndex": 19, "type": "modslot", }, "startIndex": 18, }, ], "startIndex": 0, } `; exports[`parse |( power:>1000 and -modslot:arrival ) |: lexer 1`] = ` [ [ "(", ], [ "filter", "power", ">1000", ], [ "and", ], [ "not", ], [ "filter", "modslot", "arrival", ], [ ")", ], ] `; exports[`parse |(("test" or "test") and "test")|: ast 1`] = ` { "length": 31, "op": "and", "operands": [ { "length": 18, "op": "or", "operands": [ { "args": "test", "length": 6, "op": "filter", "startIndex": 2, "type": "keyword", }, { "args": "test", "length": 6, "op": "filter", "startIndex": 12, "type": "keyword", }, ], "startIndex": 1, }, { "args": "test", "length": 6, "op": "filter", "startIndex": 24, "type": "keyword", }, ], "startIndex": 0, } `; exports[`parse |(("test" or "test") and "test")|: lexer 1`] = ` [ [ "(", ], [ "(", ], [ "filter", "keyword", "test", ], [ "or", ], [ "filter", "keyword", "test", ], [ ")", ], [ "and", ], [ "filter", "keyword", "test", ], [ ")", ], ] `; exports[`parse |(is:hunter power:>=540) or (is:warlock power:>=560)|: ast 1`] = ` { "length": 51, "op": "or", "operands": [ { "length": 23, "op": "and", "operands": [ { "args": "hunter", "length": 9, "op": "filter", "startIndex": 1, "type": "is", }, { "args": ">=540", "length": 11, "op": "filter", "startIndex": 11, "type": "power", }, ], "startIndex": 0, }, { "length": 24, "op": "and", "operands": [ { "args": "warlock", "length": 10, "op": "filter", "startIndex": 28, "type": "is", }, { "args": ">=560", "length": 11, "op": "filter", "startIndex": 39, "type": "power", }, ], "startIndex": 27, }, ], "startIndex": 0, } `; exports[`parse |(is:hunter power:>=540) or (is:warlock power:>=560)|: lexer 1`] = ` [ [ "(", ], [ "filter", "is", "hunter", ], [ "implicit_and", ], [ "filter", "power", ">=540", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "is", "warlock", ], [ "implicit_and", ], [ "filter", "power", ">=560", ], [ ")", ], ] `; exports[`parse |(is:weapon and is:sniperrifle) or not (is:armor and modslot:arrival)|: ast 1`] = ` { "length": 68, "op": "or", "operands": [ { "length": 30, "op": "and", "operands": [ { "args": "weapon", "length": 9, "op": "filter", "startIndex": 1, "type": "is", }, { "args": "sniperrifle", "length": 14, "op": "filter", "startIndex": 15, "type": "is", }, ], "startIndex": 0, }, { "length": 34, "op": "not", "operand": { "length": 30, "op": "and", "operands": [ { "args": "armor", "length": 8, "op": "filter", "startIndex": 39, "type": "is", }, { "args": "arrival", "length": 15, "op": "filter", "startIndex": 52, "type": "modslot", }, ], "startIndex": 38, }, "startIndex": 34, }, ], "startIndex": 0, } `; exports[`parse |(is:weapon and is:sniperrifle) or not (is:armor and modslot:arrival)|: lexer 1`] = ` [ [ "(", ], [ "filter", "is", "weapon", ], [ "and", ], [ "filter", "is", "sniperrifle", ], [ ")", ], [ "or", ], [ "not", ], [ "(", ], [ "filter", "is", "armor", ], [ "and", ], [ "filter", "modslot", "arrival", ], [ ")", ], ] `; exports[`parse |(is:weapon is:sniperrifle) or (is:armor modslot:arrival)|: ast 1`] = ` { "length": 56, "op": "or", "operands": [ { "length": 26, "op": "and", "operands": [ { "args": "weapon", "length": 9, "op": "filter", "startIndex": 1, "type": "is", }, { "args": "sniperrifle", "length": 14, "op": "filter", "startIndex": 11, "type": "is", }, ], "startIndex": 0, }, { "length": 26, "op": "and", "operands": [ { "args": "armor", "length": 8, "op": "filter", "startIndex": 31, "type": "is", }, { "args": "arrival", "length": 15, "op": "filter", "startIndex": 40, "type": "modslot", }, ], "startIndex": 30, }, ], "startIndex": 0, } `; exports[`parse |(is:weapon is:sniperrifle) or (is:armor modslot:arrival)|: lexer 1`] = ` [ [ "(", ], [ "filter", "is", "weapon", ], [ "implicit_and", ], [ "filter", "is", "sniperrifle", ], [ ")", ], [ "or", ], [ "(", ], [ "filter", "is", "armor", ], [ "implicit_and", ], [ "filter", "modslot", "arrival", ], [ ")", ], ] `; exports[`parse |/* My cool search */ (/* armor */ is:armor and is:blue) or (/*weapons*/ is:weapon and perkname:"Kill Clip")|: ast 1`] = ` { "comment": "my cool search", "length": 86, "op": "or", "operands": [ { "length": 34, "op": "and", "operands": [ { "args": "armor", "length": 8, "op": "filter", "startIndex": 34, "type": "is", }, { "args": "blue", "length": 7, "op": "filter", "startIndex": 47, "type": "is", }, ], "startIndex": 21, }, { "comment": "weapons", "length": 48, "op": "and", "operands": [ { "args": "weapon", "length": 9, "op": "filter", "startIndex": 72, "type": "is", }, { "args": "kill clip", "length": 20, "op": "filter", "startIndex": 86, "type": "perkname", }, ], "startIndex": 59, }, ], "startIndex": 21, } `; exports[`parse |/* My cool search */ (/* armor */ is:armor and is:blue) or (/*weapons*/ is:weapon and perkname:"Kill Clip")|: lexer 1`] = ` [ [ "comment", "my cool search", ], [ "(", ], [ "comment", "armor", ], [ "filter", "is", "armor", ], [ "and", ], [ "filter", "is", "blue", ], [ ")", ], [ "or", ], [ "(", ], [ "comment", "weapons", ], [ "filter", "is", "weapon", ], [ "and", ], [ "filter", "perkname", "kill clip", ], [ ")", ], ] `; exports[`parse |/* My cool search */ is:armor|: ast 1`] = ` { "args": "armor", "comment": "my cool search", "length": 8, "op": "filter", "startIndex": 21, "type": "is", } `; exports[`parse |/* My cool search */ is:armor|: lexer 1`] = ` [ [ "comment", "my cool search", ], [ "filter", "is", "armor", ], ] `; exports[`parse |- is:exotic - (power:>1000)|: ast 1`] = ` { "length": 27, "op": "and", "operands": [ { "length": 11, "op": "not", "operand": { "args": "exotic", "length": 9, "op": "filter", "startIndex": 2, "type": "is", }, "startIndex": 0, }, { "length": 15, "op": "not", "operand": { "args": ">1000", "length": 13, "op": "filter", "startIndex": 14, "type": "power", }, "startIndex": 12, }, ], "startIndex": 0, } `; exports[`parse |- is:exotic - (power:>1000)|: lexer 1`] = ` [ [ "not", ], [ "filter", "is", "exotic", ], [ "implicit_and", ], [ "not", ], [ "(", ], [ "filter", "power", ">1000", ], [ ")", ], ] `; exports[`parse |-(power:>1000 and -modslot:arrival)|: ast 1`] = ` { "length": 35, "op": "not", "operand": { "length": 34, "op": "and", "operands": [ { "args": ">1000", "length": 11, "op": "filter", "startIndex": 2, "type": "power", }, { "length": 16, "op": "not", "operand": { "args": "arrival", "length": 15, "op": "filter", "startIndex": 19, "type": "modslot", }, "startIndex": 18, }, ], "startIndex": 1, }, "startIndex": 0, } `; exports[`parse |-(power:>1000 and -modslot:arrival)|: lexer 1`] = ` [ [ "not", ], [ "(", ], [ "filter", "power", ">1000", ], [ "and", ], [ "not", ], [ "filter", "modslot", "arrival", ], [ ")", ], ] `; exports[`parse |-is:equipped is:haspower is:incurrentchar|: ast 1`] = ` { "length": 40, "op": "and", "operands": [ { "length": 12, "op": "not", "operand": { "args": "equipped", "length": 11, "op": "filter", "startIndex": 1, "type": "is", }, "startIndex": 0, }, { "args": "haspower", "length": 11, "op": "filter", "startIndex": 13, "type": "is", }, { "args": "incurrentchar", "length": 16, "op": "filter", "startIndex": 25, "type": "is", }, ], "startIndex": 0, } `; exports[`parse |-is:equipped is:haspower is:incurrentchar|: lexer 1`] = ` [ [ "not", ], [ "filter", "is", "equipped", ], [ "implicit_and", ], [ "filter", "is", "haspower", ], [ "implicit_and", ], [ "filter", "is", "incurrentchar", ], ] `; exports[`parse |-is:exotic -is:locked -is:maxpower -is:tagged stat:total:<55|: ast 1`] = ` { "length": 57, "op": "and", "operands": [ { "length": 10, "op": "not", "operand": { "args": "exotic", "length": 9, "op": "filter", "startIndex": 1, "type": "is", }, "startIndex": 0, }, { "length": 10, "op": "not", "operand": { "args": "locked", "length": 9, "op": "filter", "startIndex": 12, "type": "is", }, "startIndex": 11, }, { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 11, "op": "filter", "startIndex": 23, "type": "is", }, "startIndex": 22, }, { "length": 10, "op": "not", "operand": { "args": "tagged", "length": 9, "op": "filter", "startIndex": 36, "type": "is", }, "startIndex": 35, }, { "args": "total:<55", "length": 14, "op": "filter", "startIndex": 46, "type": "stat", }, ], "startIndex": 0, } `; exports[`parse |-is:exotic -is:locked -is:maxpower -is:tagged stat:total:<55|: lexer 1`] = ` [ [ "not", ], [ "filter", "is", "exotic", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "locked", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "maxpower", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "tagged", ], [ "implicit_and", ], [ "filter", "stat", "total:<55", ], ] `; exports[`parse |-source:garden -source:lastwish sunsetsafter:arrival|: ast 1`] = ` { "length": 51, "op": "and", "operands": [ { "length": 14, "op": "not", "operand": { "args": "garden", "length": 13, "op": "filter", "startIndex": 1, "type": "source", }, "startIndex": 0, }, { "length": 16, "op": "not", "operand": { "args": "lastwish", "length": 15, "op": "filter", "startIndex": 16, "type": "source", }, "startIndex": 15, }, { "args": "arrival", "length": 20, "op": "filter", "startIndex": 32, "type": "sunsetsafter", }, ], "startIndex": 0, } `; exports[`parse |-source:garden -source:lastwish sunsetsafter:arrival|: lexer 1`] = ` [ [ "not", ], [ "filter", "source", "garden", ], [ "implicit_and", ], [ "not", ], [ "filter", "source", "lastwish", ], [ "implicit_and", ], [ "filter", "sunsetsafter", "arrival", ], ] `; exports[`parse |-witherhoard|: ast 1`] = ` { "length": 12, "op": "not", "operand": { "args": "witherhoard", "length": 11, "op": "filter", "startIndex": 1, "type": "keyword", }, "startIndex": 0, } `; exports[`parse |-witherhoard|: lexer 1`] = ` [ [ "not", ], [ "filter", "keyword", "witherhoard", ], ] `; exports[`parse |cluster tracking|: ast 1`] = ` { "length": 16, "op": "and", "operands": [ { "args": "cluster", "length": 7, "op": "filter", "startIndex": 0, "type": "keyword", }, { "args": "tracking", "length": 8, "op": "filter", "startIndex": 8, "type": "keyword", }, ], "startIndex": 0, } `; exports[`parse |cluster tracking|: lexer 1`] = ` [ [ "filter", "keyword", "cluster", ], [ "implicit_and", ], [ "filter", "keyword", "tracking", ], ] `; exports[`parse |gnawing hunger|: ast 1`] = ` { "length": 14, "op": "and", "operands": [ { "args": "gnawing", "length": 7, "op": "filter", "startIndex": 0, "type": "keyword", }, { "args": "hunger", "length": 6, "op": "filter", "startIndex": 8, "type": "keyword", }, ], "startIndex": 0, } `; exports[`parse |gnawing hunger|: lexer 1`] = ` [ [ "filter", "keyword", "gnawing", ], [ "implicit_and", ], [ "filter", "keyword", "hunger", ], ] `; exports[`parse |is:armor2.0|: ast 1`] = ` { "args": "armor2.0", "length": 11, "op": "filter", "startIndex": 0, "type": "is", } `; exports[`parse |is:armor2.0|: lexer 1`] = ` [ [ "filter", "is", "armor2.0", ], ] `; exports[`parse |is:blue is:haspower -is:maxpower|: ast 1`] = ` { "length": 31, "op": "and", "operands": [ { "args": "blue", "length": 7, "op": "filter", "startIndex": 0, "type": "is", }, { "args": "haspower", "length": 11, "op": "filter", "startIndex": 8, "type": "is", }, { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 11, "op": "filter", "startIndex": 21, "type": "is", }, "startIndex": 20, }, ], "startIndex": 0, } `; exports[`parse |is:blue is:haspower -is:maxpower|: lexer 1`] = ` [ [ "filter", "is", "blue", ], [ "implicit_and", ], [ "filter", "is", "haspower", ], [ "implicit_and", ], [ "not", ], [ "filter", "is", "maxpower", ], ] `; exports[`parse |is:blue is:haspower not:maxpower|: ast 1`] = ` { "length": 31, "op": "and", "operands": [ { "args": "blue", "length": 7, "op": "filter", "startIndex": 0, "type": "is", }, { "args": "haspower", "length": 11, "op": "filter", "startIndex": 8, "type": "is", }, { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 12, "op": "filter", "startIndex": 20, "type": "is", }, "startIndex": 20, }, ], "startIndex": 0, } `; exports[`parse |is:blue is:haspower not:maxpower|: lexer 1`] = ` [ [ "filter", "is", "blue", ], [ "implicit_and", ], [ "filter", "is", "haspower", ], [ "implicit_and", ], [ "filter", "not", "maxpower", ], ] `; exports[`parse |is:blue is:weapon or is:armor not:maxpower|: ast 1`] = ` { "length": 41, "op": "and", "operands": [ { "args": "blue", "length": 7, "op": "filter", "startIndex": 0, "type": "is", }, { "length": 21, "op": "or", "operands": [ { "args": "weapon", "length": 9, "op": "filter", "startIndex": 8, "type": "is", }, { "args": "armor", "length": 8, "op": "filter", "startIndex": 21, "type": "is", }, ], "startIndex": 8, }, { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 12, "op": "filter", "startIndex": 30, "type": "is", }, "startIndex": 30, }, ], "startIndex": 0, } `; exports[`parse |is:blue is:weapon or is:armor not:maxpower|: lexer 1`] = ` [ [ "filter", "is", "blue", ], [ "implicit_and", ], [ "filter", "is", "weapon", ], [ "or", ], [ "filter", "is", "armor", ], [ "implicit_and", ], [ "filter", "not", "maxpower", ], ] `; exports[`parse |is:rocketlauncher (perk:"cluster" or perk:"tracking module")|: ast 1`] = ` { "length": 60, "op": "and", "operands": [ { "args": "rocketlauncher", "length": 17, "op": "filter", "startIndex": 0, "type": "is", }, { "length": 42, "op": "or", "operands": [ { "args": "cluster", "length": 14, "op": "filter", "startIndex": 19, "type": "perk", }, { "args": "tracking module", "length": 22, "op": "filter", "startIndex": 37, "type": "perk", }, ], "startIndex": 18, }, ], "startIndex": 0, } `; exports[`parse |is:rocketlauncher (perk:"cluster" or perk:"tracking module")|: lexer 1`] = ` [ [ "filter", "is", "rocketlauncher", ], [ "implicit_and", ], [ "(", ], [ "filter", "perk", "cluster", ], [ "or", ], [ "filter", "perk", "tracking module", ], [ ")", ], ] `; exports[`parse |is:rocketlauncher -"cluster" -"tracking module"|: ast 1`] = ` { "length": 46, "op": "and", "operands": [ { "args": "rocketlauncher", "length": 17, "op": "filter", "startIndex": 0, "type": "is", }, { "length": 10, "op": "not", "operand": { "args": "cluster", "length": 9, "op": "filter", "startIndex": 19, "type": "keyword", }, "startIndex": 18, }, { "length": 18, "op": "not", "operand": { "args": "tracking module", "length": 17, "op": "filter", "startIndex": 30, "type": "keyword", }, "startIndex": 29, }, ], "startIndex": 0, } `; exports[`parse |is:rocketlauncher -"cluster" -"tracking module"|: lexer 1`] = ` [ [ "filter", "is", "rocketlauncher", ], [ "implicit_and", ], [ "not", ], [ "filter", "keyword", "cluster", ], [ "implicit_and", ], [ "not", ], [ "filter", "keyword", "tracking module", ], ] `; exports[`parse |is:rocketlauncher -"cluster" -'tracking module'|: ast 1`] = ` { "length": 46, "op": "and", "operands": [ { "args": "rocketlauncher", "length": 17, "op": "filter", "startIndex": 0, "type": "is", }, { "length": 10, "op": "not", "operand": { "args": "cluster", "length": 9, "op": "filter", "startIndex": 19, "type": "keyword", }, "startIndex": 18, }, { "length": 18, "op": "not", "operand": { "args": "tracking module", "length": 17, "op": "filter", "startIndex": 30, "type": "keyword", }, "startIndex": 29, }, ], "startIndex": 0, } `; exports[`parse |is:rocketlauncher -"cluster" -'tracking module'|: lexer 1`] = ` [ [ "filter", "is", "rocketlauncher", ], [ "implicit_and", ], [ "not", ], [ "filter", "keyword", "cluster", ], [ "implicit_and", ], [ "not", ], [ "filter", "keyword", "tracking module", ], ] `; exports[`parse |is:weapon (is:sniperrifle or (is:armor and modslot:arrival))|: ast 1`] = ` { "length": 60, "op": "and", "operands": [ { "args": "weapon", "length": 9, "op": "filter", "startIndex": 0, "type": "is", }, { "length": 50, "op": "or", "operands": [ { "args": "sniperrifle", "length": 14, "op": "filter", "startIndex": 11, "type": "is", }, { "length": 30, "op": "and", "operands": [ { "args": "armor", "length": 8, "op": "filter", "startIndex": 30, "type": "is", }, { "args": "arrival", "length": 15, "op": "filter", "startIndex": 43, "type": "modslot", }, ], "startIndex": 29, }, ], "startIndex": 10, }, ], "startIndex": 0, } `; exports[`parse |is:weapon (is:sniperrifle or (is:armor and modslot:arrival))|: lexer 1`] = ` [ [ "filter", "is", "weapon", ], [ "implicit_and", ], [ "(", ], [ "filter", "is", "sniperrifle", ], [ "or", ], [ "(", ], [ "filter", "is", "armor", ], [ "and", ], [ "filter", "modslot", "arrival", ], [ ")", ], [ ")", ], ] `; exports[`parse |is:weapon and is:sniperrifle or not is:armor and modslot:arrival|: ast 1`] = ` { "length": 64, "op": "or", "operands": [ { "length": 28, "op": "and", "operands": [ { "args": "weapon", "length": 9, "op": "filter", "startIndex": 0, "type": "is", }, { "args": "sniperrifle", "length": 14, "op": "filter", "startIndex": 14, "type": "is", }, ], "startIndex": 0, }, { "length": 32, "op": "and", "operands": [ { "length": 12, "op": "not", "operand": { "args": "armor", "length": 8, "op": "filter", "startIndex": 36, "type": "is", }, "startIndex": 32, }, { "args": "arrival", "length": 15, "op": "filter", "startIndex": 49, "type": "modslot", }, ], "startIndex": 32, }, ], "startIndex": 0, } `; exports[`parse |is:weapon and is:sniperrifle or not is:armor and modslot:arrival|: lexer 1`] = ` [ [ "filter", "is", "weapon", ], [ "and", ], [ "filter", "is", "sniperrifle", ], [ "or", ], [ "not", ], [ "filter", "is", "armor", ], [ "and", ], [ "filter", "modslot", "arrival", ], ] `; exports[`parse |is:weapon is:sniperrifle or is:armor and modslot:arrival|: ast 1`] = ` { "length": 56, "op": "and", "operands": [ { "args": "weapon", "length": 9, "op": "filter", "startIndex": 0, "type": "is", }, { "length": 46, "op": "or", "operands": [ { "args": "sniperrifle", "length": 14, "op": "filter", "startIndex": 10, "type": "is", }, { "length": 28, "op": "and", "operands": [ { "args": "armor", "length": 8, "op": "filter", "startIndex": 28, "type": "is", }, { "args": "arrival", "length": 15, "op": "filter", "startIndex": 41, "type": "modslot", }, ], "startIndex": 28, }, ], "startIndex": 10, }, ], "startIndex": 0, } `; exports[`parse |is:weapon is:sniperrifle or is:armor and modslot:arrival|: lexer 1`] = ` [ [ "filter", "is", "weapon", ], [ "implicit_and", ], [ "filter", "is", "sniperrifle", ], [ "or", ], [ "filter", "is", "armor", ], [ "and", ], [ "filter", "modslot", "arrival", ], ] `; exports[`parse |is:weapon is:sniperrifle or not is:armor modslot:arrival|: ast 1`] = ` { "length": 55, "op": "and", "operands": [ { "args": "weapon", "length": 9, "op": "filter", "startIndex": 0, "type": "is", }, { "length": 30, "op": "or", "operands": [ { "args": "sniperrifle", "length": 14, "op": "filter", "startIndex": 10, "type": "is", }, { "length": 12, "op": "not", "operand": { "args": "armor", "length": 8, "op": "filter", "startIndex": 32, "type": "is", }, "startIndex": 28, }, ], "startIndex": 10, }, { "args": "arrival", "length": 15, "op": "filter", "startIndex": 41, "type": "modslot", }, ], "startIndex": 0, } `; exports[`parse |is:weapon is:sniperrifle or not is:armor modslot:arrival|: lexer 1`] = ` [ [ "filter", "is", "weapon", ], [ "implicit_and", ], [ "filter", "is", "sniperrifle", ], [ "or", ], [ "not", ], [ "filter", "is", "armor", ], [ "implicit_and", ], [ "filter", "modslot", "arrival", ], ] `; exports[`parse |name:"Gahlran's Right Hand"|: ast 1`] = ` { "args": "gahlran's right hand", "length": 27, "op": "filter", "startIndex": 0, "type": "name", } `; exports[`parse |name:"Gahlran's Right Hand"|: lexer 1`] = ` [ [ "filter", "name", "gahlran's right hand", ], ] `; exports[`parse |name:"Hard Light"|: ast 1`] = ` { "args": "hard light", "length": 17, "op": "filter", "startIndex": 0, "type": "name", } `; exports[`parse |name:"Hard Light"|: lexer 1`] = ` [ [ "filter", "name", "hard light", ], ] `; exports[`parse |name:'Hard Light'|: ast 1`] = ` { "args": "hard light", "length": 17, "op": "filter", "startIndex": 0, "type": "name", } `; exports[`parse |name:'Hard Light'|: lexer 1`] = ` [ [ "filter", "name", "hard light", ], ] `; exports[`parse |not "forgotten"|: ast 1`] = ` { "length": 15, "op": "not", "operand": { "args": "forgotten", "length": 11, "op": "filter", "startIndex": 4, "type": "keyword", }, "startIndex": 0, } `; exports[`parse |not "forgotten"|: lexer 1`] = ` [ [ "not", ], [ "filter", "keyword", "forgotten", ], ] `; exports[`parse |not (forgotten)|: ast 1`] = ` { "length": 15, "op": "not", "operand": { "args": "forgotten", "length": 11, "op": "filter", "startIndex": 4, "type": "keyword", }, "startIndex": 0, } `; exports[`parse |not (forgotten)|: lexer 1`] = ` [ [ "not", ], [ "(", ], [ "filter", "keyword", "forgotten", ], [ ")", ], ] `; exports[`parse |not -not:maxpower|: ast 1`] = ` { "length": 17, "op": "not", "operand": { "length": 13, "op": "not", "operand": { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 12, "op": "filter", "startIndex": 5, "type": "is", }, "startIndex": 5, }, "startIndex": 4, }, "startIndex": 0, } `; exports[`parse |not -not:maxpower|: lexer 1`] = ` [ [ "not", ], [ "not", ], [ "filter", "not", "maxpower", ], ] `; exports[`parse |not forgotten|: ast 1`] = ` { "length": 13, "op": "not", "operand": { "args": "forgotten", "length": 9, "op": "filter", "startIndex": 4, "type": "keyword", }, "startIndex": 0, } `; exports[`parse |not forgotten|: ast 2`] = ` { "length": 13, "op": "not", "operand": { "args": "forgotten", "length": 9, "op": "filter", "startIndex": 4, "type": "keyword", }, "startIndex": 0, } `; exports[`parse |not forgotten|: lexer 1`] = ` [ [ "not", ], [ "filter", "keyword", "forgotten", ], ] `; exports[`parse |not forgotten|: lexer 2`] = ` [ [ "not", ], [ "filter", "keyword", "forgotten", ], ] `; exports[`parse |not not not:maxpower|: ast 1`] = ` { "length": 20, "op": "not", "operand": { "length": 16, "op": "not", "operand": { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 12, "op": "filter", "startIndex": 8, "type": "is", }, "startIndex": 8, }, "startIndex": 4, }, "startIndex": 0, } `; exports[`parse |not not not:maxpower|: lexer 1`] = ` [ [ "not", ], [ "not", ], [ "filter", "not", "maxpower", ], ] `; exports[`parse |not not:maxpower|: ast 1`] = ` { "length": 16, "op": "not", "operand": { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 12, "op": "filter", "startIndex": 4, "type": "is", }, "startIndex": 4, }, "startIndex": 0, } `; exports[`parse |not not:maxpower|: lexer 1`] = ` [ [ "not", ], [ "filter", "not", "maxpower", ], ] `; exports[`parse |not:maxpower|: ast 1`] = ` { "length": 12, "op": "not", "operand": { "args": "maxpower", "length": 12, "op": "filter", "startIndex": 0, "type": "is", }, "startIndex": 0, } `; exports[`parse |not:maxpower|: lexer 1`] = ` [ [ "filter", "not", "maxpower", ], ] `; exports[`parse |perk:"수집가"|: ast 1`] = ` { "args": "수집가", "length": 10, "op": "filter", "startIndex": 0, "type": "perk", } `; exports[`parse |perk:"수집가"|: lexer 1`] = ` [ [ "filter", "perk", "수집가", ], ] `; exports[`parse |perk:수집가|: ast 1`] = ` { "args": "수집가", "length": 8, "op": "filter", "startIndex": 0, "type": "perk", } `; exports[`parse |perk:수집가|: lexer 1`] = ` [ [ "filter", "perk", "수집가", ], ] `; exports[`parse |‘grenade launcher reserves’|: ast 1`] = ` { "args": "grenade launcher reserves", "length": 27, "op": "filter", "startIndex": 0, "type": "keyword", } `; exports[`parse |‘grenade launcher reserves’|: lexer 1`] = ` [ [ "filter", "keyword", "grenade launcher reserves", ], ] `; exports[`parse |“grenade launcher reserves”|: ast 1`] = ` { "args": "grenade launcher reserves", "length": 27, "op": "filter", "startIndex": 0, "type": "keyword", } `; exports[`parse |“grenade launcher reserves”|: lexer 1`] = ` [ [ "filter", "keyword", "grenade launcher reserves", ], ] `; exports[`parse |수집가|: ast 1`] = ` { "args": "수집가", "length": 3, "op": "filter", "startIndex": 0, "type": "keyword", } `; exports[`parse |수집가|: lexer 1`] = ` [ [ "filter", "keyword", "수집가", ], ] `; ================================================ FILE: src/app/search/__snapshots__/search-config.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`buildSearchConfig generates a reasonable filter map: is filters 1`] = ` [ "accessories", "accountmaxpower", "adept", "arc", "armor", "armor2.0", "armor3.0", "armorintrinsic", "armormod", "artifice", "autorifle", "blue", "bow", "chest", "classitem", "common", "consumables", "cosmetic", "craftable", "crafted", "crafteddupe", "curated", "currentclass", "customstatlower", "dark", "deepsight", "dupe", "dupelower", "dupeperks", "emblems", "emotes", "energy", "engrams", "enhanceable", "enhanced", "enhancedperk", "enhancementready", "equipment", "equippable", "equipped", "exotic", "extraperk", "featured", "finishers", "focusable", "fusionrifle", "gauntlets", "ghost", "glaive", "green", "grenadelauncher", "handcannon", "hasdisabledmod", "haslight", "hasnotes", "hasornament", "haspower", "hasshader", "heavy", "heavygrenadelauncher", "helmet", "holofoil", "hunter", "incurrentchar", "indimloadout", "infusable", "infuse", "infusionfodder", "iningameloadout", "ininventory", "inleftchar", "inloadout", "inmiddlechar", "inpostmaster", "inrightchar", "invault", "kinetic", "kineticslot", "leg", "legendary", "lfr", "light", "linearfusionrifle", "lmg", "locked", "lostitems", "machinegun", "masterwork", "maxpower", "maxpowerloadout", "messages", "modded", "modifications", "movable", "newgear", "onwrongclass", "origintrait", "ornamented", "patternunlocked", "pinnaclereward", "postmaster", "power", "powerfulreward", "primary", "pulserifle", "purple", "randomroll", "rare", "reptoken", "retiredperk", "rocketlauncher", "scoutrifle", "seasonalartifacts", "shaded", "shaped", "shapeddupe", "shiny", "ships", "shotgun", "sidearm", "smg", "sniperrifle", "solar", "special", "specialgrenadelauncher", "specialorders", "stackable", "stackfull", "stasis", "statdupe", "statlower", "strand", "subclass", "submachine", "sunset", "sword", "tagged", "titan", "tracerifle", "transferable", "transmat", "uncommon", "unlocked", "vehicle", "vendor", "void", "warlock", "weapon", "weaponmod", "white", "yellow", ] `; exports[`buildSearchConfig generates a reasonable filter map: key-value filters 1`] = ` [ "armorintrinsic", "basestat", "breaker", "catalyst", "count", "deepsight", "description", "dupe", "energycapacity", "enhanced", "enhancedperk", "exactname", "exactperk", "foundry", "hash", "id", "inloadout", "keyword", "kills", "light", "masterwork", "maxbasestatvalue", "maxstatloadout", "maxstatvalue", "memento", "modslot", "name", "notes", "perk", "perkname", "power", "powerlimit", "primarystat", "season", "secondarystat", "source", "stack", "stat", "tag", "tertiarystat", "tier", "tunedstat", "weaponlevel", "year", ] `; ================================================ FILE: src/app/search/__snapshots__/search-filter.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`generateSuggestionsForFilter full suggestions for filter format 'freeform', keyword 'a' with suggestions [ 'b', 'c' ], overload undefined 1`] = ` [ "a:", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'query', keyword 'a' with suggestions [ 'b', 'c' ], overload undefined 1`] = ` [ "a:", "a:b", "a:c", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'query', keyword 'maxstatvalue' with suggestions [\\n 'weapons', 'health',\\n 'class', 'grenade',\\n 'super', 'melee',\\n 'mobility', 'resilience',\\n 'recovery', 'discipline',\\n 'intellect', 'strength',\\n 'total', 'any'\\n], overload undefined 1`] = ` [ "maxstatvalue:", "maxstatvalue:weapons", "maxstatvalue:health", "maxstatvalue:class", "maxstatvalue:grenade", "maxstatvalue:super", "maxstatvalue:melee", "maxstatvalue:mobility", "maxstatvalue:resilience", "maxstatvalue:recovery", "maxstatvalue:discipline", "maxstatvalue:intellect", "maxstatvalue:strength", "maxstatvalue:total", "maxstatvalue:any", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'query', keyword 'maxstatvalue' with suggestions [\\n 'weapons', 'health',\\n 'class', 'grenade',\\n 'super', 'melee',\\n 'mobility', 'resilience',\\n 'recovery', 'discipline',\\n 'intellect', 'strength',\\n 'total', 'any'\\n], overload undefined 2`] = ` [ "maxstatvalue:", "maxstatvalue:weapons", "maxstatvalue:health", "maxstatvalue:class", "maxstatvalue:grenade", "maxstatvalue:super", "maxstatvalue:melee", "maxstatvalue:mobility", "maxstatvalue:resilience", "maxstatvalue:recovery", "maxstatvalue:discipline", "maxstatvalue:intellect", "maxstatvalue:strength", "maxstatvalue:total", "maxstatvalue:any", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'range', keyword 'a' with suggestions undefined, overload { worthy: 10, arrivals: 11 } 1`] = ` [ "a:", "a:<", "a:>", "a:<=", "a:>=", "a:worthy", "a:arrivals", "a:<worthy", "a:<arrivals", "a:>worthy", "a:>arrivals", "a:<=worthy", "a:<=arrivals", "a:>=worthy", "a:>=arrivals", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'range', keyword 'a' with suggestions undefined, overload undefined 1`] = ` [ "a:", "a:<", "a:>", "a:<=", "a:>=", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'range', keyword 'energycapacity' with suggestions undefined, overload undefined 1`] = ` [ "energycapacity:", "energycapacity:<", "energycapacity:>", "energycapacity:<=", "energycapacity:>=", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'stat', keyword 'a' with suggestions [ 'b', 'c' ], overload undefined 1`] = ` [ "a:", "a:b:", "a:b:<", "a:b:>", "a:b:<=", "a:b:>=", "a:c:", "a:c:<", "a:c:>", "a:c:<=", "a:c:>=", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'stat', keyword 'stat' with suggestions [\\n 'rpm', 'rof', 'charge',\\n 'impact', 'handling', 'ventspeed',\\n 'heatgen', 'cooling', 'range',\\n 'stability', 'reload', 'magazine',\\n 'aimassist', 'equipspeed', 'shieldduration',\\n 'velocity', 'blastradius', 'recoildirection',\\n 'drawtime', 'zoom', 'airborne',\\n 'accuracy', 'ammogen', 'persistence',\\n 'swingspeed', 'guardefficiency', 'guardresistance',\\n 'chargerate', 'guardendurance', 'ammocapacity',\\n 'weapons', 'health', 'class',\\n 'grenade', 'super', 'melee',\\n 'mobility', 'resilience', 'recovery',\\n 'discipline', 'intellect', 'strength',\\n 'total', 'any'\\n], overload undefined 1`] = ` [ "stat:", "stat:rpm:", "stat:rpm:<", "stat:rpm:>", "stat:rpm:<=", "stat:rpm:>=", "stat:rof:", "stat:rof:<", "stat:rof:>", "stat:rof:<=", "stat:rof:>=", "stat:charge:", "stat:charge:<", "stat:charge:>", "stat:charge:<=", "stat:charge:>=", "stat:impact:", "stat:impact:<", "stat:impact:>", "stat:impact:<=", "stat:impact:>=", "stat:handling:", "stat:handling:<", "stat:handling:>", "stat:handling:<=", "stat:handling:>=", "stat:ventspeed:", "stat:ventspeed:<", "stat:ventspeed:>", "stat:ventspeed:<=", "stat:ventspeed:>=", "stat:heatgen:", "stat:heatgen:<", "stat:heatgen:>", "stat:heatgen:<=", "stat:heatgen:>=", "stat:cooling:", "stat:cooling:<", "stat:cooling:>", "stat:cooling:<=", "stat:cooling:>=", "stat:range:", "stat:range:<", "stat:range:>", "stat:range:<=", "stat:range:>=", "stat:stability:", "stat:stability:<", "stat:stability:>", "stat:stability:<=", "stat:stability:>=", "stat:reload:", "stat:reload:<", "stat:reload:>", "stat:reload:<=", "stat:reload:>=", "stat:magazine:", "stat:magazine:<", "stat:magazine:>", "stat:magazine:<=", "stat:magazine:>=", "stat:aimassist:", "stat:aimassist:<", "stat:aimassist:>", "stat:aimassist:<=", "stat:aimassist:>=", "stat:equipspeed:", "stat:equipspeed:<", "stat:equipspeed:>", "stat:equipspeed:<=", "stat:equipspeed:>=", "stat:shieldduration:", "stat:shieldduration:<", "stat:shieldduration:>", "stat:shieldduration:<=", "stat:shieldduration:>=", "stat:velocity:", "stat:velocity:<", "stat:velocity:>", "stat:velocity:<=", "stat:velocity:>=", "stat:blastradius:", "stat:blastradius:<", "stat:blastradius:>", "stat:blastradius:<=", "stat:blastradius:>=", "stat:recoildirection:", "stat:recoildirection:<", "stat:recoildirection:>", "stat:recoildirection:<=", "stat:recoildirection:>=", "stat:drawtime:", "stat:drawtime:<", "stat:drawtime:>", "stat:drawtime:<=", "stat:drawtime:>=", "stat:zoom:", "stat:zoom:<", "stat:zoom:>", "stat:zoom:<=", "stat:zoom:>=", "stat:airborne:", "stat:airborne:<", "stat:airborne:>", "stat:airborne:<=", "stat:airborne:>=", "stat:accuracy:", "stat:accuracy:<", "stat:accuracy:>", "stat:accuracy:<=", "stat:accuracy:>=", "stat:ammogen:", "stat:ammogen:<", "stat:ammogen:>", "stat:ammogen:<=", "stat:ammogen:>=", "stat:persistence:", "stat:persistence:<", "stat:persistence:>", "stat:persistence:<=", "stat:persistence:>=", "stat:swingspeed:", "stat:swingspeed:<", "stat:swingspeed:>", "stat:swingspeed:<=", "stat:swingspeed:>=", "stat:guardefficiency:", "stat:guardefficiency:<", "stat:guardefficiency:>", "stat:guardefficiency:<=", "stat:guardefficiency:>=", "stat:guardresistance:", "stat:guardresistance:<", "stat:guardresistance:>", "stat:guardresistance:<=", "stat:guardresistance:>=", "stat:chargerate:", "stat:chargerate:<", "stat:chargerate:>", "stat:chargerate:<=", "stat:chargerate:>=", "stat:guardendurance:", "stat:guardendurance:<", "stat:guardendurance:>", "stat:guardendurance:<=", "stat:guardendurance:>=", "stat:ammocapacity:", "stat:ammocapacity:<", "stat:ammocapacity:>", "stat:ammocapacity:<=", "stat:ammocapacity:>=", "stat:weapons:", "stat:weapons:<", "stat:weapons:>", "stat:weapons:<=", "stat:weapons:>=", "stat:health:", "stat:health:<", "stat:health:>", "stat:health:<=", "stat:health:>=", "stat:class:", "stat:class:<", "stat:class:>", "stat:class:<=", "stat:class:>=", "stat:grenade:", "stat:grenade:<", "stat:grenade:>", "stat:grenade:<=", "stat:grenade:>=", "stat:super:", "stat:super:<", "stat:super:>", "stat:super:<=", "stat:super:>=", "stat:melee:", "stat:melee:<", "stat:melee:>", "stat:melee:<=", "stat:melee:>=", "stat:mobility:", "stat:mobility:<", "stat:mobility:>", "stat:mobility:<=", "stat:mobility:>=", "stat:resilience:", "stat:resilience:<", "stat:resilience:>", "stat:resilience:<=", "stat:resilience:>=", "stat:recovery:", "stat:recovery:<", "stat:recovery:>", "stat:recovery:<=", "stat:recovery:>=", "stat:discipline:", "stat:discipline:<", "stat:discipline:>", "stat:discipline:<=", "stat:discipline:>=", "stat:intellect:", "stat:intellect:<", "stat:intellect:>", "stat:intellect:<=", "stat:intellect:>=", "stat:strength:", "stat:strength:<", "stat:strength:>", "stat:strength:<=", "stat:strength:>=", "stat:total:", "stat:total:<", "stat:total:>", "stat:total:<=", "stat:total:>=", "stat:any:", "stat:any:<", "stat:any:>", "stat:any:<=", "stat:any:>=", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'undefined', keyword '[ 'a' ]' with suggestions undefined, overload undefined 1`] = ` [ "is:a", "not:a", ] `; exports[`generateSuggestionsForFilter full suggestions for filter format 'undefined', keyword '[ 'a', 'b', 'c' ]' with suggestions undefined, overload undefined 1`] = ` [ "is:a", "not:a", "is:b", "not:b", "is:c", "not:c", ] `; ================================================ FILE: src/app/search/armory-search.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimLanguage } from 'app/i18n'; import { getSeason } from 'app/inventory/store/season'; import { chainComparator, compareBy } from 'app/utils/comparators'; import { emptyArray } from 'app/utils/empty'; import { getItemYear } from 'app/utils/item-utils'; import { BucketHashes } from 'data/d2/generated-enums'; import extraItemCollectibles from 'data/d2/unreferenced-collections-items.json'; import { ArmorySearchItem, SearchItemType } from './autocomplete'; import { plainString } from './text-utils'; export interface ArmoryEntry { name: string; /** The plainString'd version (with diacritics removed, if applicable). */ plainName: string; icon: string; hash: number; seasonName: string | undefined; season: number; year: number | undefined; } export function buildArmoryIndex(defs: D2ManifestDefinitions | undefined, language: DimLanguage) { if (!defs) { return undefined; } const results: ArmoryEntry[] = []; const invItemTable = defs.InventoryItem.getAll(); const seasons = Object.values(defs.Season.getAll()); const additionalCollectibles = Object.values(extraItemCollectibles); for (const h in invItemTable) { const i = invItemTable[h]; if ( // A good heuristic for "is this weapon not totally irrelevant" is the presence of // the exact hash in collections, but some reissues do not reference the latest version // in the collections entry (and thus the latest item has no `collectibleHash`). // Use `additionalCollectibles` to patch those in. Note that we do not use the // `collectibleFinder` because not matching some old items is the entire point here. (i.collectibleHash || additionalCollectibles.includes(i.hash)) && i.displayProperties && (i.inventory?.bucketTypeHash === BucketHashes.KineticWeapons || i.inventory?.bucketTypeHash === BucketHashes.EnergyWeapons || i.inventory?.bucketTypeHash === BucketHashes.PowerWeapons) ) { const season = getSeason(i, defs); const seasonName = season ? seasons.find((s) => s.seasonNumber === season)?.displayProperties?.name : undefined; results.push({ hash: i.hash, name: i.displayProperties.name, plainName: plainString(i.displayProperties.name, language), icon: i.displayProperties.icon, seasonName, season: season, year: getItemYear(i, defs), }); } } results.sort( chainComparator( compareBy((entry) => -entry.season), compareBy((entry) => entry.name), ), ); return results; } export function getArmorySuggestions( armoryIndex: ArmoryEntry[] | undefined, query: string, language: DimLanguage, ): ArmorySearchItem[] { const plainQuery = plainString(query, language); const armoryEntries = query.length ? armoryIndex?.filter((armoryItem) => armoryItem.plainName.includes(plainQuery)) : undefined; if (!armoryEntries) { return emptyArray(); } // Prefer suggestions that start with the query as opposed to those where it's in the middle const sortedEntries = armoryEntries.sort( compareBy((entry) => !entry.plainName.startsWith(plainQuery)), ); // If there are more than 10 entries, the user's query is probably not descriptive enough to show many items, // But if they've typed enough characters, maybe show some? const limitedEntries = (sortedEntries.length <= 10 ? sortedEntries : query.length >= 5 && sortedEntries.slice(0, 3)) || emptyArray(); return limitedEntries.map((armoryItem) => ({ type: SearchItemType.ArmoryEntry, query: { fullText: query || '', body: query || '', }, armoryItem, })); } ================================================ FILE: src/app/search/autocomplete.test.ts ================================================ import { Search, SearchType } from '@destinyitemmanager/dim-api-types'; import { autocompleteTermSuggestions, filterSortRecentSearches, makeFilterComplete, } from './autocomplete'; import { buildItemSearchConfig } from './items/item-search-filter'; import { quoteFilterString } from './query-parser'; /** * Given a string like "foo ba|r", find where the "|" is and remove it, * returning its index. This allows for readable test cases that depend on * cursor position. If the cursor should be at the end of the string, it can be * omitted entirely. */ function extractCaret(stringWithCaretPlaceholder: string): [caretIndex: number, query: string] { const caretIndex = stringWithCaretPlaceholder.indexOf('|'); if (caretIndex === -1) { return [stringWithCaretPlaceholder.length, stringWithCaretPlaceholder]; } return [caretIndex, stringWithCaretPlaceholder.replace('|', '')]; } describe('autocompleteTermSuggestions', () => { const searchConfig = buildItemSearchConfig(2, 'en'); const filterComplete = makeFilterComplete(searchConfig); const cases: [query: string, expected: string][] = [ ['is:haspower is:b', 'is:haspower is:bow'], ['(is:blue ju|n)', '(is:blue tag:junk)'], ['is:bow is:v|oi', 'is:bow is:void'], ['season:>outl', 'season:>outlaw'], ['not(', 'Expected failure'], ['memento:', 'memento:any'], ['foo memento:', 'foo memento:any'], ]; const plainStringCases: [query: string, mockCandidate: string][] = [['jotu', 'jötunn']]; test.each(plainStringCases)( 'autocomplete within query for plain string match {%s} - {%s}', (queryWithCaret, mockCandidate) => { const [caretIndex, query] = extractCaret(queryWithCaret); const candidates = autocompleteTermSuggestions( query, caretIndex, () => [`name:"${mockCandidate}"`], searchConfig, ); expect(candidates).toMatchSnapshot(); }, ); test.each(cases)( 'autocomplete within query for {%s}', (queryWithCaret: string, expected: string) => { const [caretIndex, query] = extractCaret(queryWithCaret); const candidates = autocompleteTermSuggestions( query, caretIndex, filterComplete, searchConfig, ); expect(candidates[0]?.query.body ?? 'Expected failure').toBe(expected); }, ); const multiWordCases: [query: string, expected: string][] = [ ['arctic haz', 'name:"arctic haze"'], ['is:weapon arctic haz| -is:exotic', 'is:weapon name:"arctic haze" -is:exotic'], ['name:"arctic haz', 'name:"arctic haze"'], ["name:'arctic haz", 'name:"arctic haze"'], ['name:"foo" arctic haz', 'name:"foo" name:"arctic haze"'], ["ager's sce", 'name:"ager\'s scepter"'], ['the last word', 'name:"the last word"'], ['acd/0 fee', 'name:"acd/0 feedback fence"'], ['stat:rpm:200 first in, last', 'stat:rpm:200 name:"first in, last out"'], ['two-tail', 'name:"two-tailed fox"'], ['(is:a or is:b) and (is:c or multi w|)', '(is:a or is:b) and (is:c or name:"multi word")'], ['"rare curio" arctic haz', '"rare curio" name:"arctic haze"'], ['"rare curio" or arctic haz', '"rare curio" or name:"arctic haze"'], ['toil and trou', 'name:"toil and trouble"'], ['perkname:"fate of', 'perkname:"fate of all fools"'], ['perkname:fate of', 'perkname:"fate of all fools"'], // Expected (or at least not yet supported) failures: ['rare curio or arctic haz', 'rare curio or name:"arctic haze"'], ['name:heritage arctic haze', 'name:heritage name:"arctic haze"'], // this actually works in the app but relies on the full manifest ['adept pali', 'adept name:"the palindrome"'], ]; // Item names the autocompleter should know about for the above multiWordCases to complete const itemNames = [ 'heritage', 'arctic haze', "ager's scepter", 'the last word', 'acd/0 feedback fence', 'first in, last out', 'two-tailed fox', 'multi word', 'toil and trouble', 'not forgotten', 'fate of all fools', 'the palindrome', ]; // Mocked out filterComplete function that only knows a few tricks const filterCompleteMock = (term: string) => { const parts = term.split(':'); let filter = 'name'; if (parts.length > 1) { filter = parts.shift()!; } let value = parts[0]; if (value.startsWith("'") || value.startsWith('"')) { value = value.slice(1); } if (value.endsWith("'") || value.endsWith('"')) { value = value.slice(0, value.length - 1); } const result = itemNames.find((i) => i.includes(value)); return result ? [`${filter}:${quoteFilterString(result)}`] : []; }; test.each(multiWordCases)( 'autocomplete within multi-word query for {%s} should suggest {%s}', (queryWithCaret: string, expected: string) => { const [caretIndex, query] = extractCaret(queryWithCaret); const candidates = autocompleteTermSuggestions( query, caretIndex, filterCompleteMock, searchConfig, ); expect(candidates[0]?.query.body).toBe(expected); }, ); }); describe('filterSortRecentSearches', () => { const recentSearches: Search[] = [ { query: 'recent saved', usageCount: 1, saved: true, lastUsage: Date.now(), type: SearchType.Item, }, { query: 'yearold saved', usageCount: 1, saved: true, lastUsage: Date.now() - 365 * 24 * 60 * 60 * 1000, type: SearchType.Item, }, { query: 'yearold unsaved', usageCount: 1, saved: false, lastUsage: Date.now() - 365 * 24 * 60 * 60 * 1000, type: SearchType.Item, }, { query: 'yearold highuse', usageCount: 100, saved: false, lastUsage: Date.now() - 365 * 24 * 60 * 60 * 1000, type: SearchType.Item, }, { query: 'dayold highuse', usageCount: 15, saved: false, lastUsage: Date.now() - 1 * 24 * 60 * 60 * 1000, type: SearchType.Item, }, { query: 'dim api autosuggest', usageCount: 0, saved: false, lastUsage: 0, type: SearchType.Item, }, ]; for (let day = 0; day < 30; day++) { for (let usageCount = 1; usageCount < 10; usageCount++) { recentSearches.push({ query: `${day} days old, ${usageCount} uses`, lastUsage: Date.now() - day * 24 * 60 * 60 * 1000, usageCount, saved: false, type: SearchType.Item, }); } } const cases = [[''], ['high']]; test.each(cases)('filter/sort recent searches for query |%s|', (query) => { const candidates = filterSortRecentSearches(query, recentSearches); expect(candidates.map((c) => c.query.fullText)).toMatchSnapshot(); }); const savedSearches: Search[] = [ { query: 'is:patternunlocked -is:crafted', usageCount: 1, saved: true, lastUsage: Date.now(), type: SearchType.Item, }, { query: '/* random-roll craftable guns */ is:patternunlocked -is:crafted', usageCount: 1, saved: true, lastUsage: Date.now() - 24 * 60 * 60 * 1000, type: SearchType.Item, }, ]; const highlightCases: string[] = ['', 'craft', 'craftable', 'crafted']; test.each(highlightCases)('check saved search highlighting for query |%s|', (query: string) => { const candidates = filterSortRecentSearches(query, savedSearches); expect(candidates).toMatchSnapshot(); }); }); describe('filterComplete', () => { const searchConfig = buildItemSearchConfig(2, 'en'); const filterComplete = makeFilterComplete(searchConfig); const terms = [['is:b'], ['jun'], ['sni'], ['stat:mob'], ['stat'], ['stat:'], ['ote']]; test.each(terms)('autocomplete terms for |%s|', (term) => { const candidates = filterComplete(term); expect(candidates).toMatchSnapshot(); }); }); ================================================ FILE: src/app/search/autocomplete.ts ================================================ import { Search } from '@destinyitemmanager/dim-api-types'; import { compact, filterMap, uniqBy } from 'app/utils/collections'; import { chainComparator, compareBy, reverseComparator } from 'app/utils/comparators'; import { ArmoryEntry, getArmorySuggestions } from './armory-search'; import { filterDescriptionText } from './filter-description'; import { canonicalFilterFormats } from './filter-types'; import { lexer, makeCommentString, parseQuery, QueryLexerError } from './query-parser'; import { FiltersMap, SearchConfig, Suggestion } from './search-config'; import { plainString } from './text-utils'; /** The autocompleter/dropdown will suggest different types of searches */ export const enum SearchItemType { /** Searches from your history */ Recent, /** Explicitly saved searches */ Saved, /** Searches suggested by DIM Sync but not part of your history */ Suggested, /** Generated autocomplete searches */ Autocomplete, /** Open help */ Help, /** Open the armory view for a page */ ArmoryEntry, } export interface SearchQuery { /** The full text of the query */ fullText: string; /** The query's top-level comment */ header?: string; /** The query text excluding the top-level comment */ body: string; /** Help text */ helpText?: string; } interface BaseSearchItem { type: SearchItemType; /** The suggested query */ query: SearchQuery; /** An optional part of the query that will be highlighted */ highlightRange?: { section: 'header' | 'body'; /** The indices of the first and last character that should be highlighted */ range: [number, number]; }; } export interface ArmorySearchItem extends BaseSearchItem { type: SearchItemType.ArmoryEntry; armoryItem: ArmoryEntry; } /** An item in the search autocompleter */ export type SearchItem = | ArmorySearchItem | (BaseSearchItem & { type: Exclude<SearchItemType, SearchItemType.ArmoryEntry>; }); /** matches a keyword that's probably a math comparison, but not with a value on the RHS */ const mathCheck = /[\d<>=]$/; /** if one of these has been typed, stop guessing which filter and just offer this filter's values */ // TODO: Generate this from the search config const filterNames = [ 'is', 'not', 'tag', 'notes', 'stat', 'stack', 'count', 'source', 'perk', 'perkname', 'mod', 'modname', 'name', 'description', 'dupe', ]; /** * Produce a memoized autocompleter function that takes search text plus a list of recent/saved searches * and produces the contents of the autocomplete list. */ export default function createAutocompleter<I, FilterCtx, SuggestionsCtx>( searchConfig: SearchConfig<I, FilterCtx, SuggestionsCtx>, armoryEntries: ArmoryEntry[] | undefined, ) { const filterComplete = makeFilterComplete(searchConfig); return ( query: string, caretIndex: number, recentSearches: Search[], includeArmory?: boolean, maxResults = 7, ): SearchItem[] => { // If there's a query, it's always the first entry const queryItem: SearchItem | undefined = query ? { type: SearchItemType.Autocomplete, query: { fullText: query, body: query, }, } : undefined; // Generate completions of the current search const filterSuggestions = autocompleteTermSuggestions( query, caretIndex, filterComplete, searchConfig, ); // Recent/saved searches const recentSearchItems = filterSortRecentSearches(query, recentSearches); // Help is always last... // Add an item for opening the filter help const helpItem: SearchItem = { type: SearchItemType.Help, query: { // use query as the text so we don't change text when selecting it fullText: query || '', body: query || '', }, }; const armorySuggestions = includeArmory ? getArmorySuggestions(armoryEntries, query, searchConfig.language) : []; // mix them together return [ ...uniqBy( compact([queryItem, ...filterSuggestions, ...recentSearchItems]), (i) => i.query.fullText, ).slice(0, maxResults), ...armorySuggestions, helpItem, ]; }; } // TODO: this should probably be different when there's a query vs not. With a query // it should sort on how closely you match, while without a query it's just offering // you your "favorite" searches. export const recentSearchComparator = reverseComparator( chainComparator<Search>( // Saved searches before recents compareBy((s) => s.saved), compareBy((s) => frecency(s.usageCount, s.lastUsage)), ), ); /** * "Frecency" combines frequency and recency to form a ranking score from [0,1]. * Note that our usages aren't individually tracked, so they never expire. */ function frecency(usageCount: number, lastUsedTimestampMillis: number) { // We just multiply them together with equal weight, but we may want to weight them differently in the future. return normalizeUsage(usageCount) * normalizeRecency(lastUsedTimestampMillis); } /** * A sigmoid normalization function that normalizes usages to [0,1]. After 10 uses it's all the same. * https://www.desmos.com/calculator/fxi9thkuft */ // function normalizeUsage(val: number) { const z = 0.4; const k = 0.5; const t = 0.9; return (1 + t) / (1 + Math.exp(-k * (val - z))) - t; } /** * An exponential decay score based on the age of the last usage. * https://www.desmos.com/calculator/3jfqccibdn */ // function normalizeRecency(timestamp: number) { const days = (Date.now() - timestamp) / (1000 * 60 * 60 * 24); const halfLife = 14; // two weeks return Math.pow(2, -days / halfLife); } export function filterSortRecentSearches(query: string, recentSearches: Search[]): SearchItem[] { // Recent/saved searches const qLower = query.toLowerCase(); const recentSearchesForQuery = query ? recentSearches.filter((s) => { const sQueryLower = s.query.toLowerCase(); return sQueryLower !== qLower && sQueryLower.includes(qLower); }) : Array.from(recentSearches); return recentSearchesForQuery.sort(recentSearchComparator).map((s) => { const ast = parseQuery(s.query); const topLevelComment = ast.comment && makeCommentString(ast.comment); const result: SearchItem = { type: s.saved ? SearchItemType.Saved : s.usageCount > 0 ? SearchItemType.Recent : SearchItemType.Suggested, query: { fullText: s.query, header: ast.comment, body: topLevelComment ? s.query.substring(topLevelComment.length).trim() : s.query, }, }; // highlight the matched range of the query if (query) { if (result.query.header) { const index = result.query.header.toLowerCase().indexOf(qLower); if (index !== -1) { result.highlightRange = { section: 'header', range: [index, index + query.length], }; } } if (!result.highlightRange) { const index = result.query.body.toLowerCase().indexOf(qLower); if (index !== -1) { result.highlightRange = { section: 'body', range: [index, index + query.length], }; } } } return result; }); } const caretEndRegex = /[\s)]|$/; /** * Find the position of the last "incomplete" filter segment of the query before the caretIndex. * * For example, given the query (with the caret at |): * name:foo bar| baz * This should return { term: "bar", index: 9 } * * @returns the start indexes of various points that could be incomplete filters */ function findLastFilter(queryUpToCaret: string): number[] | null { // Find the indexes where any incomplete filter starts. For example if the query is: // name:"foo" bar baz // then the open keywords are "bar baz" and "baz" let incompleteFilterIndices: number[] = []; try { // We can use the query lexer for this to scan through tokens in the query without parsing the whole AST. for (const token of lexer(queryUpToCaret)) { switch (token.type) { // We're trying to complete any filter. Maybe it's actually complete, which is OK because we just won't return a suggestion case 'filter': { if ( // Ignore complete quoted tokens, they're definitively finished. !token.quoted ) { incompleteFilterIndices.push(token.startIndex); } else { incompleteFilterIndices = []; } break; } case 'and': case 'or': case 'implicit_and': // ignore these - they neither start an incomplete filter section, nor end it break; default: // reset, we saw something that's definitely not part of a filter incompleteFilterIndices = []; break; } } } catch (e) { // If the lexer failed because of unmatched quotes, that's *definitely* something to autocomplete! if (e instanceof QueryLexerError) { incompleteFilterIndices = [e.startIndex]; } } return incompleteFilterIndices; } /** * Given a query and a cursor position, isolate the term that's being typed and offer reformulated queries * that replace that term with one from our filterComplete function. */ export function autocompleteTermSuggestions<I, FilterCtx, SuggestionsCtx>( query: string, caretIndex: number, filterComplete: (term: string) => string[], searchConfig: SearchConfig<I, FilterCtx, SuggestionsCtx>, ): SearchItem[] { if (!query) { return []; } // Seek to the end of the current part caretIndex = (caretEndRegex.exec(query.slice(caretIndex))?.index || 0) + caretIndex; const queryUpToCaret = query.slice(0, caretIndex); const lastFilters = findLastFilter(queryUpToCaret); if (!lastFilters) { return []; } // Find the first index that gives us suggestions and return those suggestions for (const index of lastFilters) { const base = query.slice(0, index); const term = queryUpToCaret.substring(index); const candidates = filterComplete(term); // new query is existing query minus match plus suggestion const result = candidates.map((word): SearchItem => { const filterDef = findFilter(word, searchConfig.filtersMap); const newQuery = base + word + query.slice(caretIndex); const helpText: string | undefined = filterDef ? filterDescriptionText(filterDef.description) : undefined; return { query: { fullText: newQuery, body: newQuery, helpText: helpText?.replace(/\.$/, ''), }, type: SearchItemType.Autocomplete, highlightRange: { section: 'body', range: [index, index + word.length], }, }; }); if (result.length) { return result; } } return []; } function findFilter<I, FilterCtx, SuggestionsCtx>( term: string, filtersMap: FiltersMap<I, FilterCtx, SuggestionsCtx>, ) { const parts = term.split(':'); const filterName = parts[0]; const filterValue = parts[1]; // "is:" filters are slightly special cased return filterName === 'is' ? filtersMap.isFilters[filterValue] : filtersMap.kvFilters[filterName]; } /** * This builds a filter-complete function that uses the given search config's keywords to * offer autocomplete suggestions for a partially typed term. */ export function makeFilterComplete<I, FilterCtx, SuggestionsCtx>( searchConfig: SearchConfig<I, FilterCtx, SuggestionsCtx>, ) { // these filters might include quotes, so we search for two text segments to ignore quotes & colon // i.e. `name:test` can find `name:"test item"` const freeformTerms: string[] = []; const multiqueryTermsLookup: NodeJS.Dict<string[]> = {}; for (const filter of Object.values(searchConfig.filtersMap.kvFilters)) { const formats = canonicalFilterFormats(filter.format); if (formats.includes('freeform')) { for (const k of filter.keywords) { freeformTerms.push(`${k}:`); } } if (formats.includes('multiquery')) { for (const k of filter.keywords) { (multiqueryTermsLookup[k] ??= []).push(...(filter.suggestions ?? [])); } } } // TODO: also search filter descriptions return (typed: string): string[] => { if (!typed) { return []; } const typedToLower = typed.toLowerCase(); let typedPlain = plainString(typedToLower, searchConfig.language); const typedSegments = typedPlain.split(':'); const possibleKeyword = typedSegments[0]; // because we are fighting against other elements for space in the suggestion dropdown, // we will entirely skip "not" and "<" and ">" and "<=" and ">=" suggestions, // unless the user seems to explicity be working toward them const hasNotModifier = typedPlain.startsWith('not'); const includesAdvancedMath = typedPlain.endsWith(':') || typedPlain.endsWith('<') || typedPlain.endsWith('<'); const filterLowPrioritySuggestions = (s: Suggestion) => (hasNotModifier || !s.plainText.startsWith('not:')) && (includesAdvancedMath || !/[<>]=?$/.test(s.plainText)); let mustStartWith = ''; if (freeformTerms.some((t) => typedPlain.startsWith(t))) { mustStartWith = typedSegments.shift()!; typedPlain = typedSegments.join(':'); } // for most searches (non-string-based), if there's already a colon typed, // we are on a path through known terms, not wildly guessing, so we only match // from beginning of the typed string, instead of middle snippets from suggestions. // this way, "stat:" matches "stat:" but not "basestat:" // and "stat" matches "stat:" and "basestat:" const matchType = !mustStartWith && typedPlain.includes(':') ? 'startsWith' : 'includes'; let suggestions = searchConfig.suggestions .filter( (word) => word.plainText.startsWith(mustStartWith) && word.plainText[matchType](typedPlain), ) .filter(filterLowPrioritySuggestions); // TODO: sort this first?? it depends on term in one place if (multiqueryTermsLookup[possibleKeyword] && filterNames.includes(possibleKeyword)) { // For multiquery filters, if the user has typed a + (or hasn't typed // anything) they're looking to add another query term, so offer to append // one. const existingTerms = new Set( (typedSegments[1] || '') .split('+') .filter((t) => multiqueryTermsLookup[possibleKeyword]!.includes(t)), ); const stem = `${typedSegments[0]}:${[...existingTerms].join('+')}${existingTerms.size ? '+' : ''}`; suggestions.push( ...filterMap(multiqueryTermsLookup[possibleKeyword], (t) => { if (!existingTerms.has(t)) { const newTerm = stem + t; return { rawText: newTerm, plainText: newTerm, }; } }), ); } suggestions = suggestions.sort( chainComparator( // --------------- // assumptions based on user behavior. beyond the "contains" filter above, considerations like // "the user is probably typing the begining of the filter name, not the middle" // --------------- // prioritize terms where we are typing the beginning of, ignoring the stem: // 'stat' -> 'stat:' before 'basestat:' // 'arm' -> 'is:armor' before 'is:sidearm' // but only for top level stuff (we want examples like 'basestat:' before 'stat:rpm:') compareBy( (word) => colonCount(word.plainText) > 1 ? 1 // last if it's a big one like 'stat:rpm:' : word.plainText.startsWith(typedPlain) || word.plainText.indexOf(typedPlain) === word.plainText.indexOf(':') + 1 ? -1 // first if it's a term start or segment start : 0, // mid otherwise ), // --------------- // once we have accounted for high level assumptions about user input, // make some opinionated choices about which filters are a priority // --------------- // tags are UGC and therefore important compareBy((word) => !word.plainText.startsWith('tag:')), // push "not" and "<=" and ">=" to the bottom if they are present // we discourage "not", and "<=" and ">=" are highly discoverable from "<" and ">" compareBy( (word) => word.plainText.startsWith('not:') || word.plainText.includes(':<=') || word.plainText.includes(':>='), ), // sort more-basic incomplete terms (fewer colons) to the front // i.e. suggest "stat:" before "stat:magazine:" compareBy((word) => (word.plainText.startsWith('is:') ? 0 : colonCount(word.plainText))), // for is/not, prioritize words with less left to type, // so "is:armor" comes before "is:armormod". // but only is/not, not other list-based suggestions, // otherwise it prioritizes "dawn" over "redwar" after you type "season:" // which i am not into. compareBy((word) => { if (word.plainText.startsWith('not:') || word.plainText.startsWith('is:')) { return word.plainText.length - (typedPlain.length + word.plainText.indexOf(typedPlain)); } else { return 0; } }), // sort incomplete terms (ending with ':') to the front compareBy((word) => !word.plainText.endsWith(':')), // (within the math operators that weren't shoved to the far bottom,) // push math operators to the front for things like "masterwork:" compareBy((word) => !mathCheck.test(word.plainText)), ), ); if (suggestions.length) { // we will always add in (later) a suggestion of "what you've already typed so far" // so prevent "what's been typed" from appearing in the returned suggestions from this function const deDuped = new Set(suggestions.map((suggestion) => suggestion.rawText)); deDuped.delete(typed); deDuped.delete(typedToLower); deDuped.delete(typedPlain); return [...deDuped]; } return []; }; } function colonCount(s: string) { let count = 0; for (const c of s) { if (c === ':') { count++; } } return count; } ================================================ FILE: src/app/search/d1-known-values.ts ================================================ // ✨ magic values ✨ // this file has non-programatically decided information // hashes, names, & enums, hand-crafted and chosen by us import { BucketHashes, ItemCategoryHashes, StatHashes } from 'data/d2/generated-enums'; // // STATS KNOWN VALUES // export const enum D1_StatHashes { Defense = StatHashes.Defense, // Same as in D2 Attack = 368428387, // Not the same as in D2 } /** hashes representing D1 PL stats */ export const D1LightStats = [D1_StatHashes.Defense, D1_StatHashes.Attack]; /** hashes representing D1 Progressions */ export const enum D1ProgressionHashes { Prestige = 2030054750, } // // ITEMS / ITEM CATEGORY KNOWN VALUES // /** these weapons exist in D1&2 */ export const D1ItemCategoryHashes = { autorifle: ItemCategoryHashes.AutoRifle, handcannon: ItemCategoryHashes.HandCannon, pulserifle: ItemCategoryHashes.PulseRifle, scoutrifle: ItemCategoryHashes.ScoutRifle, fusionrifle: ItemCategoryHashes.FusionRifle, sniperrifle: ItemCategoryHashes.SniperRifle, shotgun: ItemCategoryHashes.Shotgun, machinegun: ItemCategoryHashes.MachineGun, rocketlauncher: ItemCategoryHashes.RocketLauncher, sidearm: ItemCategoryHashes.Sidearm, sword: ItemCategoryHashes.Sword, }; export const enum D1BucketHashes { Artifact = 434908299, RecordBook = 2987185182, RecordBookLegacy = 549485690, Missions = BucketHashes.Engrams, // D1 missions are D2 engrams Quests = 1801258597, Bounties = 2197472680, Shader = 2973005342, Horn = 3796357825, D1Emotes = 3054419239, } // // OTHER STUFF // /** sublime engrams */ export const sublimeEngrams = [ 1986458096, // -gauntlet 2218811091, 2672986950, // -body-armor 779347563, 3497374572, // -class-item 808079385, 3592189221, // -leg-armor 738642122, 3797169075, // -helmet 838904328, ]; export const boosts = [ 1043138475, // -black-wax-idol 1772853454, // -blue-polyphage 3783295803, // -ether-seeds 3446457162, // -resupply-codes ]; export const supplies = [ 269776572, // -house-banners 3632619276, // -silken-codex 2904517731, // -axiomatic-beads 1932910919, // -network-keys ]; /** for D1 items: used to calculate which vendor an item could have come from */ export const vendorHashes: Record<'required' | 'restricted', NodeJS.Dict<number[]>> = { required: { fwc: [995344558], // SOURCE_VENDOR_FUTURE_WAR_CULT / Future War Cult do: [103311758], // SOURCE_VENDOR_DEAD_ORBIT / Dead Orbit nm: [3072854931], // SOURCE_VENDOR_NEW_MONARCHY / New Monarchy speaker: [4241664776], // SOURCE_VENDOR_SPEAKER / Speaker variks: [512830513], // SOURCE_VENDOR_FALLEN / Variks shipwright: [3721473564], // SOURCE_VENDOR_SHIPWRIGHT / Shipwright vanguard: [1482793537], // SOURCE_VENDOR_VANGUARD / osiris: [3378481830], // SOURCE_VENDOR_OSIRIS / Osiris xur: [2179714245], // SOURCE_VENDOR_BLACK_MARKET / Shaxx shaxx: [4134961255], // SOURCE_VENDOR_CRUCIBLE_HANDLER / cq: [1362425043], // SOURCE_VENDOR_CRUCIBLE_QUARTERMASTER / Crucible Quartermaster eris: [1374970038], // SOURCE_VENDOR_CROTAS_BANE / Eris Morn ev: [3559790162], // SOURCE_VENDOR_SPECIAL_ORDERS / Eververse gunsmith: [353834582], // SOURCE_VENDOR_GUNSMITH / }, restricted: { fwc: [353834582], // remove motes of light & strange coins do: [353834582], nm: [353834582], speaker: [353834582], cq: [353834582, 2682516238], // remove ammo synths and planetary materials }, }; /** for D1 items: used to calculate which activity an item could have come from * "vanilla" has no hash but checks for year == 1 */ export const D1ActivityHashes: { restricted: { [keyword: string]: number[] | undefined; }; required: { [keyword: string]: number[] | undefined; }; } = { required: { trials: [2650556703], // SOURCE_TRIALS_OF_OSIRIS / Trials ib: [1322283879], // SOURCE_IRON_BANNER / Iron Banner qw: [1983234046], // SOURCE_QUEENS_EMISSARY_QUEST / Queen's Wrath cd: [2775576620], // SOURCE_CRIMSON_DOUBLES / Crimson Doubles srl: [1234918199], // SOURCE_SRL / Sparrow Racing League vog: [440710167], // SOURCE_VAULT_OF_GLASS / Vault of Glass ce: [2585003248], // SOURCE_CROTAS_END / Crota's End ttk: [2659839637], // SOURCE_TTK / The Taken King kf: [1662673928], // SOURCE_KINGS_FALL / King's Fall roi: [2964550958], // SOURCE_RISE_OF_IRON / Rise of Iron wotm: [4160622434], // SOURCE_WRATH_OF_THE_MACHINE / Wrath of the Machine poe: [2784812137], // SOURCE_PRISON_ELDERS / Prison of Elders coe: [1537575125], // SOURCE_POE_ELDER_CHALLENGE / Challenge of Elders af: [3667653533], // SOURCE_ARCHONS_FORGE / Archons Forge dawning: [3131490494], // SOURCE_DAWNING / aot: [3068521220, 4161861381, 440710167], // SOURCE_AGES_OF_TRIUMPH && SOURCE_RAID_REPRISE }, restricted: { trials: [2179714245, 2682516238, 560942287], // remove xur exotics and patrol items ib: [3602080346], // remove engrams and random blue drops (Strike) qw: [3602080346], // remove engrams and random blue drops (Strike) cd: [3602080346], // remove engrams and random blue drops (Strike) kf: [2179714245, 2682516238, 560942287], // remove xur exotics and patrol items wotm: [2179714245, 2682516238, 560942287], // remove xur exotics and patrol items poe: [3602080346, 2682516238], // remove engrams coe: [3602080346, 2682516238], // remove engrams af: [2682516238], // remove engrams dawning: [2682516238, 1111209135], // remove engrams, planetary materials, & chroma aot: [2964550958, 2659839637, 353834582, 560942287], // Remove ROI, TTK, motes, & glimmer items }, }; ================================================ FILE: src/app/search/d2-known-values.ts ================================================ import { CustomStatWeights } from '@destinyitemmanager/dim-api-types'; import { DimItem } from 'app/inventory/item-types'; import { ArmorStatHashes } from 'app/loadout-builder/types'; import { invert } from 'app/utils/collections'; import { HashLookup, StringLookup } from 'app/utils/util-types'; import { DestinyClass, TierType } from 'bungie-api-ts/destiny2'; import { BreakerTypeHashes, BucketHashes, ItemCategoryHashes, PlugCategoryHashes, StatHashes, } from 'data/d2/generated-enums'; // ✨ magic values ✨ // this file has non-programatically decided information // hashes, names, & enums, hand-crafted and chosen by us export const d2MissingIcon = '/img/misc/missing_icon_d2.png'; // // GAME MECHANICS KNOWN VALUES // // In Edge of Fate, Tier 5 armor was introduced that has 11 energy instead of 10. export const maxEnergyCapacity = (item: DimItem): number => (item.tier === 5 ? 11 : 10); /** * @deprecated: use `maxEnergyCapacity` instead */ export const MAX_ARMOR_ENERGY_CAPACITY = 10; export const MASTERWORK_ARMOR_STAT_BONUS = 2; // // SOCKETS KNOWN VALUES // /** the default shader InventoryItem in every empty shader slot */ export const DEFAULT_SHADER = 4248210736; // InventoryItem "Default Shader" /** the default glow InventoryItem in every empty glow slot */ export const DEFAULT_GLOW = 3807544519; // InventoryItem "Remove Armor Glow" /** An array of default ornament hashes */ export const DEFAULT_ORNAMENTS: number[] = [ 2931483505, // InventoryItem "Default Ornament" Restores your weapon to its default appearance. 1959648454, // InventoryItem "Default Ornament" Restores your weapon to its default appearance. 702981643, // InventoryItem "Default Ornament" Restores your armor to its default appearance. 3854296178, // InventoryItem "Default Ornament" Restores your armor to its default appearance. ]; /** a weird set of 3 solstice ornaments that provide a single resilience stat point */ export const statfulOrnaments = [4245469491, 2978747767, 2287277682]; /** if a socket contains these, consider it empty */ export const emptySocketHashes = [ 2323986101, // InventoryItem "Empty Mod Socket" 2600899007, // InventoryItem "Empty Mod Socket" 1835369552, // InventoryItem "Empty Mod Socket" 3851138800, // InventoryItem "Empty Mod Socket" 791435474, // InventoryItem "Empty Activity Mod Socket" ]; export const armor2PlugCategoryHashesByName = { helmet: PlugCategoryHashes.EnhancementsV2Head, gauntlets: PlugCategoryHashes.EnhancementsV2Arms, chest: PlugCategoryHashes.EnhancementsV2Chest, leg: PlugCategoryHashes.EnhancementsV2Legs, classitem: PlugCategoryHashes.EnhancementsV2ClassItem, general: PlugCategoryHashes.EnhancementsV2General, } as const; /** The consistent armour 2 mod category hashes. This excludes raid, combat and legacy slots as they tend to change. */ export const armor2PlugCategoryHashes: number[] = Object.values(armor2PlugCategoryHashesByName); export const killTrackerObjectivesByHash: HashLookup<'pvp' | 'pve' | 'gambit'> = { 1501870536: 'pvp', // Objective "Crucible Opponents Defeated" inside 2285636663 "Crucible Tracker" 2439952408: 'pvp', // Objective "Crucible Opponents Defeated" inside 3244015567 "Crucible Tracker" 74070459: 'pvp', // Objective "Crucible Opponents Defeated" inside 38912240 "Crucible Tracker" 890482414: 'pvp', // Objective "Crucible opponents defeated" inside 1187045864 "Crucible Memento Tracker" 90275515: 'pve', // Objective "Enemies Defeated" inside 2240097604 "Kill Tracker" 2579044636: 'pve', // Objective "Enemies Defeated" inside 2302094943 "Kill Tracker" 73837075: 'pve', // Objective "Enemies Defeated" inside 905869860 "Kill Tracker" 345540971: 'gambit', // Objective "Gambit targets defeated" inside 3915764593 "Gambit Memento Tracker" 3387796140: 'pve', // Objective "Nightfall combatants defeated" inside 3915764594 "Nightfall Memento Tracker" 2109364169: 'pvp', // Objective "Trials opponents defeated" inside 3915764595 "Trials Memento Tracker" }; export const killTrackerSocketTypeHash = 1282012138; export const weaponMasterworkY2SocketTypeHash = 2218962841; export const enum GhostActivitySocketTypeHashes { /* Available once the Ghost shell has been fully Masterworked. */ Locked = 456763785, // SocketType "Activity Ghost Mod" /* Activity mods provide additional currency and material rewards in various activities. */ Unlocked = 2899644539, // SocketType "Activity Ghost Mod" } // // STATS KNOWN VALUES // /** the stat hash for DIM's artificial armor stat, "Total" */ export const TOTAL_STAT_HASH = -1000; export const CUSTOM_TOTAL_STAT_HASH = -111000; /** hashes representing D2 PL stats */ export const D2LightStats = [StatHashes.Attack, StatHashes.Defense, StatHashes.Power]; /** * Lookup with `'weapons' -> StatHashes.Weapons` etc. * * Only the 6 real armor 3.0 stats. */ export const realD2ArmorStatHashByName: StringLookup<StatHashes> = { weapons: StatHashes.Weapons, health: StatHashes.Health, class: StatHashes.Class, grenade: StatHashes.Grenade, super: StatHashes.Super, melee: StatHashes.Melee, }; /** * Lookup with `StatHashes.Weapons -> 'weapons'` etc. * * Only the 6 real armor stats. */ export const realD2ArmorStatSearchByHash = invert(realD2ArmorStatHashByName); // We keep the old names for now, both for D1 compatibility and for existing saved // searches. In the future we could have a different map for D1 names and D2 // names. const oldArmorStatNames: StringLookup<StatHashes> = { mobility: StatHashes.Weapons, resilience: StatHashes.Health, recovery: StatHashes.Class, discipline: StatHashes.Grenade, intellect: StatHashes.Super, strength: StatHashes.Melee, }; /** * Lookup with `'weapons' -> StatHashes.Weapons` etc. * * Includes keys with the old armor 2.0 stat names. */ export const D2ArmorStatHashByName: StringLookup<StatHashes> = { ...realD2ArmorStatHashByName, ...oldArmorStatNames, } as const; /** * Stats that all (D2) armor should have, ordered by how they're displayed in game. * * Only the 6 real armor stats, no aliases or synthetic stats. */ export const armorStats: ArmorStatHashes[] = [ StatHashes.Health, StatHashes.Melee, StatHashes.Grenade, StatHashes.Super, StatHashes.Class, StatHashes.Weapons, ]; // a set of base stat weights, all worth the same, "switched on" export const evenStatWeights = /* @__PURE__ */ armorStats.reduce<CustomStatWeights>( (o, statHash) => ({ ...o, [statHash]: 1 }), {}, ); export const D2WeaponStatHashByName = { rpm: StatHashes.RoundsPerMinute, rof: StatHashes.RoundsPerMinute, charge: StatHashes.ChargeTime, impact: StatHashes.Impact, handling: StatHashes.Handling, ventspeed: StatHashes.VentSpeed, heatgen: StatHashes.HeatGenerated, cooling: StatHashes.CoolingEfficiency, range: StatHashes.Range, stability: StatHashes.Stability, reload: StatHashes.ReloadSpeed, magazine: StatHashes.Magazine, aimassist: StatHashes.AimAssistance, equipspeed: StatHashes.Handling, shieldduration: StatHashes.ShieldDuration, velocity: StatHashes.Velocity, blastradius: StatHashes.BlastRadius, recoildirection: StatHashes.RecoilDirection, drawtime: StatHashes.DrawTime, zoom: StatHashes.Zoom, airborne: StatHashes.AirborneEffectiveness, accuracy: StatHashes.Accuracy, ammogen: StatHashes.AmmoGeneration, persistence: StatHashes.Persistence, swingspeed: StatHashes.SwingSpeed, guardefficiency: StatHashes.GuardEfficiency, guardresistance: StatHashes.GuardResistance, chargerate: StatHashes.ChargeRate, guardendurance: StatHashes.GuardEndurance, ammocapacity: StatHashes.AmmoCapacity, }; export const D2PlugCategoryByStatHash = new Map<StatHashes, PlugCategoryHashes>([ [StatHashes.Accuracy, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatAccuracy], [StatHashes.BlastRadius, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatBlastRadius], [StatHashes.ChargeTime, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatChargeTime], [StatHashes.Impact, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatDamage], [StatHashes.DrawTime, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatDrawTime], [StatHashes.Handling, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatHandling], [StatHashes.CoolingEfficiency, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatHeatEfficiency], [StatHashes.Persistence, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatPersistence], [StatHashes.Range, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatRange], [StatHashes.ReloadSpeed, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatReload], [StatHashes.Stability, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatStability], [StatHashes.Velocity, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatProjectileSpeed], [StatHashes.VentSpeed, PlugCategoryHashes.V400PlugsWeaponsMasterworksStatVentSpeed], [StatHashes.ShieldDuration, PlugCategoryHashes.V600PlugsWeaponsMasterworksStatShieldDuration], ]); // // ITEMS / ITEMCATERGORY KNOWN VALUES // /** D2 has these item types but D1 doesn't */ export const D2ItemCategoryHashesByName = { heavygrenadelauncher: ItemCategoryHashes.GrenadeLaunchers, specialgrenadelauncher: -ItemCategoryHashes.GrenadeLaunchers, tracerifle: ItemCategoryHashes.TraceRifles, linearfusionrifle: ItemCategoryHashes.LinearFusionRifles, submachine: ItemCategoryHashes.SubmachineGuns, bow: ItemCategoryHashes.Bows, glaive: ItemCategoryHashes.Glaives, transmat: ItemCategoryHashes.ShipModsTransmatEffects, weaponmod: ItemCategoryHashes.WeaponMods, armormod: ItemCategoryHashes.ArmorMods, reptoken: ItemCategoryHashes.ReputationTokens, }; export const pinnacleSources = [ 73143230, // InventoryItem "Pinnacle Gear" ]; /** The premium Eververse currency */ export const silverItemHash = 3147280338; // InventoryItem "Silver" // Deepsight harmonizer currency for extracting weapon patterns export const DEEPSIGHT_HARMONIZER = 2228452164; // For loadout mods obliterated from the defs, we instead return this def export const deprecatedPlaceholderArmorModHash = 444600262; // InventoryItem "Super Mod" // Weapon components, like barrels, mags, etc. // Plugs that contribute to a weapon's stats, but aren't its base stats, traits, or mods. export const weaponComponentPCHs = new Set<PlugCategoryHashes | undefined>([ PlugCategoryHashes.Bowstrings, PlugCategoryHashes.Batteries, PlugCategoryHashes.Blades, PlugCategoryHashes.Tubes, PlugCategoryHashes.Scopes, PlugCategoryHashes.Hafts, PlugCategoryHashes.Stocks, PlugCategoryHashes.Guards, PlugCategoryHashes.Barrels, PlugCategoryHashes.Arrows, PlugCategoryHashes.Grips, PlugCategoryHashes.Scopes, PlugCategoryHashes.Magazines, PlugCategoryHashes.MagazinesGl, PlugCategoryHashes.Rails, PlugCategoryHashes.Bolts, ]); // // BUCKETS KNOWN VALUES // /** * a weird bucket for holding dummies, which items show up in only temporarily. * * see https://github.com/Bungie-net/api/issues/687 */ export const THE_FORBIDDEN_BUCKET = 2422292810; /** FOTL shrouded pages end up in here, for some reason */ export const SOME_OTHER_DUMMY_BUCKET = 3621873013; // these aren't really normal equipment, // like you can have 1 equipped but it's glued to the character. // this array is used to prevent them from // having normal equipment sidecar buttons export const uniqueEquipBuckets = [ BucketHashes.SeasonalArtifact, BucketHashes.Emotes, BucketHashes.Finishers, ]; /** * Bucket for what appears to be milestone quest steps * Matches the D1 quest bucket hash */ export const MILESTONE_QUEST_BUCKET = 1801258597; // // PRESENTATION NODE KNOWN VALUES // export const RAID_NODE = 4025982223; // PresentationNode "Raids" export const SHADER_NODE = 1516796296; // PresentationNode "Shaders" export const ARMOR_NODE = 1605042242; // PresentationNode "Armor" /** Just to grab the string Universal Ornaments */ export const UNIVERSAL_ORNAMENTS_NODE = 3655910122; // PresentationNode "Universal Ornaments" /** The emblem metrics Account parent node, used as a fallback for orphaned metrics */ export const METRICS_ACCOUNT_NODE = 2875839731; // PresentationNode "Account" // // MISC KNOWN HASHES / ENUMS // export const ENCOUNTERS_COMPLETED_OBJECTIVE = 1579649637; export const ARMSMASTER_ACTIVITY_MODIFIER = 3704166961; export const RAID_ACTIVITY_TYPE_HASH = 2043403989; // milestones to manually mark as raid, because they don't adequately identify themselves in defs export const RAID_MILESTONE_HASHES = [ 2712317338, // Milestone "Garden of Salvation" has no associated activities to check for raid-ness ]; export const enum VendorHashes { Eververse = 3361454721, Benedict = 1265988377, Banshee = 672118013, Drifter = 248695599, AdaForge = 2917531897, AdaTransmog = 350061650, /** rahool. we override how his vendor FakeItems are displayed */ Rahool = 2255782930, Vault = 1037843411, Xur = 2190858386, DevrimKay = 396892126, Failsafe = 1576276905, RivensWishesExotics = 2388521577, XurLegendaryItems = 3751514131, // Vendor "Strange Gear Offers" VanguardArms = 153857624, // "Weekly: Vanguard Arms Rewards" } // See coreSettingsLoaded reducer action for details. And remove this if/when we no longer perform that hack. export const unadvertisedResettableVendors = [ 198624022, // Progression "Clan Reputation" 784742260, // Progression "Engram Ensiders" 2411069437, // Progression "Xûr Rank" ]; /** used to snag the icon for display */ export const WELL_RESTED_PERK = 1519921522; // SandboxPerk "Well-Rested" /** * Maps TierType to ItemRarityName in English and vice versa. * The Bungie.net version of this enum is not representative of real game strings. */ // A manually constructed bi-directional enum, // because the `enum` keyword unfortunately returns type `string`. export const ItemRarityMap = { Unknown: TierType.Unknown, [TierType.Unknown]: 'Unknown', Currency: TierType.Currency, [TierType.Currency]: 'Currency', Common: TierType.Basic, [TierType.Basic]: 'Common', Uncommon: TierType.Common, [TierType.Common]: 'Uncommon', Rare: TierType.Rare, [TierType.Rare]: 'Rare', Legendary: TierType.Superior, [TierType.Superior]: 'Legendary', Exotic: TierType.Exotic, [TierType.Exotic]: 'Exotic', } as const; /** * We use our own names for rarity because the API types don't match what's in * game (e.g. Legendary = TierType.Superior, Uncommon = TierType.Common). */ export type ItemRarityName = | 'Unknown' | 'Currency' | 'Common' | 'Uncommon' | 'Rare' | 'Legendary' | 'Exotic'; export const breakerTypes = { any: [BreakerTypeHashes.Stagger, BreakerTypeHashes.Disruption, BreakerTypeHashes.ShieldPiercing], antibarrier: [BreakerTypeHashes.ShieldPiercing], shieldpiercing: [BreakerTypeHashes.ShieldPiercing], barrier: [BreakerTypeHashes.ShieldPiercing], disruption: [BreakerTypeHashes.Disruption], overload: [BreakerTypeHashes.Disruption], stagger: [BreakerTypeHashes.Stagger], unstoppable: [BreakerTypeHashes.Stagger], }; export const breakerTypeNames = Object.entries(breakerTypes) .filter(([, hashes]) => hashes.length === 1) .reduce<Partial<Record<BreakerTypeHashes, string>>>((memo, [name, [hash]]) => { memo[hash] = name; return memo; }, {}); export const enum ModsWithConditionalStats { ElementalCapacitor = 3511092054, // InventoryItem "Elemental Capacitor" EnhancedElementalCapacitor = 711234314, // InventoryItem "Elemental Capacitor" EchoOfPersistence = 2272984671, // InventoryItem "Echo of Persistence" SparkOfFocus = 1727069360, // InventoryItem "Spark of Focus" BalancedTuning = 3122197216, // InventoryItem "Balanced Tuning" } export const ARTIFICE_PERK_HASH = 3727270518; // InventoryItem "Artifice Armor" // TODO: replace with d2ai? export const tuningModToTunedStathash: Record<number, StatHashes> = { 309000506: StatHashes.Grenade, 311164277: StatHashes.Melee, 323635379: StatHashes.Class, 388618952: StatHashes.Health, 455024236: StatHashes.Grenade, 534630542: StatHashes.Melee, 673231129: StatHashes.Super, 691392383: StatHashes.Weapons, 891771298: StatHashes.Weapons, 957763733: StatHashes.Class, 1510949672: StatHashes.Class, 1672416975: StatHashes.Grenade, 1879022254: StatHashes.Class, 1918710127: StatHashes.Weapons, 1922571986: StatHashes.Grenade, 2125798995: StatHashes.Health, 2244422610: StatHashes.Super, 3121760799: StatHashes.Weapons, 3284443097: StatHashes.Weapons, 3310526732: StatHashes.Health, 3554800389: StatHashes.Super, 3681082702: StatHashes.Health, 3946669007: StatHashes.Super, 4020349587: StatHashes.Melee, 4026414261: StatHashes.Super, 4030660414: StatHashes.Class, 4088823605: StatHashes.Health, 4116389173: StatHashes.Grenade, 4164883102: StatHashes.Melee, 4210715468: StatHashes.Melee, }; export const destinyClasses = [DestinyClass.Hunter, DestinyClass.Titan, DestinyClass.Warlock]; export const customStatClasses = [ DestinyClass.Hunter, DestinyClass.Titan, DestinyClass.Warlock, DestinyClass.Unknown, ]; ================================================ FILE: src/app/search/filter-description.tsx ================================================ import { t } from 'app/i18next-t'; import { FilterDescriptionInfo } from './filter-types'; // Because this is used in autocomplete.test.ts, t() can fail. // So it needs to be typed to return | undefined. function translateFilterDescription( description: FilterDescriptionInfo, ): undefined | string | [string, string][] { return typeof description === 'string' ? t(description) : Array.isArray(description) ? t(...description) : Object.entries(description).map(([keyword, i18nKey]) => [ keyword, Array.isArray(i18nKey) ? t(...i18nKey) : t(i18nKey), ]); } export function filterDescriptionText(description: FilterDescriptionInfo) { const descriptionText = translateFilterDescription(description); return ( descriptionText && (typeof descriptionText === 'string' ? descriptionText : descriptionText?.map(([keyword, desc]) => `${keyword}: ${desc}`).join('\n')) ); } export function FilterDescription({ description }: { description: FilterDescriptionInfo }) { const descriptionText = translateFilterDescription(description); return ( <> {typeof descriptionText === 'string' ? descriptionText : descriptionText?.map(([keyword, desc]) => ( <div key={keyword}> <b>{keyword}:</b> {desc} </div> ))} </> ); } ================================================ FILE: src/app/search/filter-types.ts ================================================ import { I18nKey, type t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; type I18nInput = Parameters<typeof t>; // a filter can return various bool-ish values type ValidFilterOutput = boolean | null | undefined; export type ItemFilter<I = DimItem> = (item: I) => ValidFilterOutput; /** * The syntax this filter accepts. * * `simple`: `is:[keyword]` and `not:[keyword]` * * `query`: `[keyword]:[suggestion]` * * `multiquery`: `[keyword]:[suggestion]+[suggestion]` * * `freeform`: `[keyword]:[literallyanything]` * * `range`: `[keyword]:op?([number]|[overload])` * * `stat`: `[keyword]:[suggestion]:op?[number]` */ export type FilterFormat = | 'simple' | 'query' | 'multiquery' | 'freeform' | 'range' | 'stat' | 'custom'; export function canonicalFilterFormats<I, FilterCtx, SuggestionsCtx>( format: FilterDefinition<I, FilterCtx, SuggestionsCtx>['format'], ): FilterFormat[] { if (!format) { return ['simple']; } return Array.isArray(format) ? format : [format]; } /** * The arguments to the filter creation function coming from * parsing the filter syntax. */ export interface FilterArgs { /** * the matched left-hand-side. will be `is` when using is: or not: syntax, * otherwise the matched filter name */ lhs: string; /** * the right-hand-side (or middle for stat filters). * if matching an is: filter, this is the keyword (rhs). otherwise, * this is the thing right next to the keyword */ filterValue: string; /** the generated comparator if this is a range or stat filter */ compare?: (value: number) => boolean; } /** * A definition of a filter or closely related group of filters. This is * self-contained and can be used for both autocomplete and for building up the * filter expression itself. We can also use it to drive filter help and filter * editor. */ export interface FilterDefinition<I, FilterCtx, SuggestionsCtx> { /** * One or more keywords which trigger the filter when typed into search bar. * What this means depends on what "format" this filter is. */ keywords: string | string[]; /** * A t()-compatible arg tuple or i18n key pointing to a full description of * the filter, to show in filter help */ description: FilterDescriptionInfo; /** * What kind of query this is, used to help generate suggestions. * Leave unset for `simple`, specify one, or specify multiple formats, * as long as their usage of `suggestions` doesn't clash. * `query` and `stat` use `suggestions`. */ format?: FilterFormat | FilterFormat[]; /** destinyVersion - 1 or 2, or if a filter applies to both, undefined */ destinyVersion?: 1 | 2; /** methods for retiring a filter */ deprecated?: boolean; /** * A function that is given context about the query and the world around it * (FilterContext) and should generate a simple filter function that is given * an item and returns whether that item should be included in the search. * Because this is a function that returns the item filter function, and it is * invoked once at the point where we parse the query, it can be used to * pre-process information that is needed by the actual filter function. that, * given a value from a more complex filter expression and the context about * the world around it, can generate a filter function. In that case, the * filter function will be generated once, at the point where the overall * query is parsed. */ filter: (args: FilterArgs & FilterCtx) => ItemFilter<I>; /** * A list of suggested keywords, for `query` and `stat` formats. * * These are used to validate the filter for query/multiquery formats. * If you don't like what this generates, use suggestionsGenerator instead. */ suggestions?: string[]; /** * For range-like filters, a mapping of strings to numbers (like season names or power cap aliases) */ overload?: { [key: string]: number }; /** * For stat filters, check whether this is a valid stat name or combination. */ validateStat?: (filterContext?: FilterCtx) => (stat: string) => boolean; /** * A custom function used to generate suggestions instead of default permutation generation from suggestions. * * This should only be necessary for freeform or custom formats. */ suggestionsGenerator?: ( args: SuggestionsCtx, ) => string[] | { keyword: string; ops?: string[] }[] | undefined; /** * given an item, this generates a filter that should match that item */ fromItem?: (item: I) => string; } export type FilterDescriptionInfo = I18nKey | I18nInput | Record<string, I18nKey | I18nInput>; ================================================ FILE: src/app/search/items/item-filter-types.ts ================================================ import { CustomStatDef } from '@destinyitemmanager/dim-api-types'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimLanguage } from 'app/i18n'; import { TagValue } from 'app/inventory/dim-item-info'; import { DimItem } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { Loadout } from 'app/loadout/loadout-types'; import { LoadoutsByItem } from 'app/loadout/selectors'; import { FilterDefinition } from 'app/search/filter-types'; import { FiltersMap, SearchConfig } from 'app/search/search-config'; import { Settings } from 'app/settings/initial-settings'; import { WishListRoll } from 'app/wishlists/types'; import { InventoryWishListRoll } from 'app/wishlists/wishlists'; /** * A slice of data that could be used by filter functions to * initialize some data required by particular filters. If a new filter needs * context that isn't here, add it to this interface and makeSearchFilterFactory * in search-filter.ts. */ export interface FilterContext { stores: DimStore[]; allItems: DimItem[]; currentStore: DimStore; loadoutsByItem: LoadoutsByItem; wishListFunction: (item: DimItem) => InventoryWishListRoll | undefined; wishListsByHash: Map<number, WishListRoll[]>; newItems: Set<string>; getTag: (item: DimItem) => TagValue | undefined; getNotes: (item: DimItem) => string | undefined; language: DimLanguage; customStats: Settings['customStats']; d2Definitions: D2ManifestDefinitions | undefined; } /** * this provides data so that SearchConfig can build smarter lists of suggestions. * all properties must be optional, so jest & api stuff can use SearchConfig without any context */ export interface SuggestionsContext { allItems?: DimItem[]; loadouts?: Loadout[]; getTag?: (item: DimItem) => TagValue | undefined; getNotes?: (item: DimItem) => string | undefined; d2Definitions?: D2ManifestDefinitions; allNotesHashtags?: string[]; customStats?: CustomStatDef[]; } export type ItemFilterDefinition = FilterDefinition<DimItem, FilterContext, SuggestionsContext>; export type ItemFilterMap = FiltersMap<DimItem, FilterContext, SuggestionsContext>; export type ItemSearchConfig = SearchConfig<DimItem, FilterContext, SuggestionsContext>; ================================================ FILE: src/app/search/items/item-search-filter.ts ================================================ import { CustomStatDef, DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { destinyVersionSelector } from 'app/accounts/selectors'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { customStatsSelector, languageSelector } from 'app/dim-api/selectors'; import { DimLanguage } from 'app/i18n'; import { TagValue } from 'app/inventory/dim-item-info'; import { DimItem } from 'app/inventory/item-types'; import { allItemsSelector, allNotesHashtagsSelector, currentStoreSelector, displayableBucketHashesSelector, getNotesSelector, getTagSelector, newItemsSelector, sortedStoresSelector, } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { Loadout } from 'app/loadout/loadout-types'; import { loadoutsSelector } from 'app/loadout/loadouts-selector'; import { LoadoutsByItem, loadoutsByItemSelector } from 'app/loadout/selectors'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { buildFiltersMap, buildSearchConfig } from 'app/search/search-config'; import { makeSearchFilterFactory, parseAndValidateQuery } from 'app/search/search-filter'; import { Settings } from 'app/settings/initial-settings'; import { querySelector } from 'app/shell/selectors'; import { wishListFunctionSelector, wishListsByHashSelector } from 'app/wishlists/selectors'; import { WishListRoll } from 'app/wishlists/types'; import { InventoryWishListRoll } from 'app/wishlists/wishlists'; import memoizeOne from 'memoize-one'; import { createSelector } from 'reselect'; import { FilterContext, SuggestionsContext } from './item-filter-types'; import advancedFilters from './search-filters/advanced'; import d1Filters from './search-filters/d1-filters'; import dupeFilters from './search-filters/dupes'; import freeformFilters from './search-filters/freeform'; import itemInfosFilters from './search-filters/item-infos'; import knownValuesFilters from './search-filters/known-values'; import loadoutFilters from './search-filters/loadouts'; import simpleRangeFilters from './search-filters/range-numeric'; import overloadedRangeFilters from './search-filters/range-overload'; import simpleFilters from './search-filters/simple'; import socketFilters from './search-filters/sockets'; import statFilters from './search-filters/stats'; import locationFilters from './search-filters/stores'; import wishlistFilters from './search-filters/wishlist'; const allFilters = [ ...dupeFilters, ...($featureFlags.wishLists ? wishlistFilters : []), ...freeformFilters, ...itemInfosFilters, ...knownValuesFilters, ...d1Filters, ...loadoutFilters, ...simpleRangeFilters, ...overloadedRangeFilters, ...simpleFilters, ...socketFilters, ...statFilters, ...locationFilters, ...advancedFilters, ]; export const buildItemFiltersMap = memoizeOne((destinyVersion: DestinyVersion) => buildFiltersMap(destinyVersion, allFilters), ); export function buildItemSearchConfig( destinyVersion: DestinyVersion, language: DimLanguage, suggestionsContext: SuggestionsContext = {}, ) { const filtersMap = buildItemFiltersMap(destinyVersion); return buildSearchConfig(language, suggestionsContext, filtersMap); } // // Selectors // /** * A selector for the suggestionsContext for a particular destiny version. * This must depend on every bit of data in suggestionsContext so that we * regenerate filter suggestions whenever any of them changes. */ export const suggestionsContextSelector = createSelector( allItemsSelector, loadoutsSelector, d2ManifestSelector, getTagSelector, getNotesSelector, allNotesHashtagsSelector, customStatsSelector, makeSuggestionsContext, ); function makeSuggestionsContext( allItems: DimItem[], loadouts: Loadout[], d2Definitions: D2ManifestDefinitions | undefined, getTag: (item: DimItem) => TagValue | undefined, getNotes: (item: DimItem) => string | undefined, allNotesHashtags: string[], customStats: CustomStatDef[], ): SuggestionsContext { return { allItems, loadouts, d2Definitions, getTag, getNotes, allNotesHashtags, customStats, }; } export const searchConfigSelector = createSelector( destinyVersionSelector, languageSelector, suggestionsContextSelector, buildItemSearchConfig, ); /** * A selector for the filterContext for a particular destiny version. This must * depend on every bit of data a filter might need to run, so that we regenerate the filter * functions whenever any of them changes. */ const filterContextSelector = createSelector( sortedStoresSelector, allItemsSelector, currentStoreSelector, loadoutsByItemSelector, wishListFunctionSelector, wishListsByHashSelector, newItemsSelector, getTagSelector, getNotesSelector, languageSelector, customStatsSelector, d2ManifestSelector, makeFilterContext, ); function makeFilterContext( stores: DimStore[], allItems: DimItem[], currentStore: DimStore | undefined, loadoutsByItem: LoadoutsByItem, wishListFunction: (item: DimItem) => InventoryWishListRoll | undefined, wishListsByHash: Map<number, WishListRoll[]>, newItems: Set<string>, getTag: (item: DimItem) => TagValue | undefined, getNotes: (item: DimItem) => string | undefined, language: DimLanguage, customStats: Settings['customStats'], d2Definitions: D2ManifestDefinitions | undefined, ): FilterContext { return { stores, allItems, currentStore: currentStore!, loadoutsByItem, wishListFunction, newItems, getTag, getNotes, language, customStats, wishListsByHash, d2Definitions, }; } /** * A selector for the search config for a particular destiny version. * Combines the searchConfig (list of filters), * and the filterContext (list of other stat information filters can use) * into a filter factory (for converting parsed strings into filter functions) */ export const filterFactorySelector = createSelector( searchConfigSelector, filterContextSelector, makeSearchFilterFactory<DimItem, FilterContext, SuggestionsContext>, ); /** A selector for a function for searching items, given the current search query. */ export const searchFilterSelector = createSelector( querySelector, filterFactorySelector, (query, filterFactory) => filterFactory(query), ); /** A selector for all items filtered by whatever's currently in the search box. */ export const filteredItemsSelector = createSelector( allItemsSelector, searchFilterSelector, displayableBucketHashesSelector, (allItems, searchFilter, displayableBuckets) => allItems.filter((i) => displayableBuckets.has(i.location.hash) && searchFilter(i)), ); /** A selector for a function for validating a query. */ export const validateQuerySelector = createSelector( searchConfigSelector, filterContextSelector, (searchConfig, filterContext) => (query: string) => parseAndValidateQuery(query, searchConfig.filtersMap, filterContext), ); /** Whether the current search query is valid. */ export const queryValidSelector = createSelector( querySelector, validateQuerySelector, (query, validateQuery) => validateQuery(query).valid, ); ================================================ FILE: src/app/search/items/search-filters/advanced.ts ================================================ import { tl } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { ItemFilterDefinition } from '../item-filter-types'; const advancedFilters: ItemFilterDefinition[] = [ { keywords: 'id', description: tl('Filter.ItemId'), format: 'freeform', filter: ({ filterValue }) => (item) => item.id === filterValue, }, { keywords: 'hash', description: tl('Filter.ItemHash'), format: 'freeform', filter: ({ filterValue }) => { const itemHash = parseInt(filterValue, 10); return (item: DimItem) => item.hash === itemHash; }, }, ]; export default advancedFilters; ================================================ FILE: src/app/search/items/search-filters/d1-filters.ts ================================================ import { tl } from 'app/i18next-t'; import { D1Item } from 'app/inventory/item-types'; import { boosts, D1ActivityHashes, sublimeEngrams, supplies, vendorHashes, } from 'app/search/d1-known-values'; import { FilterDefinition } from 'app/search/filter-types'; import { getItemYear } from 'app/utils/item-utils'; import { FilterContext, ItemFilterDefinition, SuggestionsContext } from '../item-filter-types'; /** * A filter that's only valid for Destiny 1 and gets to operate on D1Items instead, * to enable a safe cast to FilterDefinition. */ type D1FilterDefinition = FilterDefinition<D1Item, FilterContext, SuggestionsContext> & { destinyVersion: 1; }; // these just check an attribute found on DimItem const d1Filters: D1FilterDefinition[] = [ { keywords: 'sublime', description: tl('Filter.RarityTier'), destinyVersion: 1, filter: () => (item) => sublimeEngrams.includes(item.hash), }, { // Upgraded will show items that have enough XP to unlock all // their nodes and only need the nodes to be purchased. keywords: 'upgraded', description: [tl('Filter.Leveling.Upgraded'), { term: 'upgraded' }], destinyVersion: 1, filter: () => (item) => item.talentGrid?.xpComplete && !item.complete, }, { // Complete shows items that are fully leveled. keywords: 'complete', description: [tl('Filter.Leveling.Complete'), { term: 'complete' }], destinyVersion: 1, filter: () => (item) => item.complete, }, { // Incomplete will show items that are not fully leveled. keywords: 'incomplete', description: [tl('Filter.Leveling.Incomplete'), { term: 'incomplete' }], destinyVersion: 1, filter: () => (item) => item.talentGrid && !item.complete, }, { keywords: 'xpcomplete', description: [tl('Filter.Leveling.XPComplete'), { term: 'xpcomplete' }], destinyVersion: 1, filter: () => (item) => item.talentGrid?.xpComplete, }, { keywords: ['xpincomplete', 'needsxp'], description: [tl('Filter.Leveling.NeedsXP'), { term: 'xpincomplete/needsxp' }], destinyVersion: 1, filter: () => (item) => item.talentGrid?.xpComplete, }, { keywords: ['ascended'], description: tl('Filter.Ascended'), destinyVersion: 1, filter: () => (item) => item.talentGrid?.hasAscendNode && item.talentGrid.ascended, }, { keywords: ['unascended'], description: tl('Filter.Unascended'), destinyVersion: 1, filter: () => (item) => item.talentGrid?.hasAscendNode && !item.talentGrid.ascended, }, { keywords: ['tracked', 'untracked'], description: tl('Filter.Tracked'), destinyVersion: 1, filter: ({ filterValue }) => (item) => item.trackable && (filterValue === 'tracked' ? item.tracked : !item.tracked), }, { keywords: ['reforgeable', 'reforge', 'rerollable', 'reroll'], description: tl('Filter.Reforgeable'), destinyVersion: 1, filter: () => (item) => item.talentGrid?.nodes.some((n) => n.hash === 617082448), }, { keywords: 'engram', description: tl('Filter.Engrams'), destinyVersion: 1, filter: () => (item) => item.isEngram, }, { keywords: ['intellect', 'discipline', 'strength'], description: tl('Filter.NamedStat'), destinyVersion: 1, filter: ({ filterValue }) => (item) => item.stats?.some((s) => Boolean(s.displayProperties.name.toLowerCase() === filterValue && s.value > 0), ), }, { keywords: ['glimmeritem', 'glimmerboost', 'glimmersupply'], description: tl('Filter.Glimmer'), destinyVersion: 1, filter: ({ filterValue }) => (item) => { switch (filterValue) { case 'glimmerboost': return boosts.includes(item.hash); case 'glimmersupply': return supplies.includes(item.hash); case 'glimmeritem': return boosts.includes(item.hash) || supplies.includes(item.hash); } return false; }, }, { keywords: ['ornamentable', 'ornamentmissing', 'ornamentunlocked'], description: tl('Filter.Ornament'), destinyVersion: 1, filter: ({ filterValue }) => (item) => { const complete = item.talentGrid?.nodes.some((n) => n.ornament); const missing = item.talentGrid?.nodes.some((n) => !n.ornament); if (filterValue === 'ornamentunlocked') { return complete; } else if (filterValue === 'ornamentmissing') { return missing; } else { return complete || missing; } }, }, { keywords: ['quality', 'percentage'], description: [tl('Filter.Quality'), { percentage: 'percentage', quality: 'quality' }], format: 'range', destinyVersion: 1, filter: ({ compare }) => (item) => { if (!item.quality) { return false; } return compare!(item.quality.min); }, }, { keywords: [ 'fwc', 'do', 'nm', 'speaker', 'variks', 'shipwright', 'vanguard', 'osiris', 'xur', 'shaxx', 'cq', 'eris', 'ev', 'gunsmith', ], description: tl('Filter.Vendor'), destinyVersion: 1, filter: ({ filterValue }) => (item) => { const restricted = vendorHashes.restricted[filterValue]; const required = vendorHashes.required[filterValue]; const match = (vendorHash: number) => item.sourceHashes.includes(vendorHash); if (restricted) { return (!required || required.some(match)) && !restricted.some(match); } else { return required?.some(match); } }, }, { keywords: [ 'vanilla', 'trials', 'ib', 'qw', 'cd', 'srl', 'vog', 'ce', 'ttk', 'kf', 'roi', 'wotm', 'poe', 'coe', 'af', 'dawning', 'aot', ], description: tl('Filter.Release'), destinyVersion: 1, filter: ({ filterValue }) => (item) => { if (filterValue === 'vanilla') { return getItemYear(item) === 1; } else if (D1ActivityHashes.restricted[filterValue]) { return ( D1ActivityHashes.required[filterValue]?.some((sourceHash: number) => item.sourceHashes.includes(sourceHash), ) && !D1ActivityHashes.restricted[filterValue]?.some((sourceHash: number) => item.sourceHashes.includes(sourceHash), ) ); } else { return D1ActivityHashes.required[filterValue]?.some((sourceHash: number) => item.sourceHashes.includes(sourceHash), ); } }, }, ]; export default d1Filters as ItemFilterDefinition[]; ================================================ FILE: src/app/search/items/search-filters/d2-sources.ts ================================================ import { D2CalculatedSeason, D2SeasonInfo } from 'data/d2/d2-season-info'; import D2Sources from 'data/d2/source-info-v2'; const currentSeason = D2SeasonInfo[D2CalculatedSeason].season; // Fill in extra entries for the aliased source names for (const sourceAttrs of Object.values(D2Sources)) { if (sourceAttrs.aliases) { for (const alias of sourceAttrs.aliases) { D2Sources[alias] = sourceAttrs; } } } // Generate DCV source const dcvItemHashes = []; const dcvSourceHashes = []; for (const sourceAttrs of Object.values(D2Sources)) { if (sourceAttrs.enteredDCV && sourceAttrs.enteredDCV <= currentSeason) { if (!D2Sources.dcv) { D2Sources.dcv = { itemHashes: [], sourceHashes: [] }; } if (sourceAttrs.itemHashes) { dcvItemHashes.push(...sourceAttrs.itemHashes); } if (sourceAttrs.sourceHashes) { dcvSourceHashes.push(...sourceAttrs.sourceHashes); } } } D2Sources.dcv.itemHashes = [...new Set(dcvItemHashes)]; D2Sources.dcv.sourceHashes = [...new Set(dcvSourceHashes)]; export default D2Sources; ================================================ FILE: src/app/search/items/search-filters/data/d2/artifact-breaker-weapon-types.d.ts ================================================ declare module 'data/d2/artifact-breaker-weapon-types.json' { const x: Record< import('data/d2/generated-enums').BreakerTypeHashes, import('data/d2/generated-enums').ItemCategoryHashes[] >; export default x; } ================================================ FILE: src/app/search/items/search-filters/dupes-deprecated.ts ================================================ import { TagValue } from '@destinyitemmanager/dim-api-types'; import { tl } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { armorStats, DEFAULT_SHADER, TOTAL_STAT_HASH } from 'app/search/d2-known-values'; import { filterMap } from 'app/utils/collections'; import { chainComparator, compareBy, reverseComparator } from 'app/utils/comparators'; import { getArmor3TuningStat, isArtifice } from 'app/utils/item-utils'; import { collectRelevantStatHashes, computeStatDupeLower } from 'app/utils/stats'; import { BucketHashes } from 'data/d2/generated-enums'; import { ItemFilterDefinition } from '../item-filter-types'; import { computeDupes, itemDupeID } from './dupes'; import { PerksSet } from './perks-set'; const notableTags = ['favorite', 'keep']; const sortDupes = ( dupes: { [dupeID: string]: DimItem[]; }, getTag: (item: DimItem) => TagValue | undefined, ) => { // The comparator for sorting dupes - the first item will be the "best" and all others are "dupelower". const dupeComparator = reverseComparator( chainComparator<DimItem>( // primary stat compareBy((item) => item.power), compareBy((item) => { const tag = getTag(item); return Boolean(tag && notableTags.includes(tag)); }), compareBy((item) => item.masterwork), compareBy((item) => item.locked), compareBy((i) => i.id), // tiebreak by ID ), ); for (const dupeList of Object.values(dupes)) { if (dupeList.length > 1) { dupeList.sort(dupeComparator); } } return dupes; }; export const deprecatedDupeFilters: ItemFilterDefinition[] = [ { keywords: 'dupelower', description: tl('Filter.DupeLower'), deprecated: true, filter: ({ allItems, getTag }) => { const duplicates = sortDupes(computeDupes(allItems), getTag); return (item) => { if (!(item.bucket && (item.bucket.inWeapons || item.bucket.inArmor) && !item.notransfer)) { return false; } const dupeId = itemDupeID(item); const dupes = duplicates[dupeId]; if (dupes?.length > 1) { const bestDupe = dupes[0]; return item !== bestDupe; } return false; }; }, }, { keywords: 'infusionfodder', description: tl('Filter.InfusionFodder'), destinyVersion: 2, filter: ({ allItems }) => { const duplicates: { [dupeID: string]: DimItem[] } = {}; for (const i of allItems.filter((i) => i.infusionFuel)) { if (!i.comparable) { continue; } const dupeID = i.hash.toString(); if (!duplicates[dupeID]) { duplicates[dupeID] = []; } duplicates[dupeID].push(i); } return (item) => { if (!item.infusionFuel) { return false; } return duplicates[item.hash.toString()]?.some((i) => i.power < item.power); }; }, }, { keywords: 'statlower', deprecated: true, description: tl('Filter.StatLower'), filter: ({ allItems }) => { const duplicates = computeStatDupeLower(allItems); return (item) => item.bucket.inArmor && duplicates.has(item.id); }, }, { keywords: 'customstatlower', deprecated: true, description: tl('Filter.CustomStatLower'), filter: ({ allItems, customStats }) => { const duplicateSetsByClass: Partial<Record<DimItem['classType'], Set<string>[]>> = {}; for (const customStat of customStats) { const relevantStatHashes = collectRelevantStatHashes(customStat.weights); (duplicateSetsByClass[customStat.class] ||= []).push( computeStatDupeLower(allItems, relevantStatHashes), ); } return (item) => item.bucket.inArmor && // highlight the item if it's statlower for all class-relevant custom stats. // this duplicates existing behavior for old style default-named custom stat, // but should be extended to also be a stat name-based filter // for users with multiple stats per class, a la customstatlower:pve duplicateSetsByClass[item.classType]?.every((dupeSet) => dupeSet.has(item.id)); }, }, { keywords: ['crafteddupe', 'shapeddupe'], deprecated: true, description: tl('Filter.CraftedDupe'), filter: ({ allItems }) => { const duplicates = computeDupes(allItems); return (item) => { const dupeId = itemDupeID(item); if (!checkIfIsDupe(duplicates, dupeId, item)) { return false; } const itemDupes = duplicates?.[dupeId]; return itemDupes?.some((d) => d.crafted); }; }, }, { keywords: ['dupeperks'], deprecated: true, description: tl('Filter.DupePerks'), filter: ({ allItems }) => { const duplicates = new Map<string, PerksSet>(); function getDupeId(item: DimItem) { // Don't compare across buckets or across types (e.g. Titan armor vs Hunter armor) return `${item.bucket.hash}|${item.classType}`; } for (const i of allItems) { if (i.sockets?.allSockets.some((s) => s.isPerk && s.socketDefinition.defaultVisible)) { const dupeId = getDupeId(i); if (!duplicates.has(dupeId)) { duplicates.set(dupeId, new PerksSet()); } duplicates.get(dupeId)!.insert(i); } } return (item) => item.sockets?.allSockets.some((s) => s.isPerk && s.socketDefinition.defaultVisible) && Boolean(duplicates.get(getDupeId(item))?.hasPerkDupes(item)); }, }, { keywords: ['statdupe'], description: tl('Filter.DupeStats'), deprecated: true, destinyVersion: 2, filter: ({ allItems }) => { const collector: Record<string, string[]> = {}; for (const item of allItems) { if ( // This filter is for armor with stats !item.bucket.inArmor || !item.stats || // Special cases for class items (item.bucket.hash === BucketHashes.ClassArmor && // Older class items have no stats. (item.stats.find((s) => s.statHash === TOTAL_STAT_HASH)?.base === 0 || // Exotic class items all have identical stats. item.rarity === 'Exotic')) ) { continue; } // *Stat-related* reasons an item's stats might not be directly comparable to another item's. const statExceptionKey = isArtifice(item) ? 'artifice' : getArmor3TuningStat(item); const statValues = filterMap(item.stats, (s) => { if (armorStats.includes(s.statHash)) { return s.base; } }); const key = `${statExceptionKey}|${item.classType}|${item.bucket.name}|${statValues.join()}`; (collector[key] ??= []).push(item.id); } const dupes = new Set<string>(); for (const key in collector) { const items = collector[key]; if (items.length > 1) { for (const id of items) { dupes.add(id); } } } return (item) => dupes.has(item.id); }, }, ]; export function checkIfIsDupe( duplicates: { [dupeID: string]: DimItem[]; }, dupeId: string, item: DimItem, ) { return ( duplicates[dupeId]?.length > 1 && item.hash !== DEFAULT_SHADER && item.bucket.hash !== BucketHashes.SeasonalArtifact ); } ================================================ FILE: src/app/search/items/search-filters/dupes.ts ================================================ import { stripAdept } from 'app/compare/compare-utils'; import { tl } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { DEFAULT_SHADER, TOTAL_STAT_HASH, armorStats, customStatClasses, destinyClasses, } from 'app/search/d2-known-values'; import { filterMap, isEmpty } from 'app/utils/collections'; import { compareBy } from 'app/utils/comparators'; import { getArmor3StatFocus, getArmor3TuningStat, isArmor3, isArtifice, } from 'app/utils/item-utils'; import { getArmorArchetypeSocket } from 'app/utils/socket-utils'; import { collectRelevantStatHashes, computeStatDupeLower } from 'app/utils/stats'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { ItemFilterDefinition } from '../item-filter-types'; import { deprecatedDupeFilters } from './dupes-deprecated'; import { PerksSet } from './perks-set'; type ItemFilterContext = Parameters<ItemFilterDefinition['filter']>[0]; // Return undefined from a key generator if that item does not belong in this dupe comparison at all. const dupeTypeLookupRaw: Record< string, { keyGenerator: (item: DimItem) => string | undefined; confirmItemsInGroup?: (items: DimItem[], context: ItemFilterContext) => DimItem[]; } > = { // Simple, classic is:dupe behavior. Finds items that look like each other, and groups them together. item: { keyGenerator: itemDupeID }, // Groups items by their base stats. // Considers two pieces with the same stats, but different tuner mods (or one item has artifice/tuner) to be different. stats: { keyGenerator: (item) => { if (isStatRelevantArmor(item)) { // *Stat-related* reasons an item's stats might not be directly comparable to another item's. const statExceptionKey = isArtifice(item) ? 'artifice' : getArmor3TuningStat(item); const statValues = filterMap(item.stats!, (s) => { if (armorStats.includes(s.statHash)) { return `#${s.statHash}:${s.base}#`; } }); return `${statExceptionKey}|${item.classType}|${item.bucket.name}|${item.isExotic}|${statValues.join()}`; } }, }, // This is a bad filter but the customer is always right. // Uses base stats, and ignores the fact that T5 tuner mods exist. untunedstats: { keyGenerator: (item) => { if (isStatRelevantArmor(item)) { const statValues = filterMap(item.stats!, (s) => { if (armorStats.includes(s.statHash)) { return `#${s.statHash}:${s.base}#`; } }); return `${item.classType}|${item.bucket.name}|${item.isExotic}|${statValues.join()}`; } }, }, // Groups items by their archetype. archetype: { keyGenerator: (item) => { const archetype = getArmorArchetypeSocket(item); if (archetype) { return `${item.classType}|${item.bucket.name}|${item.isExotic}|${archetype.plugged?.plugDef.hash}`; } }, }, // Groups items by their tertiary stat (the non-zero one on armor 3.0 that isn't controlled by archetype). tertiarystat: { keyGenerator: (item) => { if (isArmor3(item) && isStatRelevantArmor(item)) { const tertiaryStatHash = getArmor3StatFocus(item)[2]; return `${item.classType}|${item.bucket.name}|${item.isExotic}|${tertiaryStatHash}`; } }, }, // Groups armor 3.0 by the trio of stats they have a non-zero base stat in. nonzerostats: { keyGenerator: (item) => { if (isArmor3(item) && isStatRelevantArmor(item)) { const nonZeroStats = getArmor3StatFocus(item).sort(); return `${item.classType}|${item.bucket.name}|${item.isExotic}|${nonZeroStats.join()}`; } }, }, // Groups items by their tuned stat, a controllable +5 on Tier 5 armor 3.0. tunedstat: { keyGenerator: (item) => { if (isArmor3(item) && isStatRelevantArmor(item)) { const tertiaryStatHash = getArmor3StatFocus(item)[2]; return `${item.classType}|${item.bucket.name}|${item.isExotic}|${tertiaryStatHash}`; } }, }, // Finds items with the same set bonus. Not useful in isolation, but good for narrowing statlower computations. setbonus: { keyGenerator: (item) => { if (item.setBonus) { return `${item.classType}|${item.bucket.name}|${item.isExotic}|${item.setBonus.hash}`; } }, }, // Finds items with all the same perks or which contain a subset that comprises another item's perks. perks: { keyGenerator: (item) => `${item.bucket.hash}|${item.classType}|${item.isExotic}|${item.ammoType}|${item.typeName}`, confirmItemsInGroup: (items) => { items = items.filter((i) => i.sockets?.allSockets.some((s) => s.isPerk && s.socketDefinition.defaultVisible), ); const perksSet = new PerksSet(items); return items.filter((i) => perksSet.hasPerkDupes(i)); }, }, // Finds items with all the same traits or which contain a subset that comprises another item's traits. traits: { keyGenerator: (item) => item.bucket.inWeapons ? `${item.bucket.hash}|${item.classType}|${item.isExotic}|${item.ammoType}|${item.typeName}` : undefined, confirmItemsInGroup: (items) => { items = items.filter((i) => i.sockets?.allSockets.some((s) => s.isPerk && s.socketDefinition.defaultVisible), ); const perksSet = new PerksSet(items, 'traits'); return items.filter((i) => perksSet.hasPerkDupes(i)); }, }, // "LOWER" CALCULATORS. // These do little key-based dupe-ness narrowing of their own (except narrowing items to armor, currently). // They process last in line among dupe filters, after other dupe types have grouped items, // allowing "lowest within matching set bonus" or "lowest among armor with the same archetype" statlower: { keyGenerator: (item) => { if (isStatRelevantArmor(item)) { // computeStatDupeLower already computes items by bucket. No need to partition here. return ''; } }, // Run lower computer against each group to decide which are lower. confirmItemsInGroup: (items) => { const lowerIds = computeStatDupeLower(items); return items.filter((i) => lowerIds.has(i.id)); }, }, // Currently checks to see if EVERY STAT on a piece, within EVERY CUSTOM STAT TOTAL, is beat by every stat // on some other piece in the same slot for the same guardian class. // Every additional custom stat will narrow this filter's result. customstatlower: { keyGenerator: (item) => { if (isStatRelevantArmor(item)) { // computeStatDupeLower already computes items by bucket. No need to partition here. return ''; } }, // Run lower computer against each group to decide which are lower. confirmItemsInGroup: (allArmors, { customStats }) => { const armorsByDestinyClass = Map.groupBy(allArmors, (i) => i.classType); // Empty the itemset for classes with no applicable custom stat. const existingCustomStatClasses = Array.from(new Set(customStats.map((s) => s.class))); for (const destinyClass of armorsByDestinyClass.keys()) { if ( !existingCustomStatClasses.includes(DestinyClass.Unknown) && !existingCustomStatClasses.includes(destinyClass) ) { armorsByDestinyClass.set(destinyClass, []); } } // Fill each class with an empty array so we can assert! they are found later. for (const destinyClass of customStatClasses) { if (!armorsByDestinyClass.has(destinyClass)) { armorsByDestinyClass.set(destinyClass, []); } } for (const customStat of customStats) { const thisStatClassArmors = customStat.class === DestinyClass.Unknown ? allArmors : armorsByDestinyClass.get(customStat.class)!; const relevantStatHashes = collectRelevantStatHashes(customStat.weights); const thisStatLowerIds = computeStatDupeLower(thisStatClassArmors, relevantStatHashes); const armorSetsToNarrow = customStat.class === DestinyClass.Unknown ? destinyClasses : [customStat.class]; for (const destinyClass of armorSetsToNarrow) { armorsByDestinyClass.set( destinyClass, armorsByDestinyClass.get(destinyClass)!.filter((i) => thisStatLowerIds.has(i.id)), ); } } return destinyClasses.flatMap((destinyClass) => armorsByDestinyClass.get(destinyClass)!); }, }, }; const lowerComparatorTypes = ['statlower', 'customstatlower']; const dupeFilters: ItemFilterDefinition[] = [ { keywords: ['dupe'], format: ['multiquery', 'simple'], suggestions: Object.keys(dupeTypeLookupRaw), suggestionsGenerator: () => [ 'is:dupe', 'dupe:stats', 'dupe:archetype+tertiarystat', 'dupe:nonzerostats', 'dupe:setbonus+statlower', 'dupe:traits', 'dupe:statlower', 'dupe:customstatlower', ], description: { item: tl('Filter.Dupe'), stats: tl('Filter.DupeStats'), untunedstats: tl('Filter.DupeUntunedStats'), archetype: tl('Filter.DupeArchetype'), tertiarystat: tl('Filter.DupeTertiary'), nonzerostats: tl('Filter.DupeZeroStats'), tunedstat: tl('Filter.DupeTunedStat'), setbonus: tl('Filter.DupeSetBonus'), perks: tl('Filter.DupePerks'), traits: tl('Filter.DupeTraits'), statlower: tl('Filter.StatLower'), customstatlower: tl('Filter.CustomStatLower'), }, destinyVersion: 2, filter: (context) => { const { allItems } = context; let { filterValue } = context; if (filterValue === 'dupe') { filterValue = 'item'; } const dupeTypes = filterValue.split('+'); const invalidDupeTypes = dupeTypes.filter((t) => !(t in dupeTypeLookupRaw)); if (invalidDupeTypes.length) { throw new Error(`Invalid dupe identifiers: ${invalidDupeTypes.join()}`); } // Run "lower" measurers last, so their confirmItemsInGroup functions are run against a reduced subset of items dupeTypes.sort(compareBy((t) => lowerComparatorTypes.includes(t))); const collector: Record<string, DimItem[]> = {}; itemLoop: for (const item of allItems) { const keys = []; for (const dupeType of dupeTypes) { const key = dupeTypeLookupRaw[dupeType].keyGenerator(item); if (key === undefined) { continue itemLoop; } keys.push(key); } if (keys.length) { // Check shouldn't be strictly necessary but it helps ensure no surprises. (collector[keys.join('%%')] ??= []).push(item); } } if (isEmpty(collector)) { throw new Error(`No items passed the inclusion conditions of all filters: ${filterValue}`); } const dupes = new Set<string>(); for (const key in collector) { let items = collector[key]; for (const dupeType of dupeTypes) { const confirmFunction = dupeTypeLookupRaw[dupeType].confirmItemsInGroup; if (confirmFunction) { items = confirmFunction(items, context); } } if ( // If "lower" measurers were involved, anything that has passed the confirmItemsInGroup is a "lower" and should be highlighted. dupeTypes.some((t) => lowerComparatorTypes.includes(t)) || // Otherwise, items have been sorted into unique identifier arrays, and if an array has more than one item, // then they had the same unique identifier and should be marked dupes. items.length > 1 ) { for (const item of items) { dupes.add(item.id); } } } return (item) => // I do not know why this is necessary, how is a DEFAULT_SHADER being dupe checked? item.hash !== DEFAULT_SHADER && item.bucket.hash !== BucketHashes.SeasonalArtifact && item.comparable && dupes.has(item.id); }, }, { keywords: 'count', description: tl('Filter.DupeCount'), format: 'range', filter: ({ compare, allItems }) => { const duplicates = computeDupes(allItems); return (item) => { const dupeId = itemDupeID(item); return compare!(duplicates[dupeId]?.length ?? 0); }; }, }, ...deprecatedDupeFilters, ]; export default dupeFilters; function isStatRelevantArmor(item: DimItem) { return ( // This filter is for armor with stats item.bucket.inArmor && item.stats && // Allow all non-class items (item.bucket.hash !== BucketHashes.ClassArmor || // Older class items have no base stats. Allow if there's base stats. (item.stats.find((s) => s.statHash === TOTAL_STAT_HASH)?.base !== 0 && // Exotic class items all have identical stats. Allow others. item.rarity !== 'Exotic')) ); } /** outputs a string combination of the identifying features of an item, or the hash if classified */ export function itemDupeID(item: DimItem) { return ( (item.classified && `${item.hash}`) || `${ // Consider adept versions of weapons to be the same as the normal type item.bucket.inWeapons ? stripAdept(item.name) : item.name }${ // Some items have the same name across different classes, e.g. "Kairos Function Boots" item.classType }${ // Some items have the same name across different tiers, e.g. "Traveler's Chosen" item.rarity }${ // The engram that dispenses the Taraxippos scout rifle is also called Taraxippos item.bucket.hash }` ); } export function computeDupes(allItems: DimItem[]) { // Holds a map from item hash to count of occurrences of that hash const duplicates: { [dupeID: string]: DimItem[] } = {}; for (const i of allItems) { if (!i.comparable) { continue; } const dupeID = itemDupeID(i); if (!duplicates[dupeID]) { duplicates[dupeID] = []; } duplicates[dupeID].push(i); } return duplicates; } ================================================ FILE: src/app/search/items/search-filters/freeform.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { tl } from 'app/i18next-t'; import { DimItem, DimPlug } from 'app/inventory/item-types'; import { quoteFilterString } from 'app/search/query-parser'; import { matchText, plainString, startWordRegexp, testStringsFromDisplayProperties, testStringsFromDisplayPropertiesMap, } from 'app/search/text-utils'; import { filterMap } from 'app/utils/collections'; import { isD1Item } from 'app/utils/item-utils'; import { DestinyInventoryItemDefinition, TierType } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes, PlugCategoryHashes } from 'data/d2/generated-enums'; import memoizeOne from 'memoize-one'; import { ItemFilterDefinition } from '../item-filter-types'; const interestingPlugTypes = new Set([PlugCategoryHashes.Frames, PlugCategoryHashes.Intrinsics]); const getPerkNamesFromManifest = memoizeOne( (allItems: { [hash: number]: DestinyInventoryItemDefinition }) => filterMap(Object.values(allItems), (item) => { const pch = item.plug?.plugCategoryHash; return pch && interestingPlugTypes.has(pch) ? item.displayProperties.name.toLowerCase() || undefined : undefined; }), ); const getUniqueItemNamesFromManifest = memoizeOne( (allManifestItems: { [hash: number]: DestinyInventoryItemDefinition }) => { const itemNames = new Set<string>(); for (const h in allManifestItems) { const item = allManifestItems[h]; if (!item.itemCategoryHashes || !item.displayProperties.name) { continue; } const isArmor = item.itemCategoryHashes.includes(ItemCategoryHashes.Armor); // there's annoying white armors named stuff like "Gauntlets" that distract from things like is:gauntlets if (isArmor && item.inventory!.tierType === TierType.Basic) { continue; } if (isArmor || item.itemCategoryHashes.includes(ItemCategoryHashes.Weapon)) { itemNames.add(item.displayProperties.name); } } return itemNames; }, ); function itemNameToExactSearch(str: string) { return `exactname:${quoteFilterString(str.toLowerCase())}`; } const nameFilter = { keywords: ['name', 'exactname'], description: tl('Filter.Name'), format: 'freeform', suggestionsGenerator: ({ d2Definitions, allItems }) => { if (d2Definitions && allItems) { const itemNames = new Set<string>(); // Prioritize items the user actually owns, by adding them first in the list for (const i of allItems) { if (i.bucket.inWeapons || i.bucket.inArmor || i.bucket.inGeneral || i.bucket.inInventory) { itemNames.add(i.name); } } for (const n of getUniqueItemNamesFromManifest(d2Definitions.InventoryItem.getAll())) { itemNames.add(n); } return Array.from(itemNames, itemNameToExactSearch); } }, filter: ({ filterValue, language, lhs }) => { const test = matchText(filterValue, language, /* exact */ lhs === 'exactname'); return (item) => test(item.name); }, fromItem: (item) => itemNameToExactSearch(item.name), } satisfies ItemFilterDefinition; const freeformFilters: ItemFilterDefinition[] = [ nameFilter, { keywords: 'notes', description: tl('Filter.Notes'), format: 'freeform', suggestionsGenerator: ({ allNotesHashtags }) => ['notes:', ...(allNotesHashtags || [])], filter: ({ filterValue, getNotes, language }) => { filterValue = plainString(filterValue, language); return (item) => { const notes = getNotes(item); return Boolean(notes && plainString(notes, language).includes(filterValue)); }; }, }, { keywords: 'description', description: tl('Filter.DescriptionFilter'), format: 'freeform', filter: ({ filterValue, language }) => { filterValue = plainString(filterValue, language); return (item) => plainString(item.description, language).includes(filterValue); }, }, { keywords: 'perk', description: tl('Filter.Perk'), format: 'freeform', filter: ({ filterValue, language, d2Definitions }) => { const startWord = startWordRegexp(plainString(filterValue, language), language); const test = (s: string) => startWord.test(plainString(s, language)); return (item) => (isD1Item(item) && item.talentGrid && testStringsFromDisplayPropertiesMap(test, item.talentGrid?.nodes)) || (item.sockets && testStringsFromAllSockets(test, item, d2Definitions)) || testStringsFromSetBonuses(test, item, d2Definitions); }, }, { keywords: ['perkname', 'exactperk'], description: tl('Filter.PerkName'), format: 'freeform', suggestionsGenerator: ({ d2Definitions, allItems }) => { if (d2Definitions && allItems) { const perkNames = new Set<string>(); // favor items we actually own by inserting them first for (const item of allItems) { if ( item.sockets && (item.bucket.inWeapons || item.bucket.inArmor || item.bucket.inGeneral) ) { for (const socket of item.sockets.allSockets) { if (socket.isPerk) { for (const plug of socket.plugOptions) { perkNames.add(plug.plugDef.displayProperties.name.toLowerCase()); } } } } } // supplement the list with perks from definitions, so people can search things they don't own for (const perkName of getPerkNamesFromManifest(d2Definitions.InventoryItem.getAll())) { perkNames.add(perkName); } for (const setBonus of Object.values(d2Definitions.EquipableItemSet.getAll())) { perkNames.add(setBonus.displayProperties.name.toLowerCase()); for (const perk of setBonus.setPerks) { perkNames.add( d2Definitions.SandboxPerk.get( perk.sandboxPerkHash, ).displayProperties.name.toLowerCase(), ); } } return Array.from(perkNames, (s) => `exactperk:${quoteFilterString(s)}`); } }, filter: ({ lhs, filterValue, language, d2Definitions }) => { const test = matchText(filterValue, language, /* exact */ lhs === 'exactperk'); return (item) => (isD1Item(item) && testStringsFromDisplayPropertiesMap(test, item.talentGrid?.nodes, false)) || testStringsFromAllSockets(test, item, d2Definitions, /* includeDescription */ false) || testStringsFromSetBonuses(test, item, d2Definitions, /* includeDescription */ false); }, }, { keywords: 'keyword', description: tl('Filter.PartialMatch'), format: 'freeform', filter: ({ filterValue, getNotes, language, d2Definitions }) => { filterValue = plainString(filterValue, language); const test = (s: string) => plainString(s, language).includes(filterValue); return (item) => { const notes = getNotes(item); return ( (notes && test(notes)) || test(item.name) || test(item.description) || test(item.typeName) || (isD1Item(item) && testStringsFromDisplayPropertiesMap(test, item.talentGrid?.nodes)) || testStringsFromAllSockets(test, item, d2Definitions) || testStringsFromSetBonuses(test, item, d2Definitions) || (d2Definitions && (testStringsFromObjectives(test, d2Definitions, item.objectives) || testStringsFromRewards(test, d2Definitions, item.pursuit))) ); }; }, }, ]; export default freeformFilters; function testStringsFromObjectives( test: (str: string) => boolean, defs: D2ManifestDefinitions, objectives: DimItem['objectives'], ): boolean { return Boolean( objectives?.some((o) => test(defs.Objective.get(o.objectiveHash)?.progressDescription ?? '')), ); } function testStringsFromRewards( test: (str: string) => boolean, defs: D2ManifestDefinitions, pursuitInfo: DimItem['pursuit'], ): boolean { return Boolean( pursuitInfo?.rewards.some((r) => testStringsFromDisplayProperties(test, defs.InventoryItem.get(r.itemHash).displayProperties), ), ); } /** includes name and description unless you set the arg2 flag */ function testStringsFromAllSockets( test: (str: string) => boolean, item: DimItem, defs: D2ManifestDefinitions | undefined, includeDescription = true, ): boolean { if (!item.sockets) { return false; } for (const socket of item.sockets.allSockets) { for (const plug of socket.plugOptions) { if ( testStringsFromDisplayPropertiesMap( test, plug.plugDef.displayProperties, includeDescription, ) || (includeDescription && test(plug.plugDef.itemTypeDisplayName)) || (defs && getPlugPerks(plug, defs).some((perk) => testStringsFromDisplayPropertiesMap(test, perk.displayProperties, includeDescription), )) ) { return true; } } // include tooltips from the plugged item if (socket.plugged?.plugDef.tooltipNotifications) { for (const t of socket.plugged.plugDef.tooltipNotifications) { if (test(t.displayString)) { return true; } } } } return false; } function testStringsFromSetBonuses( test: (str: string) => boolean, item: DimItem, defs: D2ManifestDefinitions | undefined, includeDescription = true, ): boolean { if (!item.setBonus || !defs) { return false; } if (testStringsFromDisplayPropertiesMap(test, item.setBonus.displayProperties)) { return true; } for (const bonus of item.setBonus.setPerks) { const perkDef = defs.SandboxPerk.get(bonus.sandboxPerkHash); if (testStringsFromDisplayPropertiesMap(test, perkDef.displayProperties, includeDescription)) { return true; } } return false; } function getPlugPerks(plug: DimPlug, defs: D2ManifestDefinitions) { return (plug.plugDef.perks || []).map((perk) => defs.SandboxPerk.get(perk.perkHash)); } ================================================ FILE: src/app/search/items/search-filters/item-infos.ts ================================================ import { tl } from 'app/i18next-t'; import { itemTagSelectorList } from 'app/inventory/dim-item-info'; import { ItemFilterDefinition } from '../item-filter-types'; // check item tags or presence of notes const itemInfosFilters: ItemFilterDefinition[] = [ { keywords: 'tagged', description: tl('Filter.Tags.Tagged'), filter: ({ getTag }) => (item) => getTag(item) !== undefined, }, { keywords: 'tag', description: tl('Filter.Tags.Tag'), format: 'query', suggestions: itemTagSelectorList.map((tag) => tag.type ?? 'none'), filter: ({ filterValue, getTag }) => (item) => item.taggable && (getTag(item) || 'none') === filterValue, }, { keywords: 'hasnotes', description: tl('Filter.HasNotes'), filter: ({ getNotes }) => (item) => Boolean(getNotes(item)), }, ]; export default itemInfosFilters; ================================================ FILE: src/app/search/items/search-filters/known-values.ts ================================================ import { D1Categories } from 'app/destiny1/d1-bucket-categories'; import { D2Categories } from 'app/destiny2/d2-bucket-categories'; import { tl } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { getEvent } from 'app/inventory/store/season'; import { D1BucketHashes, D1ItemCategoryHashes } from 'app/search/d1-known-values'; import { D2ItemCategoryHashesByName, ItemRarityName, breakerTypes, pinnacleSources, } from 'app/search/d2-known-values'; import { cosmeticTypes, damageTypeNames } from 'app/search/search-filter-values'; import { getItemDamageShortName } from 'app/utils/item-utils'; import { LookupTable } from 'app/utils/util-types'; import { DamageType, DestinyAmmunitionType, DestinyClass, DestinyRecordState, } from 'bungie-api-ts/destiny2'; import artifactBreakerMods from 'data/d2/artifact-breaker-weapon-types.json'; import { D2EventEnum, D2EventInfo } from 'data/d2/d2-event-info-v2'; import focusingOutputs from 'data/d2/focusing-item-outputs.json'; import { BreakerTypeHashes, BucketHashes, ItemCategoryHashes } from 'data/d2/generated-enums'; import powerfulSources from 'data/d2/powerful-rewards.json'; import { ItemFilterDefinition } from '../item-filter-types'; import D2Sources from './d2-sources'; const D2EventPredicateLookup = Object.fromEntries( Object.entries(D2EventInfo).map(([index, event]) => [ event.shortname, Number(index) as D2EventEnum, ]), ); // filters relying on curated known values (class names, rarities, elements) /** Alternate search names for rarity tiers. */ const rarityMap: NodeJS.Dict<ItemRarityName> = { white: 'Common', green: 'Uncommon', blue: 'Rare', purple: 'Legendary', yellow: 'Exotic', common: 'Common', uncommon: 'Uncommon', rare: 'Rare', legendary: 'Legendary', exotic: 'Exotic', }; export const d2AmmoTypes = { primary: DestinyAmmunitionType.Primary, special: DestinyAmmunitionType.Special, heavy: DestinyAmmunitionType.Heavy, }; const classes = ['titan', 'hunter', 'warlock']; const itemCategoryHashesByName: { [key: string]: number } = { ...D1ItemCategoryHashes, ...D2ItemCategoryHashesByName, }; // Some common aliases for item categories const itemCategoryAliases: LookupTable<string, string> = { lfr: 'linearfusionrifle', lmg: 'machinegun', smg: 'submachine', } as const; export const damageFilter = { keywords: damageTypeNames, description: tl('Filter.DamageType'), filter: ({ filterValue }) => (item) => getItemDamageShortName(item) === filterValue, fromItem: (item) => `is:${getItemDamageShortName(item)}`, } satisfies ItemFilterDefinition; const prismaticDamageLookupTable: { [key in DamageType]: string | undefined } = { [DamageType.None]: undefined, [DamageType.Kinetic]: undefined, [DamageType.Arc]: 'light', [DamageType.Thermal]: 'light', [DamageType.Void]: 'light', [DamageType.Raid]: undefined, [DamageType.Stasis]: 'dark', [DamageType.Strand]: 'dark', }; const prismaticDamageFilter = { keywords: ['light', 'dark'], description: tl('Filter.PrismaticDamageType'), filter: ({ filterValue }) => (item) => { const damageType = item.element?.enumValue ?? DamageType.None; return prismaticDamageLookupTable[damageType] === filterValue; }, } satisfies ItemFilterDefinition; export const classFilter = { keywords: ['titan', 'hunter', 'warlock'], description: tl('Filter.Class'), filter: ({ filterValue }) => { const classType = classes.indexOf(filterValue); return (item) => !item.classified && item.classType === classType; }, fromItem: (item) => item.classType === DestinyClass.Unknown ? '' : `is:${classes[item.classType]}`, } satisfies ItemFilterDefinition; // A mapping from the bucket hash to DIM item types const bucketToType: LookupTable<BucketHashes, string> = { [BucketHashes.Engrams]: 'engrams', [BucketHashes.LostItems]: 'lostitems', [BucketHashes.Messages]: 'messages', [BucketHashes.SpecialOrders]: 'specialorders', [BucketHashes.KineticWeapons]: 'kineticslot', [BucketHashes.EnergyWeapons]: 'energy', [BucketHashes.PowerWeapons]: 'power', [BucketHashes.Helmet]: 'helmet', [BucketHashes.Gauntlets]: 'gauntlets', [BucketHashes.ChestArmor]: 'chest', [BucketHashes.LegArmor]: 'leg', [BucketHashes.ClassArmor]: 'classitem', [BucketHashes.Subclass]: 'subclass', [BucketHashes.Ghost]: 'ghost', [BucketHashes.Emblems]: 'emblems', [BucketHashes.Ships]: 'ships', [BucketHashes.Vehicle]: 'vehicle', [BucketHashes.Emotes]: 'emotes', [BucketHashes.Finishers]: 'finishers', [BucketHashes.SeasonalArtifact]: 'seasonalartifacts', [BucketHashes.Accessories]: 'accessories', [BucketHashes.Consumables]: 'consumables', [BucketHashes.Modifications]: 'modifications', }; const d1BucketToType: LookupTable<BucketHashes | D1BucketHashes, string> = { [BucketHashes.LostItems]: 'lostitems', [BucketHashes.SpecialOrders]: 'specialorders', [BucketHashes.Messages]: 'messages', [BucketHashes.KineticWeapons]: 'primary', [BucketHashes.EnergyWeapons]: 'special', [BucketHashes.PowerWeapons]: 'heavy', [BucketHashes.Helmet]: 'helmet', [BucketHashes.Gauntlets]: 'gauntlets', [BucketHashes.ChestArmor]: 'chest', [BucketHashes.LegArmor]: 'leg', [BucketHashes.ClassArmor]: 'classitem', [BucketHashes.Subclass]: 'subclass', [D1BucketHashes.Artifact]: 'artifact', [BucketHashes.Ghost]: 'ghost', [BucketHashes.Consumables]: 'consumables', [BucketHashes.Materials]: 'material', [BucketHashes.Modifications]: 'ornaments', [BucketHashes.Emblems]: 'emblems', [D1BucketHashes.Shader]: 'shader', [BucketHashes.Emotes]: 'emote', [BucketHashes.Ships]: 'ships', [BucketHashes.Vehicle]: 'vehicle', [D1BucketHashes.Horn]: 'horn', [D1BucketHashes.Bounties]: 'bounties', [D1BucketHashes.Quests]: 'quests', [D1BucketHashes.Missions]: 'missions', [D1BucketHashes.D1Emotes]: 'emotes', }; export const itemTypeFilter = { keywords: Object.values(D2Categories) // stuff like Engrams, Kinetic, Gauntlets, Emblems, Finishers, Modifications .flat() .map((v) => { const type = bucketToType[v]; if (!type && $DIM_FLAVOR === 'dev') { throw new Error( `itemTypeFilter: You forgot to map a string type name for bucket hash ${v}`, ); } return type!; }), destinyVersion: 2, description: tl('Filter.ArmorCategory'), // or 'Filter.WeaponClass' filter: ({ filterValue }) => { let bucketHash: BucketHashes; for (const [bucketHashStr, type] of Object.entries(bucketToType)) { if (type === filterValue) { bucketHash = parseInt(bucketHashStr, 10); break; } } return (item) => item.bucket.hash === bucketHash; }, fromItem: (item) => `is:${bucketToType[item.bucket.hash as BucketHashes]}`, } satisfies ItemFilterDefinition; // D1 has different item types, otherwise this is the same as itemTypeFilter. const d1itemTypeFilter = { keywords: Object.values(D1Categories) // stuff like Engrams, Kinetic, Gauntlets, Emblems, Finishers, Modifications .flat() .map((v) => { const type = d1BucketToType[v]; if (!type && $DIM_FLAVOR === 'dev') { throw new Error( `d1itemTypeFilter You forgot to map a string type name for bucket hash ${v}`, ); } return type!; }), destinyVersion: 1, description: tl('Filter.ArmorCategory'), // or 'Filter.WeaponClass' filter: ({ filterValue }) => { let bucketHash: BucketHashes | D1BucketHashes; for (const [bucketHashStr, type] of Object.entries(d1BucketToType)) { if (type === filterValue) { bucketHash = parseInt(bucketHashStr, 10); break; } } return (item) => item.bucket.hash === bucketHash; }, fromItem: (item) => `is:${d1BucketToType[item.bucket.hash as BucketHashes]}`, } satisfies ItemFilterDefinition; export const itemCategoryFilter = { keywords: [ ...Object.keys(itemCategoryHashesByName), ...Object.keys(itemCategoryAliases), 'grenadelauncher', ], description: tl('Filter.WeaponType'), filter: ({ filterValue }) => { // Before special GLs and heavy GLs were entirely separated, `is:grenadelauncher` matched both. // This keeps existing searches valid and unchanged in behavior. if (filterValue === 'grenadelauncher') { return (item) => item.itemCategoryHashes.includes(ItemCategoryHashes.GrenadeLaunchers) || item.itemCategoryHashes.includes(-ItemCategoryHashes.GrenadeLaunchers); } filterValue = filterValue.replace(/\s/g, ''); const categoryHash = itemCategoryHashesByName[itemCategoryAliases[filterValue] ?? filterValue]; if (!categoryHash) { throw new Error(`Unknown weapon type ${filterValue}`); } return (item) => item.itemCategoryHashes.includes(categoryHash); }, fromItem: (item) => { /* The last ICH will be the most specific, so start there and try find a corresponding search filter. If we can't find one (e.g. for slug shotguns), try the next most specific ICH and so on. */ for (let i = item.itemCategoryHashes.length - 1; i >= 0; i--) { const itemCategoryHash = item.itemCategoryHashes[i]; const typeTag = Object.entries(itemCategoryHashesByName).find( ([_tag, ich]) => ich === itemCategoryHash, )?.[0]; if (typeTag) { return `is:${typeTag}`; } } return ''; }, } satisfies ItemFilterDefinition; export const ammoTypeFilter = { keywords: ['special', 'primary', 'heavy'], description: tl('Filter.AmmoType'), destinyVersion: 2, filter: ({ filterValue }) => { const ammoType = d2AmmoTypes[filterValue as keyof typeof d2AmmoTypes]; return (item: DimItem) => item.ammoType === ammoType; }, fromItem: (item) => { const ammoType = Object.entries(d2AmmoTypes).find( ([_ammoType, value]) => value === item.ammoType, ); return ammoType ? `is:${ammoType[0]}` : ''; }, } satisfies ItemFilterDefinition; const knownValuesFilters: ItemFilterDefinition[] = [ damageFilter, prismaticDamageFilter, classFilter, itemCategoryFilter, itemTypeFilter, d1itemTypeFilter, ammoTypeFilter, { keywords: [ 'common', 'uncommon', 'rare', 'legendary', 'exotic', 'white', 'green', 'blue', 'purple', 'yellow', ], description: tl('Filter.RarityTier'), filter: ({ filterValue }) => { const rarityName = rarityMap[filterValue]; if (!rarityName) { throw new Error(`Unknown rarity type ${filterValue}`); } return (item) => item.rarity === rarityName; }, }, { keywords: 'cosmetic', description: tl('Filter.Cosmetic'), filter: () => (item) => cosmeticTypes.includes(item.bucket.hash), }, { keywords: ['haslight', 'haspower'], description: tl('Filter.ContributePower'), filter: () => (item) => item.power > 0, }, { keywords: 'breaker', description: tl('Filter.Breaker'), format: 'query', suggestions: [...Object.keys(breakerTypes), 'intrinsic'], destinyVersion: 2, filter: ({ filterValue }) => { if (filterValue === 'intrinsic') { return (item) => Boolean(item.breakerType); } const breakerType = breakerTypes[filterValue as keyof typeof breakerTypes]; if (!breakerType) { throw new Error(`Unknown breaker type ${filterValue}`); } const breakingIchs = breakerType.flatMap((ty) => artifactBreakerMods[ty] || []); return (item) => item.breakerType ? breakerType.includes(item.breakerType?.hash as BreakerTypeHashes) : item.itemCategoryHashes.some((ich) => breakingIchs.includes(ich)); }, }, { keywords: 'foundry', description: tl('Filter.Foundry'), format: 'query', suggestions: ['daito', 'hakke', 'omolon', 'suros', 'tex-mechanica', 'veist', 'any'], destinyVersion: 2, filter: ({ filterValue }) => { switch (filterValue) { case 'any': return (item) => Boolean(item.foundry); default: return (item) => item.foundry === filterValue; } }, }, { keywords: 'powerfulreward', description: tl('Filter.PowerfulReward'), destinyVersion: 2, filter: () => (item) => item.pursuit?.rewards.some((r) => powerfulSources.includes(r.itemHash)), }, { keywords: 'pinnaclereward', description: tl('Filter.PinnacleReward'), destinyVersion: 2, filter: () => (item) => item.pursuit?.rewards.some((r) => pinnacleSources.includes(r.itemHash)), }, { keywords: ['craftable'], description: tl('Filter.Craftable'), destinyVersion: 2, filter: () => (item) => Boolean(item.patternUnlockRecord), }, { keywords: ['patternunlocked'], description: tl('Filter.PatternUnlocked'), destinyVersion: 2, filter: () => patternIsUnlocked, }, { keywords: 'source', description: tl('Filter.Event'), // or 'Filter.Source' format: 'query', suggestions: [...Object.keys(D2Sources), ...Object.keys(D2EventPredicateLookup)], destinyVersion: 2, filter: ({ filterValue }) => { if (D2Sources[filterValue]) { const sourceInfo = D2Sources[filterValue]; return (item) => (item.source && sourceInfo.sourceHashes?.includes(item.source)) || sourceInfo.itemHashes?.includes(item.hash); } else if (D2EventPredicateLookup[filterValue]) { const predicate = D2EventPredicateLookup[filterValue]; return (item: DimItem) => getEvent(item) === predicate; } else { throw new Error(`Unknown item source ${filterValue}`); } }, }, { keywords: 'focusable', description: tl('Filter.Focusable'), destinyVersion: 2, filter: () => { const outputValues = Object.values(focusingOutputs); return (item) => outputValues.includes(item.hash); }, }, ]; export function patternIsUnlocked(item: DimItem) { return ( item.patternUnlockRecord && !(item.patternUnlockRecord.state & DestinyRecordState.ObjectiveNotCompleted) ); } export default knownValuesFilters; ================================================ FILE: src/app/search/items/search-filters/loadouts.ts ================================================ import { tl } from 'app/i18next-t'; import { getHashtagsFromString } from 'app/inventory/note-hashtags'; import { InGameLoadout, isInGameLoadout, Loadout } from 'app/loadout/loadout-types'; import { quoteFilterString } from 'app/search/query-parser'; import { ItemFilterDefinition } from '../item-filter-types'; export function loadoutToSearchString(loadout: Loadout | InGameLoadout) { return `inloadout:${quoteFilterString(loadout.name.toLowerCase())}`; } // related: https://github.com/DestinyItemManager/DIM/issues/9069 // sanity check: `inloadout:#hashta` should not suggest `inloadout:inloadout:#hashtag` (double prefix) function loadoutToSuggestions(loadout: Loadout) { return [ quoteFilterString(loadout.name.toLowerCase()), // loadout name ...getHashtagsFromString(loadout.name, loadout.notes), // #hashtags in the name/notes ].map((suggestion) => `inloadout:${suggestion}`); } const loadoutFilters: ItemFilterDefinition[] = [ { keywords: 'inloadout', format: ['simple', 'range', 'freeform'], suggestionsGenerator: ({ loadouts }) => loadouts?.flatMap(loadoutToSuggestions), description: tl('Filter.InLoadout'), filter: ({ lhs, filterValue, loadoutsByItem, compare }) => { // the range search for how many loadouts an item is in: // inloadout:>=3 if (compare) { return (item) => compare(loadoutsByItem[item.id]?.length ?? 0); } // the default search: // is:inloadout if (lhs === 'is') { return (item) => Boolean(loadoutsByItem[item.id]); } // a search like // inloadout:"loadout name here" // inloadout:"pvp" (for all loadouts with pvp in their name) // inloadout:"#pve" (for all loadouts with the #pve hashtag in name or notes) return (item) => loadoutsByItem[item.id]?.some( ({ loadout }) => loadout.id === filterValue || loadout.name.toLowerCase().includes(filterValue) || (filterValue.startsWith('#') && // short circuit for less load !isInGameLoadout(loadout) && getHashtagsFromString(loadout.notes) .map((t) => t.toLowerCase()) .includes(filterValue)), ); }, }, { keywords: 'iningameloadout', format: 'simple', description: tl('Filter.InInGameLoadout'), filter: ({ loadoutsByItem }) => (item) => Boolean(loadoutsByItem[item.id]?.some((l) => isInGameLoadout(l.loadout))), }, { keywords: 'indimloadout', format: 'simple', description: tl('Filter.InDimLoadout'), filter: ({ loadoutsByItem }) => (item) => Boolean(loadoutsByItem[item.id]?.some((l) => !isInGameLoadout(l.loadout))), }, ]; export default loadoutFilters; ================================================ FILE: src/app/search/items/search-filters/perks-set.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import { normalizeToEnhanced } from 'app/utils/perk-utils'; import { getSocketsByType } from 'app/utils/socket-utils'; type PerkType = Parameters<typeof getSocketsByType>[1]; /** * A PerksSet can be populated with a bunch of items, and can then answer questions * such as: * 1. Are there any items that have (at least) all the same perks (in the same * columns) as the input item? This covers both exactly-identical perk sets, * as well as items that are perk-subsets of the input item (e.g. there may * be another item that has all the same perks, plus some extra options in * some columns). */ export class PerksSet { // A map from item ID to a list of columns, each of which has a set of perkHashes mapping = new Map<string, Set<number>[]>(); perkType: PerkType = 'perks'; constructor(items?: DimItem[], perkType?: PerkType) { if (perkType) { this.perkType = perkType; } if (items) { for (const i of items) { this.insert(i); } } } insert(item: DimItem) { this.mapping.set(item.id, makePerksSet(item, this.perkType)); } hasPerkDupes(item: DimItem) { const perksSet = makePerksSet(item, this.perkType); for (const [id, set] of this.mapping) { if (id === item.id) { continue; } if (perksSet.every((column) => set.some((otherColumn) => column.isSubsetOf(otherColumn)))) { return true; } } return false; } } function makePerksSet(item: DimItem, perkType?: PerkType) { if (perkType === 'perks') { return item .sockets!.allSockets.filter((s) => s.isPerk && s.socketDefinition.defaultVisible) .map((s) => new Set(s.plugOptions.map((p) => p.plugDef.hash))); } return getSocketsByType(item, perkType).map( (s) => new Set(s.plugOptions.map((p) => normalizeToEnhanced(p.plugDef.hash))), ); } ================================================ FILE: src/app/search/items/search-filters/range-numeric.ts ================================================ import { tl } from 'app/i18next-t'; import { getItemKillTrackerInfo, getItemYear } from 'app/utils/item-utils'; import { ItemFilterDefinition } from '../item-filter-types'; const simpleRangeFilters: ItemFilterDefinition[] = [ { keywords: 'stack', description: tl('Filter.StackLevel'), format: 'range', filter: ({ compare }) => (item) => compare!(item.amount), }, { keywords: 'year', description: tl('Filter.Year'), format: 'range', filter: ({ compare }) => (item) => compare!(getItemYear(item) ?? 0), }, { keywords: 'level', destinyVersion: 1, description: tl('Filter.RequiredLevel'), format: 'range', filter: ({ compare }) => (item) => compare!(item.equipRequiredLevel), }, { keywords: 'kills', description: tl('Filter.MasterworkKills'), format: ['range', 'stat'], destinyVersion: 2, suggestions: ['pve', 'pvp', 'gambit'], validateStat: () => (stat) => ['pve', 'pvp', 'gambit'].includes(stat), filter: ({ filterValue, compare }) => (item) => { const killTrackerInfo = getItemKillTrackerInfo(item); return Boolean( killTrackerInfo && (!filterValue.length || filterValue === killTrackerInfo.type) && compare!(killTrackerInfo.count), ); }, }, { keywords: 'weaponlevel', description: tl('Filter.WeaponLevel'), format: 'range', destinyVersion: 2, filter: ({ compare }) => (item) => Boolean(item.craftedInfo) && compare!(item.craftedInfo?.level || 0), }, { keywords: 'tier', description: tl('Filter.Tier'), format: 'range', destinyVersion: 2, filter: ({ compare }) => (item) => compare!(item.tier), }, ]; export default simpleRangeFilters; ================================================ FILE: src/app/search/items/search-filters/range-overload.ts ================================================ import { tl } from 'app/i18next-t'; import { getSeason } from 'app/inventory/store/season'; import { powerLevelByKeyword } from 'app/search/power-levels'; import { allStatNames, statHashByName } from 'app/search/search-filter-values'; import { D2CalculatedSeason } from 'data/d2/d2-season-info'; import seasonTags from 'data/d2/season-tags.json'; import { ItemFilterDefinition } from '../item-filter-types'; export const seasonTagToNumber = { ...seasonTags, next: D2CalculatedSeason + 1, current: D2CalculatedSeason, }; // overloadedRangeFilters: stuff that may test a range, but also accepts a word // this word might become a number like arrival ====> 11, // then be processed normally in a number check const overloadedRangeFilters: ItemFilterDefinition[] = [ { keywords: 'masterwork', description: tl('Filter.Masterwork'), format: ['simple', 'query', 'range'], destinyVersion: 2, suggestions: allStatNames, filter: ({ lhs, filterValue, compare }) => { // the "is:masterwork" case if (lhs === 'is') { return (item) => item.masterwork; } // "masterwork:<5" case if (compare) { return (item) => item.masterworkInfo && compare(item.masterworkInfo.tier ?? 0); } // "masterwork:range" case const searchedMasterworkStatHash = statHashByName[filterValue]; return (item) => Boolean( item.masterworkInfo?.stats?.some( (s) => filterValue === 'any' || (s.isPrimary && s.hash === searchedMasterworkStatHash), ), ); }, }, { keywords: 'energycapacity', description: tl('Filter.EnergyCapacity'), format: 'range', destinyVersion: 2, filter: ({ compare }) => (item) => item.energy && compare!(item.energy.energyCapacity), }, { keywords: 'season', description: tl('Filter.Season'), format: 'range', destinyVersion: 2, overload: Object.fromEntries(Object.entries(seasonTagToNumber).reverse()), filter: ({ compare }) => (item) => compare!(getSeason(item)), }, { keywords: ['light', 'power'], /* t('Filter.PowerKeywords') */ description: tl('Filter.PowerLevel'), format: 'range', overload: powerLevelByKeyword, filter: ({ compare }) => (item) => Boolean(item.power && compare!(item.power)), }, { keywords: 'powerlimit', description: tl('Filter.Deprecated'), format: 'range', overload: powerLevelByKeyword, destinyVersion: 2, deprecated: true, filter: ({ compare }) => () => // no items are sunset, they all have effectively unlimited caps. The // manifest specifies this cap or something similar for almost // everything: compare!(999990), }, ]; export default overloadedRangeFilters; ================================================ FILE: src/app/search/items/search-filters/simple.ts ================================================ import { tl } from 'app/i18next-t'; import { compact } from 'app/utils/collections'; import { isArmor3 } from 'app/utils/item-utils'; import { BucketHashes } from 'data/d2/generated-enums'; import { ItemFilterDefinition } from '../item-filter-types'; // simple checks against check an attribute found on DimItem const simpleFilters: ItemFilterDefinition[] = compact<ItemFilterDefinition | false>([ { keywords: 'armor2.0', description: tl('Filter.Energy'), destinyVersion: 2, filter: () => (item) => Boolean(item.energy) && item.bucket.inArmor && !isArmor3(item), }, { keywords: 'armor3.0', description: tl('Filter.Armor3'), destinyVersion: 2, filter: () => (item) => Boolean(item.energy) && item.bucket.inArmor && isArmor3(item), }, { keywords: 'weapon', description: tl('Filter.Weapon'), filter: () => (item) => item.bucket?.sort === 'Weapons' && item.bucket.hash !== BucketHashes.SeasonalArtifact && item.bucket.hash !== BucketHashes.Subclass, }, { keywords: 'armor', description: tl('Filter.Armor'), filter: () => (item) => item.bucket?.sort === 'Armor', }, { keywords: ['equipment', 'equippable'], description: tl('Filter.Equipment'), filter: () => (item) => item.equipment, }, { keywords: ['postmaster', 'inpostmaster'], description: tl('Filter.Postmaster'), filter: () => (item) => item.location?.inPostmaster, }, { keywords: 'equipped', description: tl('Filter.Equipped'), filter: () => (item) => item.equipped, }, { keywords: ['transferable', 'movable'], description: tl('Filter.Transferable'), filter: () => (item) => !item.notransfer, }, { keywords: 'stackable', description: tl('Filter.Stackable'), filter: () => (item) => item.maxStackSize > 1, }, { keywords: 'stackfull', description: tl('Filter.StackFull'), filter: () => (item) => item.maxStackSize > 1 && item.amount === item.maxStackSize, }, { keywords: ['infusable', 'infuse'], description: tl('Filter.Infusable'), filter: () => (item) => item.infusable, }, { keywords: 'locked', description: tl('Filter.Locked'), filter: () => (item) => item.lockable && item.locked, }, { keywords: 'unlocked', description: tl('Filter.Locked'), filter: () => (item) => item.lockable && !item.locked, }, $featureFlags.newItems && { keywords: 'new', description: tl('Filter.NewItems'), filter: ({ newItems }) => (item) => newItems.has(item.id), }, { keywords: 'sunset', destinyVersion: 2, description: tl('Filter.Deprecated'), deprecated: true, filter: () => () => false, }, { keywords: ['crafted', 'shaped'], destinyVersion: 2, description: tl('Filter.IsCrafted'), filter: () => (item) => item.crafted === 'crafted', }, { keywords: ['vendor'], destinyVersion: 2, description: tl('Filter.VendorItem'), filter: () => (item) => Boolean(item.vendor), }, { keywords: 'ininventory', description: tl('Filter.InInventory'), filter: ({ allItems }) => { const ownedHashes = new Set(allItems.map((item) => item.hash)); return (item) => ownedHashes.has(item.hash); }, }, { keywords: ['featured', 'newgear'], description: tl('Filter.Featured'), destinyVersion: 2, filter: () => (item) => item.featured, }, ]); export default simpleFilters; ================================================ FILE: src/app/search/items/search-filters/sockets.ts ================================================ import { tl } from 'app/i18next-t'; import { enhancementSocketHash } from 'app/inventory/store/crafted'; import { DEFAULT_GLOW, DEFAULT_ORNAMENTS, DEFAULT_SHADER, emptySocketHashes, } from 'app/search/d2-known-values'; import { plainString } from 'app/search/text-utils'; import { getSpecialtySocketMetadata, isArtifice, modSlotTags } from 'app/utils/item-utils'; import { enhancedVersion } from 'app/utils/perk-utils'; import { countEnhancedPerks, getIntrinsicArmorPerkSocket, getSocketsByCategoryHash, matchesCuratedRoll, } from 'app/utils/socket-utils'; import { StringLookup } from 'app/utils/util-types'; import { DestinyItemSubType, DestinyRecordState } from 'bungie-api-ts/destiny2'; import craftingMementos from 'data/d2/crafting-mementos.json'; import { ItemCategoryHashes, PlugCategoryHashes, SocketCategoryHashes, } from 'data/d2/generated-enums'; import { ItemFilterDefinition } from '../item-filter-types'; import { patternIsUnlocked } from './known-values'; export const modslotFilter = { keywords: 'modslot', description: tl('Filter.ModSlot'), format: 'query', suggestions: modSlotTags.concat(['none', 'activity']), destinyVersion: 2, filter: ({ filterValue }) => (item) => { const modSocketTag = getSpecialtySocketMetadata(item)?.slotTag; return Boolean( (filterValue === 'none' && !modSocketTag) || (modSocketTag && (filterValue === 'any' || filterValue === 'activity' || modSocketTag === filterValue)), ); }, fromItem: (item) => { const modSocketTag = getSpecialtySocketMetadata(item)?.slotTag; return modSocketTag ? `modslot:${modSocketTag}` : ''; }, } satisfies ItemFilterDefinition; const socketFilters: ItemFilterDefinition[] = [ modslotFilter, { keywords: 'artifice', description: tl('Filter.Artifice'), destinyVersion: 2, filter: () => (item) => isArtifice(item), }, { keywords: 'randomroll', description: tl('Filter.RandomRoll'), destinyVersion: 2, filter: () => (item) => Boolean(item.bucket.inArmor && item.energy) || (!item.crafted && item.sockets?.allSockets.some( (s) => s.isPerk && s.plugOptions.length > 0 && s.hasRandomizedPlugItems, )), }, { keywords: 'curated', description: tl('Filter.Curated'), destinyVersion: 2, filter: ({ d2Definitions }) => (item) => matchesCuratedRoll(d2Definitions!, item), }, { keywords: ['holofoil', 'shiny'], description: tl('Filter.Holofoil'), destinyVersion: 2, filter: () => (i) => i.bucket.inWeapons && i.holofoil, }, { keywords: 'extraperk', description: tl('Filter.ExtraPerk'), destinyVersion: 2, filter: () => (item) => { if (!(item.bucket?.sort === 'Weapons' && item.rarity === 'Legendary')) { return false; } return getSocketsByCategoryHash(item.sockets, SocketCategoryHashes.WeaponPerks_Reusable) .filter( (socket) => socket.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Frames && socket.hasRandomizedPlugItems, ) .some((socket) => socket.plugOptions.length > 1); }, }, { keywords: ['shaded', 'hasshader'], description: tl('Filter.HasShader'), destinyVersion: 2, filter: () => (item) => item.sockets?.allSockets.some((socket) => Boolean( socket.plugged?.plugDef.itemSubType === DestinyItemSubType.Shader && socket.plugged.plugDef.hash !== DEFAULT_SHADER, ), ), }, { keywords: ['ornamented', 'hasornament'], description: tl('Filter.HasOrnament'), destinyVersion: 2, filter: () => (item) => item.sockets?.allSockets.some((socket) => Boolean( socket.plugged && (socket.plugged.plugDef.itemSubType === DestinyItemSubType.Ornament || socket.plugged.plugDef.plug.plugCategoryIdentifier.match( /armor_skins_(titan|warlock|hunter)_(head|arms|chest|legs|class)/, )) && socket.plugged.plugDef.hash !== DEFAULT_GLOW && !DEFAULT_ORNAMENTS.includes(socket.plugged.plugDef.hash) && !socket.plugged.plugDef.itemCategoryHashes?.includes( ItemCategoryHashes.ArmorModsGlowEffects, ), ), ), }, { keywords: 'hasdisabledmod', description: tl('Filter.DisabledModSlot'), destinyVersion: 2, filter: () => (item) => !item.itemCategoryHashes.includes(ItemCategoryHashes.Subclasses) && item.sockets?.allSockets.some((socket) => Boolean(socket.plugged && socket.visibleInGame && !socket.plugged.enabled), ), }, { keywords: 'modded', description: tl('Filter.Mods.Y3'), destinyVersion: 2, filter: () => (item) => item.sockets?.allSockets.some((socket) => Boolean( socket.plugged && !emptySocketHashes.includes(socket.plugged.plugDef.hash) && socket.plugged.plugDef.plug?.plugCategoryIdentifier.match( /(v400.weapon.mod_(guns|damage|magazine)|enhancements.|v900.weapon.mod_)/, ) && // enforce that this provides a perk (excludes empty slots) socket.plugged.plugDef.perks.length, ), ), }, { keywords: 'armorintrinsic', format: ['simple', 'query'], suggestions: ['none'], description: tl('Filter.ArmorIntrinsic'), destinyVersion: 2, filter: ({ filterValue, language }) => { if (filterValue === 'armorintrinsic') { return (item) => Boolean(!item.isExotic && getIntrinsicArmorPerkSocket(item)); } if (filterValue === 'none') { return (item) => Boolean(!item.isExotic && item.bucket.inArmor && !getIntrinsicArmorPerkSocket(item)); } return (item) => { const intrinsic = getIntrinsicArmorPerkSocket(item)?.plugged?.plugDef.displayProperties.name; return Boolean(intrinsic && plainString(intrinsic, language).includes(filterValue)); }; }, }, { keywords: 'deepsight', description: tl('Filter.Deepsight'), format: ['simple', 'query'], suggestions: ['harmonizable', 'extractable'], destinyVersion: 2, filter: ({ filterValue }) => (item) => !patternIsUnlocked(item) && (filterValue === 'harmonizable' ? // is:harmonizable checks for an "insert harmonizer" socket Boolean( item.sockets?.allSockets.some( (s) => s.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.CraftingPlugsWeaponsModsExtractors && s.visibleInGame, ), ) : // is:extractable checks for red-borderness Boolean( item.deepsightInfo && item.patternUnlockRecord && item.patternUnlockRecord.state & DestinyRecordState.ObjectiveNotCompleted, )), }, { keywords: 'memento', description: tl('Filter.Memento'), format: 'query', destinyVersion: 2, suggestions: ['any', 'none', ...Object.keys(craftingMementos)], filter: ({ filterValue }) => { const list = (craftingMementos as StringLookup<number[]>)[filterValue]; return (item) => item.sockets?.allSockets.some( (s) => (s.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Mementos && (filterValue === 'any' || list?.includes(s.plugged.plugDef.hash))) || // Crafted items with no memento (filterValue === 'none' && item.crafted && s.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.CraftingRecipesEmptySocket), ); }, }, { keywords: 'catalyst', description: tl('Filter.Catalyst'), format: 'query', destinyVersion: 2, suggestions: ['complete', 'incomplete', 'missing'], filter: ({ filterValue }) => (item) => { if (!item.catalystInfo) { return false; } switch (filterValue) { case 'missing': return !item.catalystInfo.unlocked; case 'complete': return item.catalystInfo.complete; case 'incomplete': return item.catalystInfo.unlocked && !item.catalystInfo.complete; default: return false; } }, }, { keywords: 'enhancedperk', description: tl('Filter.EnhancedPerk'), format: ['simple', 'range'], destinyVersion: 2, filter: ({ lhs, compare }) => { if (compare) { return (item) => item.sockets && compare(countEnhancedPerks(item.sockets)); } if (lhs === 'is') { return (item) => item.sockets && countEnhancedPerks(item.sockets) > 0; } return (_item) => false; }, }, { keywords: 'enhanceable', description: tl('Filter.Enhanceable'), destinyVersion: 2, filter: () => (item) => Boolean( (item.craftedInfo?.enhancementTier || 0) < 3 && item.sockets?.allSockets.some( (s) => s.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.CraftingPlugsWeaponsModsEnhancers, ), ), }, { keywords: 'enhanced', description: tl('Filter.Enhanced'), destinyVersion: 2, format: ['simple', 'range'], filter: ({ lhs, compare }) => { if (compare) { return (item) => compare(item.craftedInfo?.enhancementTier || 0); } if (lhs === 'is') { return (item) => item.crafted === 'enhanced' && (item.craftedInfo?.enhancementTier || 0) > 0; } // shouldn't ever get here but need the default case return (_item) => false; }, }, { keywords: 'enhancementready', description: tl('Filter.EnhancementReady'), destinyVersion: 2, filter: () => (item) => { if (!item.crafted || !item.craftedInfo) { return false; } if (item.crafted === 'enhanced') { return item.sockets?.allSockets .find((s) => s.socketDefinition.socketTypeHash === enhancementSocketHash) ?.reusablePlugItems?.some((p) => p.canInsert); } if (item.crafted === 'crafted') { return item.sockets?.allSockets.some((s) => { const enhancedPerk = enhancedVersion(s.plugged?.plugDef.hash || 0) || 0; return ( enhancedPerk && s.plugSet?.plugHashesThatCanRoll.includes(enhancedPerk) && s.plugSet?.craftingData && (s.plugSet?.craftingData?.[enhancedPerk]?.requiredLevel || 0) <= (item.craftedInfo?.level || 0) ); }); } return false; }, }, { keywords: 'retiredperk', description: tl('Filter.RetiredPerk'), destinyVersion: 2, filter: () => (item) => { if (!(item.bucket?.sort === 'Weapons' && item.rarity === 'Legendary')) { return false; } return getSocketsByCategoryHash(item.sockets, SocketCategoryHashes.WeaponPerks_Reusable).some( (socket) => socket.plugOptions.some((p) => p.cannotCurrentlyRoll), ); }, }, { keywords: 'adept', description: tl('Filter.IsAdept'), destinyVersion: 2, filter: () => (item) => item.adept && item.bucket.inWeapons, }, { keywords: 'origintrait', description: tl('Filter.OriginTrait'), destinyVersion: 2, filter: () => (item) => item.sockets?.allSockets.some((s) => s.plugged?.plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.WeaponModsOriginTraits), ), }, ]; export default socketFilters; ================================================ FILE: src/app/search/items/search-filters/stats.ts ================================================ import { CustomStatDef } from '@destinyitemmanager/dim-api-types'; import { tl } from 'app/i18next-t'; import { DimItem, DimStat } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { maxLightItemSet, maxStatLoadout } from 'app/loadout-drawer/auto-loadouts'; import { realD2ArmorStatHashByName } from 'app/search/d2-known-values'; import { allAtomicStats, armor3OrdinalIndexByName, armorAnyStatHashes, armorStatHashes, dimArmorStatHashByName, est, estStatNames, searchableArmorStatNames, searchableD2Armor3StatNames, statHashByName, statOrdinals, weaponStatNames, } from 'app/search/search-filter-values'; import { generateGroupedSuggestionsForFilter } from 'app/search/suggestions-generation'; import { mapValues, maxOf, sumBy } from 'app/utils/collections'; import { getArmor3StatFocus, getArmor3TuningStat, getStatValuesByHash, isArmor3, isClassCompatible, } from 'app/utils/item-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { once } from 'es-toolkit'; import { ItemFilterDefinition } from '../item-filter-types'; const validateStat: ItemFilterDefinition['validateStat'] = (filterContext) => { const customStatLabels = filterContext?.customStats?.map((c) => c.shortLabel) ?? []; const possibleStatNames = [...allAtomicStats, ...customStatLabels]; return (stat) => possibleStatNames.includes(stat) || stat.split(/&|\+/).every((s) => s !== 'any' && possibleStatNames.includes(s)); }; // filters that operate on stats, several of which calculate values from all items beforehand const statFilters: ItemFilterDefinition[] = [ { keywords: 'stat', // t('Filter.StatsExtras') description: tl('Filter.Stats'), format: 'stat', suggestionsGenerator: ({ customStats }) => generateGroupedSuggestionsForFilter( { keywords: 'stat', format: 'stat', suggestions: [...allAtomicStats, ...(customStats?.map((c) => c.shortLabel) ?? [])], }, {}, ), validateStat, filter: ({ filterValue, compare, customStats }) => statFilterFromString(filterValue, compare!, customStats), }, { keywords: 'basestat', // t('Filter.StatsExtras') description: tl('Filter.StatsBase'), format: 'stat', // Note: weapons of the same hash also have the same base stats, so this is only useful for // armor really, so the suggestions only list armor stats. But `validateStats` does allow // other stats too because there's no good reason to forbid it... suggestionsGenerator: ({ customStats }) => generateGroupedSuggestionsForFilter( { keywords: 'basestat', format: 'stat', suggestions: [ ...searchableArmorStatNames, ...estStatNames, ...(customStats?.map((c) => c.shortLabel) ?? []), ], }, {}, ), validateStat, filter: ({ filterValue, compare, customStats }) => statFilterFromString(filterValue, compare!, customStats, true), }, { // looks for a loadout (simultaneously equippable) maximized for this stat keywords: 'maxstatloadout', description: tl('Filter.StatsLoadout'), format: 'query', suggestions: Object.keys(dimArmorStatHashByName), destinyVersion: 2, filter: ({ filterValue, stores, allItems }) => { const maxStatLoadout = findMaxStatLoadout(stores, allItems, filterValue); return (item) => { // filterValue stat must exist, and this must be armor if (!item.bucket.inArmor || !statHashByName[filterValue]) { return false; } return maxStatLoadout.includes(item.id); }; }, }, { keywords: 'maxstatvalue', description: tl('Filter.StatsMax'), format: 'query', suggestions: searchableArmorStatNames, destinyVersion: 2, filter: ({ filterValue, allItems }) => { const highestStatsPerSlotPerTier = gatherHighestStats(allItems); return (item: DimItem) => checkIfStatMatchesMaxValue(highestStatsPerSlotPerTier, item, filterValue); }, }, { keywords: 'maxbasestatvalue', description: tl('Filter.StatsMax'), format: 'query', suggestions: searchableArmorStatNames, destinyVersion: 2, filter: ({ filterValue, allItems }) => { const highestStatsPerSlotPerTier = gatherHighestStats(allItems); return (item: DimItem) => checkIfStatMatchesMaxValue(highestStatsPerSlotPerTier, item, filterValue, true); }, }, { keywords: 'maxpowerloadout', description: tl('Filter.MaxPowerLoadout'), destinyVersion: 2, filter: ({ stores, allItems }) => { const maxPowerLoadoutItems = calculateMaxPowerLoadoutItems(stores, allItems); return (item: DimItem) => maxPowerLoadoutItems.includes(item.id); }, }, { keywords: ['maxpower', 'accountmaxpower'], description: tl('Filter.MaxPower'), destinyVersion: 2, filter: ({ allItems, filterValue }) => { const classMatters = filterValue === 'maxpower'; const maxPowerPerBucket = calculateMaxPowerPerBucket(allItems, classMatters); return (item: DimItem) => // items can be 0pl but king of their own little kingdom, // like halloween masks, so let's exclude 0pl Boolean(item.power) && maxPowerPerBucket[maxPowerKey(item, classMatters)] <= item.power; }, }, { // looks for a loadout (simultaneously equippable) maximized for this stat keywords: Object.keys(statOrdinals), description: tl('Filter.StatsOrdinal'), format: 'query', suggestions: Object.keys(realD2ArmorStatHashByName), destinyVersion: 2, filter: ({ lhs, filterValue }) => { // A documented assumption: this lookup must succeed if logic even reached this filter, because `keywords` above const ordinal = statOrdinals[lhs]!; const seekingStatHash = realD2ArmorStatHashByName[filterValue]; if (!seekingStatHash) { throw Error(`invalid stat name: "${filterValue}"`); } return (item) => isArmor3(item) && getArmor3StatFocus(item)[ordinal] === seekingStatHash; }, }, { keywords: 'tunedstat', description: tl('Filter.TunedStat'), format: 'query', suggestions: searchableD2Armor3StatNames, destinyVersion: 2, filter: ({ filterValue }) => { const validStatHash = searchableD2Armor3StatNames.includes(filterValue); if (!validStatHash) { throw Error(`invalid stat name: "${filterValue}"`); } // Check if we are looking for generic 'primary', 'secondary', or 'tertiary' const ordinalIdx = armor3OrdinalIndexByName[filterValue]; const armorStatHash = realD2ArmorStatHashByName[filterValue]; const isUnfocused = filterValue === 'unfocused'; return (item) => { const tunedStat = getArmor3TuningStat(item); if (tunedStat === undefined) { return false; } // Looking for tunedstat:unfocused if (isUnfocused) { return !getArmor3StatFocus(item).includes(tunedStat); } // Standard tunedstat:statname handling if (ordinalIdx === undefined) { return armorStatHash !== null && tunedStat === armorStatHash; } // Looking for tunedstat: 'primary', 'secondary', or 'tertiary' const expectedStat = getArmor3StatFocus(item)?.[ordinalIdx] ?? null; return expectedStat !== null && tunedStat === expectedStat; }; }, }, ]; export default statFilters; /** * given a stat name, this returns a FilterDefinition for comparing that stat */ function statFilterFromString( statNames: string, compare: (value: number) => boolean, customStats: CustomStatDef[], byBaseValue = false, ): (item: DimItem) => boolean { // this will be used to index into the right property of a DimStat const byWhichValue = byBaseValue ? 'base' : 'value'; // a special case filter where we check for any single (natural) stat matching the comparator if (statNames === 'any') { const statMatches = (s: DimStat) => armorAnyStatHashes.includes(s.statHash) && compare(s[byWhichValue]); return (item) => Boolean(item.stats?.find(statMatches)); } else if (statNames in est) { return (item) => { if (!item.bucket.inArmor || !item.stats) { return false; } const sortedStats = item.stats .filter((s) => armorAnyStatHashes.includes(s.statHash)) .map((s) => s[byWhichValue]) .sort((a, b) => b - a); return compare(sortedStats[est[statNames as keyof typeof est]]); }; } else if (weaponStatNames.includes(statNames)) { // return earlier for weapon stats. these shouldn't do addition/averaging. const statHash = statHashByName[statNames]; return (item) => { const statValuesByHash = getStatValuesByHash(item, byWhichValue); const statValue = statValuesByHash[statHash]; if (statValue === undefined) { return false; } return compare(statValue); }; } const statCombiner = createStatCombiner(statNames, byWhichValue, customStats); // the filter computes combined values of requested stats and runs the total against comparator return (item) => Boolean(item.bucket.inArmor) && compare(statCombiner(item)); } // converts the string "mobility+strength&discipline" into a function which // returns an item's MOB + average( STR, DIS ) // this should only be run on armor stats function createStatCombiner( statString: string, byWhichValue: 'base' | 'value', customStats: CustomStatDef[], ) { // an array of arrays of stat retrieval functions. // inner arrays are averaged, then outer array is totaled const nestedAddends = statString.split('+').map((addendString) => { const averagedHashes = addendString.split('&').map((statName) => { // Support "highest&secondhighest" if (statName in est) { return ( statValuesByHash: NodeJS.Dict<number>, sortStats: () => number[][], item: DimItem, ) => { if (!item.bucket.inArmor || !item.stats) { return 0; } const sortedStats = sortStats(); const statHash = sortedStats[est[statName as keyof typeof est]][0]; if (!statHash) { throw new Error(`invalid stat name: "${statName}"`); } return statValuesByHash[statHash] || 0; }; } const statHash = statHashByName[statName]; // if we found a statHash here, this is a normal real stat, like discipline if (statHash) { // would ideally be "?? 0" but polyfills are big and || works fine return (statValuesByHash: NodeJS.Dict<number>) => statValuesByHash[statHash] || 0; } // custom stats this string represents const namedCustomStats = customStats.filter((c) => c.shortLabel === statName); if (namedCustomStats.length) { return (statValuesByHash: NodeJS.Dict<number>, _: any, item: DimItem) => { const thisClassCustomStat = namedCustomStats.find((c) => isClassCompatible(c.class, item.classType), ); // if this item's guardian class doesn't have a custom stat named statName // return false to not match if (!thisClassCustomStat) { return 0; } // otherwise, check the stat value against this custom stat's value return statValuesByHash[thisClassCustomStat.statHash] || 0; }; } throw new Error(`invalid stat name: "${statName}"`); }); return averagedHashes; }); return (item: DimItem) => { const statValuesByHash = getStatValuesByHash(item, byWhichValue); // Computed lazily const sortStats = once(() => (item.stats ?? []) .filter((s) => armorAnyStatHashes.includes(s.statHash)) .map((s) => [s.statHash, s[byWhichValue]]) .sort((a, b) => b[1] - a[1]), ); return sumBy(nestedAddends, (averageGroup) => { const averaged = sumBy(averageGroup, (statFn) => statFn(statValuesByHash, sortStats, item)) / averageGroup.length; return averaged; }); }; } function findMaxStatLoadout(stores: DimStore[], allItems: DimItem[], statName: string) { const maxStatHash = statHashByName[statName]; return stores.flatMap((store) => // Accessing id is safe: maxStatLoadout only includes items with a power level, // i.e. only weapons and armor and those are instanced. maxStatLoadout(maxStatHash, allItems, store).items.map((i) => i.id), ); } type MaxValuesDict = Record< 'all' | 'nonexotic', { [slotName: string]: { [statHash: string]: { value: number; base: number } } } >; /** given our known max stat dict, see if this item and stat are among the max stat havers */ function checkIfStatMatchesMaxValue( maxStatValues: MaxValuesDict, item: DimItem, statName: string, byBaseValue = false, ) { // this must be armor with stats if (!item.bucket.inArmor || !item.stats) { return false; } const statHashes: number[] = statName === 'any' ? armorStatHashes : [statHashByName[statName]]; const byWhichValue = byBaseValue ? 'base' : 'value'; const useWhichMaxes = item.isExotic ? 'all' : 'nonexotic'; const itemSlot = `${item.bucket.hash}|${item.classType}`; const maxStatsForSlot = maxStatValues[useWhichMaxes][itemSlot]; const matchingStats = item.stats?.filter( (s) => statHashes.includes(s.statHash) && s[byWhichValue] === maxStatsForSlot?.[s.statHash][byWhichValue], ); return matchingStats && Boolean(matchingStats.length); } function gatherHighestStats(allItems: DimItem[]) { const maxStatValues: MaxValuesDict = { all: {}, nonexotic: {} }; for (const i of allItems) { // we only want armor with stats if (!i.bucket.inArmor || !i.stats) { continue; } const itemSlot = `${i.bucket.hash}|${i.classType}`; // if this is an exotic item, update overall maxes, but don't ruin the curve for the nonexotic maxes const itemTiers: ('all' | 'nonexotic')[] = i.isExotic ? ['all'] : ['all', 'nonexotic']; const thisSlotMaxGroups = itemTiers.map((t) => (maxStatValues[t][itemSlot] ??= {})); for (const stat of i.stats) { for (const thisSlotMaxes of thisSlotMaxGroups) { const thisSlotThisStatMaxes = (thisSlotMaxes[stat.statHash] ??= { value: 0, base: 0, }); thisSlotThisStatMaxes.value = Math.max(thisSlotThisStatMaxes.value, stat.value); thisSlotThisStatMaxes.base = Math.max(thisSlotThisStatMaxes.base, stat.base); } } } return maxStatValues; } function calculateMaxPowerLoadoutItems(stores: DimStore[], allItems: DimItem[]) { return stores.flatMap((store) => maxLightItemSet(allItems, store).equippable.map((i) => i.id)); } function maxPowerKey(item: DimItem, classMatters: boolean) { return `${item.bucket.hash}-${classMatters && item.bucket.inArmor ? item.classType : ''}`; } function calculateMaxPowerPerBucket(allItems: DimItem[], classMatters: boolean) { // disregard no-class armor const validItems = allItems.filter((i) => i.classType !== DestinyClass.Classified); const allItemsByBucketClass = Object.groupBy(validItems, (i) => maxPowerKey(i, classMatters)); return mapValues(allItemsByBucketClass, (items) => items.length ? maxOf(items, (i) => i.power) : 0, ); } ================================================ FILE: src/app/search/items/search-filters/stores.ts ================================================ import { tl } from 'app/i18next-t'; import { getStore } from 'app/inventory/stores-helpers'; import { itemCanBeEquippedBy } from 'app/utils/item-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { ItemFilterDefinition } from '../item-filter-types'; // filters that check stores const locationFilters: ItemFilterDefinition[] = [ { keywords: ['inleftchar', 'inmiddlechar', 'inrightchar'], description: tl('Filter.Location'), filter: ({ filterValue, stores }) => { let storeIndex = 0; switch (filterValue) { case 'inleftchar': storeIndex = 0; break; case 'inmiddlechar': if (stores.length === 4) { storeIndex = 1; } break; case 'inrightchar': if (stores.length > 2) { storeIndex = stores.length - 2; } break; } const storeId = stores[storeIndex].id; return (item) => item.bucket.accountWide && !item.location.inPostmaster ? item.owner !== 'vault' : item.owner === storeId; }, }, { keywords: 'onwrongclass', description: tl('Filter.Class'), filter: ({ stores }) => (item) => { const ownerStore = getStore(stores, item.owner); return ( !item.classified && item.owner !== 'vault' && !item.bucket.accountWide && item.classType !== DestinyClass.Unknown && ownerStore && !itemCanBeEquippedBy(item, ownerStore) && !item.location?.inPostmaster ); }, }, { keywords: 'invault', description: tl('Filter.Location'), filter: () => (item) => item.owner === 'vault', }, { keywords: 'incurrentchar', description: tl('Filter.Location'), filter: ({ currentStore }) => (item) => currentStore ? item.owner === currentStore.id : false, }, { keywords: 'currentclass', description: tl('Filter.CurrentClass'), filter: ({ currentStore }) => (item) => currentStore ? item.classType === currentStore.classType : false, }, ]; export default locationFilters; ================================================ FILE: src/app/search/items/search-filters/wishlist.ts ================================================ import { tl } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { InventoryWishListRoll } from 'app/wishlists/wishlists'; import { ItemFilterDefinition } from '../item-filter-types'; import { computeDupes, itemDupeID } from './dupes'; import { checkIfIsDupe } from './dupes-deprecated'; const checkIfIsWishlist = ( item: DimItem, wishListFunction: (item: DimItem) => InventoryWishListRoll | undefined, ) => { const roll = wishListFunction(item); return roll && !roll.isUndesirable; }; const wishlistFilters: ItemFilterDefinition[] = [ { keywords: 'wishlist', description: tl('Filter.Wishlist'), destinyVersion: 2, filter: ({ wishListFunction }) => (item) => checkIfIsWishlist(item, wishListFunction), }, { keywords: 'wishlistdupe', deprecated: true, description: tl('Filter.WishlistDupe'), destinyVersion: 2, filter: ({ wishListFunction, allItems }) => { const duplicates = computeDupes(allItems); return (item) => { const dupeId = itemDupeID(item); if (!checkIfIsDupe(duplicates, dupeId, item)) { return false; } const itemDupes = duplicates?.[dupeId]; return itemDupes?.some((d) => checkIfIsWishlist(d, wishListFunction)); }; }, }, { keywords: 'wishlistnotes', description: tl('Filter.WishlistNotes'), format: 'freeform', destinyVersion: 2, filter: ({ wishListFunction, filterValue }) => (item) => wishListFunction(item)?.notes?.toLocaleLowerCase().includes(filterValue), }, { keywords: 'trashlist', description: tl('Filter.Trashlist'), destinyVersion: 2, filter: ({ wishListFunction }) => (item) => wishListFunction(item)?.isUndesirable, }, { keywords: 'wishlistunknown', destinyVersion: 2, description: tl('Filter.WishlistUnknown'), filter: ({ wishListsByHash }) => (item) => !wishListsByHash.has(item.hash), }, { keywords: 'wishlistable', destinyVersion: 2, description: tl('Filter.WishlistEnabled'), filter: () => (item) => item.wishListEnabled, }, ]; export default wishlistFilters; ================================================ FILE: src/app/search/loadouts/__snapshots__/loadout-search-filter.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`buildSearchConfig generates a reasonable filter map: is filters 1`] = ` [ "fashiononly", "modsonly", ] `; exports[`buildSearchConfig generates a reasonable filter map: key-value filters 1`] = ` [ "contains", "exactcontains", "exactname", "keyword", "light", "name", "notes", "power", "season", "subclass", ] `; ================================================ FILE: src/app/search/loadouts/loadout-filter-types.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimLanguage } from 'app/i18n'; import { DimItem } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { Loadout } from 'app/loadout/loadout-types'; import { LoadoutsByItem } from 'app/loadout/selectors'; import { FilterDefinition } from '../filter-types'; import { FiltersMap, SearchConfig } from '../search-config'; /** * A slice of data that could be used by loadout filter functions to * initialize some data required by particular filters. If a new filter needs * context that isn't here, add it to this interface and makeSearchFilterFactory * in search-filter.ts. */ export interface LoadoutFilterContext { /** * The selected store on the loadouts page */ selectedLoadoutsStore: DimStore; loadoutsByItem: LoadoutsByItem; language: DimLanguage; allItems: DimItem[]; d2Definitions: D2ManifestDefinitions | undefined; } /** * this provides data so that SearchConfig can build smarter lists of suggestions. * all properties must be optional, so jest & api stuff can use SearchConfig without any context */ export interface LoadoutSuggestionsContext { loadouts?: Loadout[]; /** * The selected store on the loadouts page */ selectedLoadoutsStore?: DimStore; allItems?: DimItem[]; d2Definitions?: D2ManifestDefinitions; } export type LoadoutFilterDefinition = FilterDefinition< Loadout, LoadoutFilterContext, LoadoutSuggestionsContext >; export type LoadoutFilterMap = FiltersMap<Loadout, LoadoutFilterContext, LoadoutSuggestionsContext>; export type LoadoutSearchConfig = SearchConfig< Loadout, LoadoutFilterContext, LoadoutSuggestionsContext >; ================================================ FILE: src/app/search/loadouts/loadout-search-filter.test.ts ================================================ import { canonicalFilterFormats } from 'app/search/filter-types'; import { buildLoadoutsFiltersMap } from './loadout-search-filter'; describe('buildSearchConfig', () => { const searchConfig = buildLoadoutsFiltersMap(2); test('generates a reasonable filter map', () => { expect(Object.keys(searchConfig.isFilters).sort()).toMatchSnapshot('is filters'); expect(Object.keys(searchConfig.kvFilters).sort()).toMatchSnapshot('key-value filters'); }); test('filter formats specify unambiguous formats ', () => { /* * We have a bunch of filter formats for which `keyword:value` * with purely alphabetic values can be valid syntax. Filters should * avoid specifying more than one of these. * query and freeform filters are sort of the same thing, * except queries are exhaustive and freeform aren't. Overloaded * range filters can also accept single words as filter value, * because `season:worthy` is actually `season:10` and we don't * want these to be mistaken for queries or freeforms. */ for (const filter of searchConfig.allFilters) { let formats = canonicalFilterFormats(filter.format); if (formats.length < 1) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`filter ${filter.keywords} has no formats`); } formats = formats.filter( (f) => f === 'query' || f === 'freeform' || (f === 'range' && filter.overload), ); if (formats.length > 1) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`filter ${filter.keywords} specifies ambiguous formats ${formats}`); } } }); }); ================================================ FILE: src/app/search/loadouts/loadout-search-filter.ts ================================================ import { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { destinyVersionSelector } from 'app/accounts/selectors'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { languageSelector } from 'app/dim-api/selectors'; import { DimLanguage } from 'app/i18n'; import { DimItem } from 'app/inventory/item-types'; import { allItemsSelector } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { Loadout } from 'app/loadout/loadout-types'; import { loadoutsSelector } from 'app/loadout/loadouts-selector'; import { LoadoutsByItem, loadoutsByItemSelector, selectedLoadoutStoreSelector, } from 'app/loadout/selectors'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { buildFiltersMap, buildSearchConfig } from 'app/search/search-config'; import { makeSearchFilterFactory, parseAndValidateQuery } from 'app/search/search-filter'; import memoizeOne from 'memoize-one'; import { createSelector } from 'reselect'; import { LoadoutFilterContext, LoadoutSuggestionsContext } from './loadout-filter-types'; import freeformFilters from './search-filters/freeform'; import overloadedRangeFilters from './search-filters/range-overload'; import simpleFilters from './search-filters/simple'; const allLoadoutFilters = [...simpleFilters, ...freeformFilters, ...overloadedRangeFilters]; // // Selectors // /** * A selector for the suggestionsContext for a particular destiny version. * This must depend on every bit of data in suggestionsContext so that we * regenerate filter suggestions whenever any of them changes. */ export const loadoutSuggestionsContextSelector = createSelector( loadoutsSelector, selectedLoadoutStoreSelector, allItemsSelector, d2ManifestSelector, makeLoadoutSuggestionsContext, ); function makeLoadoutSuggestionsContext( loadouts: Loadout[], selectedLoadoutsStore: DimStore, allItems: DimItem[], d2Definitions: D2ManifestDefinitions | undefined, ): LoadoutSuggestionsContext { return { loadouts, selectedLoadoutsStore, allItems, d2Definitions, }; } export const loadoutSearchConfigSelector = createSelector( destinyVersionSelector, languageSelector, loadoutSuggestionsContextSelector, buildLoadoutsSearchConfig, ); function makeLoadoutFilterContext( selectedLoadoutsStore: DimStore, loadoutsByItem: LoadoutsByItem, language: DimLanguage, allItems: DimItem[], d2Definitions: D2ManifestDefinitions | undefined, ): LoadoutFilterContext { return { selectedLoadoutsStore, loadoutsByItem, language, allItems, d2Definitions, }; } /** * A selector for the filterContext for a particular destiny version. This must * depend on every bit of data a filter might need to run, so that we regenerate the filter * functions whenever any of them changes. */ const loadoutFilterContextSelector = createSelector( selectedLoadoutStoreSelector, loadoutsByItemSelector, languageSelector, allItemsSelector, d2ManifestSelector, makeLoadoutFilterContext, ); /** * A selector for the search config for a particular destiny version. * Combines the searchConfig (list of filters), * and the filterContext (list of other stat information filters can use) * into a filter factory (for converting parsed strings into filter functions) */ export const loadoutFilterFactorySelector = createSelector( loadoutSearchConfigSelector, loadoutFilterContextSelector, makeSearchFilterFactory, ); /** A selector for a function for validating a query. */ export const validateLoadoutQuerySelector = createSelector( loadoutSearchConfigSelector, loadoutFilterContextSelector, (searchConfig, filterContext) => (query: string) => parseAndValidateQuery(query, searchConfig.filtersMap, filterContext), ); export const buildLoadoutsFiltersMap = memoizeOne((destinyVersion: DestinyVersion) => buildFiltersMap(destinyVersion, allLoadoutFilters), ); function buildLoadoutsSearchConfig( destinyVersion: DestinyVersion, language: DimLanguage, suggestionsContext: LoadoutSuggestionsContext, ) { const filtersMap = buildLoadoutsFiltersMap(destinyVersion); return buildSearchConfig(language, suggestionsContext, filtersMap); } ================================================ FILE: src/app/search/loadouts/search-filters/freeform.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { tl } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { getHashtagsFromString } from 'app/inventory/note-hashtags'; import { DimStore } from 'app/inventory/store-types'; import { findItemForLoadout, getLight, getModsFromLoadout } from 'app/loadout-drawer/loadout-utils'; import { Loadout } from 'app/loadout/loadout-types'; import { powerLevelByKeyword } from 'app/search/power-levels'; import { matchText, plainString } from 'app/search/text-utils'; import { compact, filterMap } from 'app/utils/collections'; import { emptyArray } from 'app/utils/empty'; import { isClassCompatible, itemCanBeEquippedByStoreId } from 'app/utils/item-utils'; import { BucketHashes } from 'data/d2/generated-enums'; import { FilterDefinition } from '../../filter-types'; import { quoteFilterString } from '../../query-parser'; import { LoadoutFilterContext, LoadoutSuggestionsContext } from '../loadout-filter-types'; function deduplicate<T>(someArray: (T | undefined | null)[]) { return compact(Array.from(new Set(someArray))); } function subclassFromLoadout( loadout: Loadout, d2Definitions: D2ManifestDefinitions, allItems: DimItem[] | undefined, store: DimStore | undefined, ) { for (const item of loadout.items) { const resolvedItem = findItemForLoadout( d2Definitions, allItems ?? emptyArray(), store?.id, item, ); if (resolvedItem?.bucket.hash === BucketHashes.Subclass) { return resolvedItem; } } } function isLoadoutCompatibleWithStore(loadout: Loadout, store: DimStore | undefined) { return !store || isClassCompatible(loadout.classType, store.classType); } type EquippedItemBuckets = Record<string, DimItem[]>; /** Convenience check for items that contribute to power level */ function equipsAllItemsForPowerLevel(items: EquippedItemBuckets): boolean { return ( (items[BucketHashes.KineticWeapons]?.length > 0 && items[BucketHashes.EnergyWeapons]?.length > 0 && items[BucketHashes.PowerWeapons]?.length > 0 && items[BucketHashes.Helmet]?.length > 0 && items[BucketHashes.Gauntlets]?.length > 0 && items[BucketHashes.ChestArmor]?.length > 0 && items[BucketHashes.LegArmor]?.length > 0 && items[BucketHashes.ClassArmor]?.length > 0) ?? false ); } /** Convenience function to get all items that contribute to power level */ function allLoadoutItemsForPowerLevel(items: EquippedItemBuckets): DimItem[] { return [ items[BucketHashes.KineticWeapons], items[BucketHashes.EnergyWeapons], items[BucketHashes.PowerWeapons], items[BucketHashes.Helmet], items[BucketHashes.Gauntlets], items[BucketHashes.ChestArmor], items[BucketHashes.LegArmor], items[BucketHashes.ClassArmor], ].flat(); } /** * Simplified version of getItemsFromLoadoutItems that doesn't generate warnItems, and only * converts equipped armor and weapons that can be equipped. */ function getEquippedItemsFromLoadout( loadout: Loadout, d2Definitions: D2ManifestDefinitions, allItems: DimItem[], store: DimStore, ): EquippedItemBuckets { // We have two big requirements here: // 1. items must be weapons or armor // 2. items must be able to be equipped by the character // This may not be sufficient, but for the moment it seems good enough const dimItems = filterMap(loadout.items, (loadoutItem) => { if (loadoutItem.equip) { const newItem = findItemForLoadout(d2Definitions, allItems, store.id, loadoutItem); if ( newItem && (newItem.bucket.inWeapons || newItem.bucket.inArmor) && itemCanBeEquippedByStoreId(newItem, store.id, loadout.classType, true) ) { return newItem; } } }); // Resolve this into an object that tells us what we need to know return Object.groupBy(dimItems, (item) => item.bucket.hash); } const freeformFilters: FilterDefinition< Loadout, LoadoutFilterContext, LoadoutSuggestionsContext >[] = [ { keywords: ['name', 'exactname'], description: tl('LoadoutFilter.Name'), format: 'freeform', suggestionsGenerator: ({ loadouts, selectedLoadoutsStore }) => loadouts ?.filter((loadout) => isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) .map((loadout) => `exactname:${quoteFilterString(loadout.name.toLowerCase())}`), filter: ({ filterValue, language, lhs }) => { const test = matchText(filterValue, language, /* exact */ lhs === 'exactname'); return (loadout) => test(loadout.name); }, }, { keywords: ['subclass'], description: tl('LoadoutFilter.Subclass'), format: 'freeform', suggestionsGenerator: ({ loadouts, allItems, d2Definitions, selectedLoadoutsStore }) => { if (!loadouts || !d2Definitions) { return []; } return deduplicate( loadouts.flatMap((loadout) => { if (!isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) { return; } const subclass = subclassFromLoadout( loadout, d2Definitions, allItems, selectedLoadoutsStore, ); if (!subclass) { return; } const damageName = subclass.element?.displayProperties.name; return [ `subclass:${quoteFilterString(subclass.name.toLowerCase())}`, damageName && `subclass:${quoteFilterString(damageName.toLowerCase())}`, ]; }), ); }, filter: ({ filterValue, language, allItems, d2Definitions, selectedLoadoutsStore }) => { const test = matchText(filterValue, language, false); return (loadout: Loadout) => { if (!isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) { return false; } const subclass = d2Definitions && subclassFromLoadout(loadout, d2Definitions, allItems, selectedLoadoutsStore); if (!subclass) { return false; } if (test(subclass.name)) { return true; } const damageName = subclass.element?.displayProperties.name; return damageName !== undefined && test(damageName); }; }, }, { keywords: ['contains', 'exactcontains'], description: tl('LoadoutFilter.Contains'), format: 'freeform', suggestionsGenerator: ({ d2Definitions, allItems, loadouts, selectedLoadoutsStore }) => { if (!d2Definitions || !loadouts) { return []; } return deduplicate( loadouts.flatMap((loadout) => { if (!isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) { return; } const itemSuggestions = loadout.items.map((item) => { const resolvedItem = findItemForLoadout( d2Definitions, allItems ?? emptyArray(), selectedLoadoutsStore?.id, item, ); return ( resolvedItem && `exactcontains:${quoteFilterString(resolvedItem.name.toLowerCase())}` ); }); const modSuggestions = getModsFromLoadout(d2Definitions, loadout).map( (mod) => `exactcontains:${quoteFilterString(mod.resolvedMod.displayProperties.name.toLowerCase())}`, ); return [...itemSuggestions, ...modSuggestions]; }), ); }, filter: ({ filterValue, language, allItems, d2Definitions, selectedLoadoutsStore, lhs }) => { const test = matchText(filterValue, language, lhs === 'exactcontains'); return (loadout) => { if (!d2Definitions || !isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) { return false; } return ( loadout.items.some((item) => { const resolvedItem = findItemForLoadout( d2Definitions, allItems, selectedLoadoutsStore?.id, item, ); return resolvedItem && test(resolvedItem?.name); }) || getModsFromLoadout(d2Definitions, loadout).some((mod) => test(mod.resolvedMod.displayProperties.name), ) ); }; }, }, { keywords: 'notes', description: tl('LoadoutFilter.Notes'), format: 'freeform', filter: ({ filterValue, language }) => { filterValue = plainString(filterValue, language); return (loadout) => Boolean(loadout.notes && plainString(loadout.notes, language).includes(filterValue)); }, }, { keywords: 'keyword', description: tl('LoadoutFilter.PartialMatch'), format: 'freeform', suggestionsGenerator: ({ loadouts, selectedLoadoutsStore }) => loadouts ? Array.from( new Set([ ...loadouts .filter((loadout) => isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) .flatMap((loadout) => getHashtagsFromString(loadout.notes, loadout.notes)), ]), ) : [], filter: ({ filterValue, language }) => { filterValue = plainString(filterValue, language); const test = (s: string) => plainString(s, language).includes(filterValue); return (loadout) => test(loadout.name) || Boolean(loadout.notes && test(loadout.notes)); }, }, { keywords: ['light', 'power'], /* t('Filter.PowerKeywords') */ description: tl('LoadoutFilter.LoadoutLight'), format: 'range', overload: powerLevelByKeyword, filter: ({ compare, allItems, d2Definitions, selectedLoadoutsStore }) => { if (!d2Definitions || !selectedLoadoutsStore || !allItems) { return () => false; } return (loadout: Loadout) => { if (!isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) { return false; } // Get the equipped items that contribute to the power level (weapons, armor) const equippedItems = getEquippedItemsFromLoadout( loadout, d2Definitions, allItems, selectedLoadoutsStore, ); // Require that the loadout has an item in all weapon + armor slots if (!equipsAllItemsForPowerLevel(equippedItems)) { return false; } // Calculate light level of items const lightLevel = Math.floor( getLight(selectedLoadoutsStore, allLoadoutItemsForPowerLevel(equippedItems)), ); return Boolean(compare!(lightLevel)); }; }, }, ]; export default freeformFilters; ================================================ FILE: src/app/search/loadouts/search-filters/range-overload.ts ================================================ import { tl } from 'app/i18next-t'; import { getLoadoutSeason } from 'app/loadout-drawer/loadout-utils'; import { seasonTagToNumber } from 'app/search/items/search-filters/range-overload'; import { LoadoutFilterDefinition } from '../loadout-filter-types'; // overloadedRangeFilters: stuff that may test a range, but also accepts a word const overloadedRangeFilters: LoadoutFilterDefinition[] = [ { keywords: 'season', description: tl('LoadoutFilter.Season'), format: 'range', destinyVersion: 2, overload: Object.fromEntries(Object.entries(seasonTagToNumber).reverse()), filter: ({ d2Definitions, compare }) => { const seasons = d2Definitions ? Object.values(d2Definitions.Season.getAll()) .sort((a, b) => b.seasonNumber - a.seasonNumber) .filter((s) => s.startDate) : []; return (loadout) => compare!(getLoadoutSeason(loadout, seasons)?.seasonNumber ?? -1); }, }, ]; export default overloadedRangeFilters; ================================================ FILE: src/app/search/loadouts/search-filters/simple.ts ================================================ import { tl } from 'app/i18next-t'; import { isArmorModsOnly, isFashionOnly } from 'app/loadout-drawer/loadout-utils'; import { LoadoutFilterDefinition } from '../loadout-filter-types'; // simple checks against check an attribute found on DimItem const simpleFilters: LoadoutFilterDefinition[] = [ { keywords: 'fashiononly', description: tl('LoadoutFilter.FashionOnly'), destinyVersion: 2, filter: ({ d2Definitions }) => (loadout) => isFashionOnly(d2Definitions!, loadout), }, { keywords: 'modsonly', description: tl('LoadoutFilter.ModsOnly'), destinyVersion: 2, filter: ({ d2Definitions }) => (loadout) => isArmorModsOnly(d2Definitions!, loadout), }, ]; export default simpleFilters; ================================================ FILE: src/app/search/plug-search.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimLanguage } from 'app/i18n'; import { PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { startWordRegexp } from './text-utils'; export function createPlugSearchPredicate( query: string, language: DimLanguage, defs: D2ManifestDefinitions, ) { if (!query.length) { return (_plug: PluggableInventoryItemDefinition) => true; } const regexp = startWordRegexp(query, language); return (plug: PluggableInventoryItemDefinition) => regexp.test(plug.displayProperties.name) || regexp.test(plug.displayProperties.description) || regexp.test(plug.itemTypeDisplayName) || plug.perks.some((perk) => { const perkDef = defs.SandboxPerk.get(perk.perkHash); return ( perkDef && (regexp.test(perkDef.displayProperties.name) || regexp.test(perkDef.displayProperties.description) || regexp.test(perk.requirementDisplayString)) ); }); } ================================================ FILE: src/app/search/power-levels.ts ================================================ import { D2CalculatedSeason, D2SeasonInfo } from 'data/d2/d2-season-info'; // shortcuts for power numbers const currentSeason = D2SeasonInfo[D2CalculatedSeason]; export const powerLevelByKeyword = { powerfloor: currentSeason.powerFloor, softcap: currentSeason.softCap, powerfulcap: currentSeason.powerfulCap, pinnaclecap: currentSeason.pinnacleCap, }; ================================================ FILE: src/app/search/query-parser.test.ts ================================================ import { AndOp, canonicalizeQuery, FilterOp, lexer, NoOp, NotOp, OrOp, parseQuery, QueryAST, quoteFilterString, Token, } from './query-parser'; // To update the snapshots, run: // npx jest --updateSnapshot src/app/search/query-parser.test.ts // TODO: failed parse - a failed parse should return the parts that didn't fail plus an error code?? // Some of these are contrived, some are from past search parsing issues const cases = [ [`is:blue is:haspower -is:maxpower`], [`is:blue is:haspower not:maxpower`], [`not:maxpower`], [`not -not:maxpower`], [`not not not:maxpower`], [`is:blue is:weapon or is:armor not:maxpower`], // => is:blue and (is:weapon or is:armor) and -is:maxpower [`not not:maxpower`], [`-is:equipped is:haspower is:incurrentchar`], [`-source:garden -source:lastwish sunsetsafter:arrival`], [`-is:exotic -is:locked -is:maxpower -is:tagged stat:total:<55`], [`(is:weapon is:sniperrifle) or (is:armor modslot:arrival)`], [`(is:weapon and is:sniperrifle) or not (is:armor and modslot:arrival)`], [`is:weapon and is:sniperrifle or not is:armor and modslot:arrival`], // => (is:weapon and is:sniperrifle) or (-is:armor and modslot:arrival) [`is:weapon is:sniperrifle or not is:armor modslot:arrival`], // => is:weapon and (is:sniperrifle or -is:armor) and modslot:arrival [`is:weapon is:sniperrifle or is:armor and modslot:arrival`], // => is:weapon and (is:sniperrifle or (-is:armor modslot:arrival)) [`is:weapon (is:sniperrifle or (is:armor and modslot:arrival))`], // => is:weapon and (is:sniperrifle or (-is:armor modslot:arrival)) [`-(power:>1000 and -modslot:arrival)`], [`( power:>1000 and -modslot:arrival ) `], [`- is:exotic - (power:>1000)`], [`is:armor2.0`], [`not forgotten`], // => -"forgotten" [`cluster tracking`], // => "cluster" and "tracking" [`name:"Hard Light"`], [`name:'Hard Light'`], [`name:"Gahlran's Right Hand"`], [`-witherhoard`], [`perk:수집가`], [`perk:"수집가"`], [`"수집가"`], [`수집가`], [`is:rocketlauncher -"cluster" -"tracking module"`], [`is:rocketlauncher -"cluster" -'tracking module'`], [`is:rocketlauncher (perk:"cluster" or perk:"tracking module")`], [`(is:hunter power:>=540) or (is:warlock power:>=560)`], [`"grenade launcher reserves"`], // These test copy/pasting from somewhere that automatically converts quotes to "smart quotes" [`“grenade launcher reserves”`], [`‘grenade launcher reserves’`], [`(("test" or "test") and "test")`], // https://github.com/TMMania/TMManias-DIM-Filter-Gallery/blob/main/Master%20Filter [ ` (\n (\n (is:weapon -is:maxpower powerlimit:1060 or tag:junk or is:blue)\n or\n (\n (is:armor -is:exotic -is:classitem)\n \n -(is:titan (basestat:recovery:>=18 or basestat:total:>=63))\n -(is:hunter ((basestat:recovery:>=13 basestat:mobility:>=18) or basestat:recovery:>15 or basestat:total:>=63))\n -(is:warlock ((basestat:recovery:>=18 discipline:>=17) or basestat:total:>=63))\n \n -(\n ((basestat:mobility:>=18 basestat:resilience:>=13) or\n (basestat:mobility:>=18 basestat:recovery:>=13) or\n (basestat:mobility:>=18 basestat:discipline:>=13) or\n (basestat:mobility:>=18 basestat:intellect:>=13) or\n (basestat:mobility:>=18 basestat:strength:>=13) or\n (basestat:resilience:>=18 basestat:mobility:>=13) or\n (basestat:resilience:>=18 basestat:recovery:>=13) or\n (basestat:resilience:>=18 basestat:discipline:>=13) or\n (basestat:resilience:>=18 basestat:intellect:>=13) or\n (basestat:resilience:>=18 basestat:strength:>=13) or\n (basestat:recovery:>=18 basestat:mobility:>=13) or\n (basestat:recovery:>=18 basestat:resilience:>=13) or\n (basestat:recovery:>=18 basestat:discipline:>=13) or\n (basestat:recovery:>=18 basestat:intellect:>=13) or\n (basestat:recovery:>=18 basestat:strength:>=13) or\n (basestat:discipline:>=18 basestat:mobility:>=13) or\n (basestat:discipline:>=18 basestat:resilience:>=13) or\n (basestat:discipline:>=18 basestat:recovery:>=13) or\n (basestat:discipline:>=18 basestat:intellect:>=13) or\n (basestat:discipline:>=18 basestat:strength:>=13) or\n (basestat:intellect:>=18 basestat:mobility:>=13) or\n (basestat:intellect:>=18 basestat:resilience:>=13) or\n (basestat:intellect:>=18 basestat:recovery:>=13) or\n (basestat:intellect:>=18 basestat:discipline:>=13) or\n (basestat:intellect:>=18 basestat:strength:>=13) or\n (basestat:strength:>=18 basestat:mobility:>=13) or\n (basestat:strength:>=18 basestat:resilience:>=13) or\n (basestat:strength:>=18 basestat:recovery:>=13) or\n (basestat:strength:>=18 basestat:discipline:>=13) or\n (basestat:strength:>=18 basestat:intellect:>=13))\n )\n \n -(\n ((basestat:mobility:>=13 basestat:resilience:>=13 basestat:recovery:>=13) or\n (basestat:mobility:>=13 basestat:resilience:>=13 basestat:discipline:>=13) or\n (basestat:mobility:>=13 basestat:resilience:>=13 basestat:intellect:>=13) or\n (basestat:mobility:>=13 basestat:resilience:>=13 basestat:strength:>=13) or\n (basestat:mobility:>=13 basestat:recovery:>=13 basestat:discipline:>=13) or\n (basestat:mobility:>=13 basestat:recovery:>=13 basestat:intellect:>=13) or\n (basestat:mobility:>=13 basestat:recovery:>=13 basestat:strength:>=13) or\n (basestat:mobility:>=13 basestat:discipline:>=13 basestat:intellect:>=13) or\n (basestat:mobility:>=13 basestat:discipline:>=13 basestat:strength:>=13) or\n (basestat:mobility:>=13 basestat:intellect:>=13 basestat:strength:>=13) or\n (basestat:resilience:>=13 basestat:recovery:>=13 basestat:discipline:>=13) or\n (basestat:resilience:>=13 basestat:recovery:>=13 basestat:intellect:>=13) or\n (basestat:resilience:>=13 basestat:recovery:>=13 basestat:strength:>=13) or\n (basestat:resilience:>=13 basestat:discipline:>=13 basestat:intellect:>=13) or\n (basestat:resilience:>=13 basestat:discipline:>=13 basestat:strength:>=13) or\n (basestat:resilience:>=13 basestat:intellect:>=13 basestat:strength:>=13) or\n (basestat:recovery:>=13 basestat:discipline:>=13 basestat:intellect:>=13) or\n (basestat:recovery:>=13 basestat:discipline:>=13 basestat:strength:>=13) or\n (basestat:recovery:>=13 basestat:intellect:>=13 basestat:strength:>=13) or\n (basestat:discipline:>=13 basestat:intellect:>=13 basestat:strength:>=13))\n )\n \n -(basestat:mobility:>=8 basestat:resilience:>=8 basestat:recovery:>=8 basestat:discipline:>=8 basestat:intellect:>=8 basestat:strength:>=8)\n )\n \n or\n (is:classitem ((is:dupelower -is:modded) or (is:sunset))) \n or\n (is:armor -powerlimit:>1060) \n or\n (is:armor is:blue)\n )\n -tag:keep -tag:archive -tag:favorite -tag:infuse -is:maxpower -power:>=1260 -is:inloadout -is:masterwork\n )`, ], // Plaintext special case [`not forgotten`], [`not (forgotten)`], [`not "forgotten"`], [`gnawing hunger`], // Comments [`/* My cool search */ is:armor`], [` /* My cool search */\n is:armor`], [ `/* My cool search */ (/* armor */ is:armor and is:blue) or (/*weapons*/ is:weapon and perkname:"Kill Clip")`, ], [` `], ]; // Each of these asserts that the first query is the same as the second query once parsed const equivalentSearches = [ [ `is:blue is:weapon or is:armor not:maxpower`, `is:blue and (is:weapon or is:armor) and -is:maxpower`, ], [`not forgotten`, `-"forgotten"`], [`cluster tracking`, `"cluster" and "tracking"`], [ `is:weapon and is:sniperrifle or not is:armor and modslot:arrival`, `(is:weapon and is:sniperrifle) or (-is:armor and modslot:arrival)`, ], [ `is:weapon is:sniperrifle or not is:armor modslot:arrival`, `is:weapon and (is:sniperrifle or -is:armor) and modslot:arrival`, ], [ `is:weapon is:sniperrifle or is:armor and modslot:arrival`, `is:weapon and (is:sniperrifle or (is:armor and modslot:arrival))`, ], [ `is:rocketlauncher perk:"cluster" or perk:"tracking module"`, `is:rocketlauncher (perk:"cluster" or perk:"tracking module")`, ], [`is:blue (is:rocketlauncher`, `is:blue is:rocketlauncher`], [` is:blue `, `is:blue`], ]; // Test what we generate as the canonical form of queries. The first is the input, // the second is the canonical version const canonicalize = [ [`is:blue is:haspower not:maxpower`, `is:blue is:haspower -is:maxpower`], [ `is:weapon and is:sniperrifle or not is:armor and modslot:arrival`, `(is:weapon is:sniperrifle) or (-is:armor modslot:arrival)`, ], [ `is:rocketlauncher perk:"cluster" or perk:'tracking module'`, `is:rocketlauncher (perk:cluster or perk:"tracking module")`, ], [`( power:>1000 and -modslot:arrival ) `, `power:>1000 -modslot:arrival`], [`food fight`, `food and fight`], [`/* My cool search */\n is:armor`, `/* my cool search */ is:armor`], [ `/* My cool search */ (/* armor */ is:armor and is:blue) or (/*weapons*/ is:weapon and perkname:"Kill Clip")`, `/* my cool search */ (is:armor is:blue) or (is:weapon perkname:"kill clip")`, ], [`inloadout:"----<()>fast"`, `inloadout:"----<()>fast"`], [`perkname:"foobar"`, `perkname:foobar`], [`perkname:'foo bar'`, `perkname:"foo bar"`], [`perkname:"foobar"`, `perkname:foobar`], [`perkname:'foo"bar'`, `perkname:'foo"bar'`], [`perkname:"foo\\"bar"`, `perkname:'foo"bar'`], [`perkname:'foo\\"ba\\'r'`, `perkname:"foo\\"ba'r"`], ]; // Test that we can quote a string, parse it back as part of a search, and get the original string const quotes = [ [`foobar`], [`Foo\\bar`], [`My cool loadout`], [`My "cool" loadout`], [`My "cool" loadout's little brother`], [`My "cool" load\\out's little brother`], ]; test.each(cases)('parse |%s|', (query) => { // Test just the lexer const tokens: Token[] = []; for (const t of lexer(query)) { tokens.push(t); } expect( tokens.map((t) => { switch (t.type) { case 'comment': return [t.type, t.content]; case 'filter': return [t.type, t.keyword, t.args]; default: return [t.type]; } }), ).toMatchSnapshot('lexer'); // Test the full parse tree const ast = parseQuery(query); expect(ast).toMatchSnapshot('ast'); }); test.each(equivalentSearches)('|%s| is equivalent to |%s|', (firstQuery, secondQuery) => { const firstAST = parseQuery(firstQuery); const secondAST = parseQuery(secondQuery); expect(stripIndexes(firstAST)).toEqual(stripIndexes(secondAST)); }); /** Remove the startIndex and length from the AST to make them comparable */ function stripIndexes(ast: QueryAST): AndOp | OrOp | NotOp | FilterOp | NoOp { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access delete (ast as any).startIndex; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access delete (ast as any).length; switch (ast.op) { case 'and': case 'or': for (const op of ast.operands) { stripIndexes(op); } break; case 'not': stripIndexes(ast.operand); break; default: break; } return ast; } test.each(canonicalize)('|%s| is canonically |%s|', (query, expectedCanonical) => { const canonicalized = canonicalizeQuery(parseQuery(query)); expect(canonicalized).toEqual(expectedCanonical); }); test.each(quotes)('|%s| quoting roundtrip', (str) => { const quoted = quoteFilterString(str); const ast = parseQuery(`name:${quoted}`); if (ast.op === 'filter') { expect(ast.args).toEqual(str.toLowerCase()); } else { throw new Error(`Failed: ${quoted}`); } }); ================================================ FILE: src/app/search/query-parser.ts ================================================ /* ; Lazy BNF diagram of our search grammar <query> ::= <term> | <term> <term> <clause> ::= <opt-whitespace> <clause> <terms> ::= <term> { " " <term>} <term> ::= <string> | <filter> | <group> | <boolean> <filter> ::= ["-"]<filterName>:<filterValue>[<operator><number>] <filterName> ::= <the set of known filter names - is, notes, perks, tag, stat, etc.> <filterValue> ::= <keyword> | "stat:" <statName> | <string> <keyword> ::= <the set of known keyword filters - locked, sniperrifle, tagged, etc.> <statName> ::= <the set of known stat keywords - charge, impact, resilience, etc.> <operator> ::= "none" | "=" | "<" | "<=" | ">" | ">=" ; Numbers are positive only, but don't need to be <number> ::= DIGIT{DIGIT} <group> ::= "(" <query> ")" <boolean> ::= "or" | "not" | "and" ; Strings are basically anything within matching quotes, either single or double <string> ::= WORD | "\"" WORD {" " WORD} "\"" | "'" WORD {" " WORD} "'\"'" */ import { convertToError } from 'app/utils/errors'; import { escapeQuotes, normalizeQuotes, unescapedDoubleQuoteCharacters, unescapedSingleQuoteCharacters, } from './text-utils'; /* **** Parser **** */ interface QueryASTCommon { error?: Error; comment?: string; /** The beginning index of the query string where this was found. */ startIndex: number; /** The length of the portion of the query string that this operator consists of, including its sub-expressions/operands. */ length: number; } /** * A tree of the parsed query. Boolean/unary operators have children (operands) that * describe their relationship. */ export type QueryAST = AndOp | OrOp | NotOp | FilterOp | NoOp; /** If ALL of of the operands are true, this resolves to true. There may be any number of operands. */ export interface AndOp extends QueryASTCommon { op: 'and'; operands: QueryAST[]; } /** If any of the operands is true, this resolves to true. There may be any number of operands. */ export interface OrOp extends QueryASTCommon { op: 'or'; operands: QueryAST[]; } /** An operator which negates the result of its only operand. */ export interface NotOp extends QueryASTCommon { op: 'not'; operand: QueryAST; } /** This represents one of our filter function definitions, such as is:, season:, etc. */ export interface FilterOp extends QueryASTCommon { op: 'filter'; /** * The name of the filter function, without any trailing :. The only weird case is * stats, which will appear like "stat:strength". */ type: string; /** * Any arguments to the filter function as a single string. e.g: haspower, arrivals, >=1000 */ args: string; } /** This is mostly for error cases and empty string */ export interface NoOp extends QueryASTCommon { op: 'noop'; } /** * The lexer is implemented as a generator, but generators don't support peeking without advancing * the iterator. This wraps the generator in an object that buffers the next element if you call peek(). */ class PeekableGenerator<T> { private gen: Generator<T>; private next: T | undefined; constructor(gen: Generator<T>) { this.gen = gen; } /** * Get what the next item from the generator will be, without advancing it. */ peek(): T | undefined { if (!this.next) { const n = this.gen.next(); if (!n.done) { this.next = n.value; } } return this.next; } /** * Get the next element from the generator and advance it to the next element. */ pop(): T | undefined { if (this.next) { const ret = this.next; this.next = undefined; return ret; } const n = this.gen.next(); if (!n.done) { return n.value; } } } /** * A table of operator precedence for our three binary operators. Operators with higher precedence group together * before those with lower precedence. The "op" property maps them to an AST node. */ const operators = { // The implicit `and` (two statements separated by whitespace) has lower precedence than either the explicit or or and. implicit_and: { precedence: 1, op: 'and', }, or: { precedence: 2, op: 'or', }, and: { precedence: 3, op: 'and', }, } as const; /** * The query parser first lexes the string, then parses it into an AST (abstract syntax tree) * representing the logical structure of the query. This AST can then be walked to match up * to defined filters and generate an actual filter function. * * We choose to produce an AST instead of executing the search inline with parsing both to * make testing easier, and to allow for things like canonicalization of search queries. */ export function parseQuery(query: string): QueryAST { // This implements operator precedence via this mechanism: // https://eli.thegreenplace.net/2012/08/02/parsing-expressions-by-precedence-climbing /** * This extracts the next "atom" aka "value" from the token stream. An atom is either * an individual filter expression, or a grouped expression. Basically anything that's * not a binary operator. "not" is also in here because it's really just a modifier on an atom. */ function parseAtom(tokens: PeekableGenerator<Token>): QueryAST { const token: Token | undefined = tokens.pop(); if (!token) { throw new Error('expected an atom'); } switch (token.type) { case 'filter': { const keyword = token.keyword; if (keyword === 'not') { // `not:` a synonym for `-is:`. We could fix this up in filter execution but I chose to normalize it here. return { op: 'not', operand: { op: 'filter', type: 'is', args: token.args, startIndex: token.startIndex, length: token.length, }, startIndex: token.startIndex, length: token.length, }; } else { return { op: 'filter', type: keyword, args: token.args, startIndex: token.startIndex, length: token.length, }; } } case 'not': { // The operand should always be an atom const operand = parseAtom(tokens); return { op: 'not', operand, startIndex: token.startIndex, length: token.length + operand.length, }; } case '(': { const result = parse(tokens); result.length += result.startIndex - token.startIndex; result.startIndex = token.startIndex; if (tokens.peek()?.type === ')') { const closeParen = tokens.pop(); result.length += closeParen!.length; } return result; } case 'comment': { const comment = token.content; const next = parseAtom(tokens); return { ...next, comment: comment, startIndex: next.startIndex, length: next.length, }; } default: throw new Error( `Unexpected token type, looking for an atom: ${JSON.stringify(token)}, ${query}`, ); } } /** * Parse a stream of tokens into an AST. `minPrecedence` determined the minimum operator precedence * of operators that will be included in this portion of the parse. */ function parse(tokens: PeekableGenerator<Token>, minPrecedence = 1): QueryAST { let ast: QueryAST = { op: 'noop', startIndex: 0, length: 0 }; try { ast = parseAtom(tokens); let token: Token | undefined; while ((token = tokens.peek())) { if (token.type === ')') { break; } const operator = operators[token.type as keyof typeof operators]; if (!operator) { throw new Error(`Expected an operator, got ${JSON.stringify(token)}`); } else if (operator.precedence < minPrecedence) { break; } tokens.pop(); const nextMinPrecedence = operator.precedence + 1; // all our operators are left-associative const rhs = parse(tokens, nextMinPrecedence); // Our operators allow for more than 2 operands, to avoid deep logic trees. // This logic tries to combine them where possible. if (isSameOp(operator.op, ast)) { ast.operands.push(rhs); ast.length += rhs.length; } else { const title = ast.comment; delete ast.comment; ast = { op: operator.op, operands: isSameOp(operator.op, rhs) ? [ast, ...rhs.operands] : [ast, rhs], startIndex: Math.min(rhs.startIndex, ast.startIndex, token.startIndex), length: ast.length + rhs.length + token.length, }; if (title) { ast.comment = title; } } } } catch (e) { ast.error = convertToError(e); } return ast; } const tokens = new PeekableGenerator(lexer(query)); try { if (!tokens.peek()) { return { op: 'noop', startIndex: 0, length: 0 }; } } catch (e) { return { op: 'noop', error: convertToError(e), startIndex: 0, length: 0 }; } const ast = parse(tokens); return ast; } function isSameOp<T extends 'and' | 'or'>(binOp: T, op: QueryAST): op is AndOp | OrOp { return binOp === op.op; } /* **** Lexer **** */ // Lexer token types type NoArgTokenType = '(' | ')' | 'not' | 'or' | 'and' | 'implicit_and'; export type Token = { startIndex: number; length: number; quoted?: boolean } & ( | { type: NoArgTokenType } | { type: 'filter'; keyword: string; args: string } | { type: 'comment'; content: string } ); // Parens: `(` can be followed by whitespace, while `)` can be preceded by it const parens = /(\(\s*|\s*\))/y; // A `-` followed by any amount of whitespace is the same as "not" const negation = /-\s*/y; // `not`, `or`, and `and` keywords. or and not can be preceded by whitespace, and any of them can be followed by whitespace. // `not` can't be preceded by whitespace because that whitespace is an implicit `and`. const booleanKeywords = /(not|\s+or|\s+and)\s+/y; // Filter names like is:, stat:, etc const filterName = /[a-z]+:/y; // Arguments to filters are pretty unconstrained const filterArgs = /[^\s()]+/y; // Words without quotes are basically any non-whitespace that doesn't terminate a group const bareWords = /[^\s)]+/y; // Whitespace that doesn't match anything else is an implicit `and` const whitespace = /\s+/y; const comment = /\/\*(.*?)\*\/\s*/y; export function makeCommentString(text: string) { return `/* ${text} */`; } export class QueryLexerError extends Error { // The index and length of the range within the query string where the error occurred startIndex: number; length: number; constructor(message: string, startIndex: number, length: number) { super(message); this.startIndex = startIndex; this.length = length; this.name = 'QueryLexerError'; } } /** A special version of QueryLexerError for when quotes aren't closed. */ export class QueryLexerOpenQuotesError extends QueryLexerError { constructor(message: string, startIndex: number, length: number) { super(message, startIndex, length); this.name = 'QueryLexerError'; } } /** * The lexer yields a series of tokens representing the linear structure of the search query. * This throws an exception if it finds an invalid input. * * Example: "is:blue -is:maxpower" turns into: * ["filter", "is", "blue"], ["implicit_and"], ["not"], ["filter", "is", "maxpower"] */ export function* lexer(query: string): Generator<Token> { query = query.toLowerCase(); query = normalizeQuotes(query); let match: string | undefined; let i = 0; const consume = (str: string) => (i += str.length); /** * If `query` matches `re` starting at `i`, return the matched portion of the string. Otherwise return undefined. * This avoids having to make slices of strings just to start the regex in the middle of a string. * * Note that regexes passed to this must have the "sticky" flag set (y) and should not use ^, which will match the * beginning of the string, ignoring the index we want to start from. The sticky flag ensures our regex will match * based on the beginning of the string. */ const extract = (re: RegExp): string | undefined => { // These checks only run in unit tests if ($DIM_FLAVOR === 'test') { if (!re.sticky) { throw new Error('regexp must be sticky'); } if (re.source.startsWith('^')) { throw new Error('regexp cannot start with ^ and be repositioned'); } } re.lastIndex = i; const match = re.exec(query); if (match) { const result = match[0]; if (result.length > 0) { consume(result); return match.length > 1 ? match[1] : result; } } return undefined; }; /** * Consume and return the contents of a quoted string. */ const consumeString = (startingQuoteChar: string) => { const initial = i; // Quoted string consume(startingQuoteChar); let str = ''; while (i < query.length) { const char = query[i]; consume(char); // Handle character escapes e.g. \", \', \\ if (char === '\\') { const escapeStart = i; if (i < query.length) { const escaped = query[i]; if (escaped === '"' || escaped === "'" || escaped === '\\') { str += escaped; consume(escaped); } else { throw new QueryLexerError( `Unrecognized escape sequence \\${escaped}`, escapeStart, i - escapeStart, ); } } else { str = str + char; } } else if (char === startingQuoteChar) { return str; } else { str = str + char; } } throw new QueryLexerOpenQuotesError( `Unterminated quotes: |${query.slice(initial)}| ${initial}`, initial, i - initial, ); }; while (i < query.length) { const char = query[i]; const startIndex = i; if ((match = extract(parens)) !== undefined) { // Start/end group yield { startIndex, length: i - startIndex, type: match.trim() as NoArgTokenType }; } else if (char === '"' || char === "'") { const quotedString = consumeString(char); // Quoted string yield { startIndex, length: i - startIndex, type: 'filter', keyword: 'keyword', args: quotedString, quoted: true, }; } else if (extract(negation) !== undefined) { // minus sign is the same as "not" yield { startIndex, length: i - startIndex, type: 'not' }; } else if ((match = extract(booleanKeywords)) !== undefined) { // boolean keywords yield { startIndex, length: i - startIndex, type: match.trim() as NoArgTokenType }; } else if ((match = extract(comment)) !== undefined) { yield { startIndex, length: i - startIndex, type: 'comment', content: match.trim(), }; } else if ((match = extract(filterName)) !== undefined) { // Keyword searches - is:, stat:discipline:, etc const keyword = match.slice(0, match.length - 1); const nextChar = query[i]; let args: string; let quoted = false; if (nextChar === '"' || nextChar === "'") { try { quoted = true; args = consumeString(nextChar); } catch (e) { if (e instanceof QueryLexerOpenQuotesError) { // Rethrow but include the filter prefix (e.g. name:) in the range throw new QueryLexerOpenQuotesError(e.message, startIndex, e.length + match.length); } else { throw e; } } } else if ((match = extract(filterArgs)) !== undefined) { args = match; } else { throw new QueryLexerError( `missing keyword arguments for ${keyword}`, startIndex, query.length - startIndex, ); } yield { startIndex, length: i - startIndex, type: 'filter', keyword, args, quoted, }; } else if ((match = extract(bareWords)) !== undefined) { // bare words that aren't keywords are effectively "keyword" type filters yield { startIndex, length: i - startIndex, type: 'filter', keyword: 'keyword', args: match, }; } else if (extract(whitespace) !== undefined) { // Ignore whitespace at the beginning and end of the string if (startIndex !== 0 && i !== query.length) { yield { startIndex, length: i - startIndex, type: 'implicit_and' }; } } else { throw new QueryLexerError( `unrecognized tokens: |${query.slice(i)}| ${i}`, startIndex, query.length - startIndex, ); } if (startIndex === i) { throw new Error('bug: forgot to consume characters'); } } } const quoteNeedingCharacters = /[\s()]/; /** * Quote a string if it's needed. * * @example * * quoteFilterString("foo bar") => "\"foo bar\"" * quoteFilterString("foobar") => "foobar" * quoteFilterString("foo\"bar") => foobar" */ export function quoteFilterString(arg: string) { const hasSingle = unescapedSingleQuoteCharacters.test(arg); const hasDouble = unescapedDoubleQuoteCharacters.test(arg); const hasOthers = quoteNeedingCharacters.test(arg); if (!hasSingle && !hasDouble && !hasOthers) { return arg; } // When text is wrapped in quotes, the lexer begins watching for these escape sequences: // \" \' \\ // and throws an error on anything else following a backslash. // Now that quoteFilterString is committed to adding quotes, // it escapes existing backslashes so they are treated as just backslashes. arg = arg.replaceAll('\\', '\\\\'); let quoteChar: string; // As long as one quote type is safe to add, wrapping the string with it defuses everything, including Other symbols if (!hasDouble || !hasSingle) { quoteChar = hasDouble ? `'` : `"`; } else { // Reaching here means there's both types of quotes. Choose to use double quotes, and escape existing ones. quoteChar = `"`; arg = escapeQuotes(arg, true); } return `${quoteChar}${arg}${quoteChar}`; } /** * Build a standardized version of the query as a string. This is useful for deduping queries. * Example: 'is:weapon and is:sniperrifle or not is:armor and modslot:arrival' => * '(-is:armor modslot:arrival) or (is:sniperrifle is:weapon)' */ export function canonicalizeQuery(query: QueryAST, depth = 0): string { const result = (() => { switch (query.op) { case 'filter': return query.type === 'keyword' ? quoteFilterString(query.args) : `${query.type}:${quoteFilterString(query.args)}`; case 'not': return `-${canonicalizeQuery(query.operand, depth + 1)}`; case 'and': case 'or': { const joinedOperands = query.operands .map((q) => canonicalizeQuery(q, depth + 1)) .join( query.op === 'and' && !query.operands.some((op) => op.op === 'filter' && op.type === 'keyword') ? ' ' : ` ${query.op} `, ); return depth === 0 ? joinedOperands : `(${joinedOperands})`; } case 'noop': return ''; } })(); // Only preserve the top-level comment if (query.comment && depth === 0) { return `${makeCommentString(query.comment)} ${result}`; } return result; } ================================================ FILE: src/app/search/search-config.test.ts ================================================ import { canonicalFilterFormats } from './filter-types'; import { buildItemFiltersMap } from './items/item-search-filter'; import { parseAndValidateQuery } from './search-filter'; describe('buildSearchConfig', () => { const searchConfig = buildItemFiltersMap(2); test('generates a reasonable filter map', () => { expect(Object.keys(searchConfig.isFilters).sort()).toMatchSnapshot('is filters'); expect(Object.keys(searchConfig.kvFilters).sort()).toMatchSnapshot('key-value filters'); }); test('filter formats specify unambiguous formats ', () => { /* * We have a bunch of filter formats for which `keyword:value` * with purely alphabetic values can be valid syntax. Filters should * avoid specifying more than one of these. * query and freeform filters are sort of the same thing, * except queries are exhaustive and freeform aren't. Overloaded * range filters can also accept single words as filter value, * because `season:worthy` is actually `season:10` and we don't * want these to be mistaken for queries or freeforms. */ for (const filter of searchConfig.allFilters) { let formats = canonicalFilterFormats(filter.format); if (formats.length < 1) { throw new Error(`filter ${filter.keywords.toString()} has no formats`); } formats = formats.filter( (f) => f === 'query' || f === 'freeform' || (f === 'range' && filter.overload), ); if (formats.length > 1) { throw new Error( `filter ${filter.keywords.toString()} specifies ambiguous formats ${formats.toString()}`, ); } } }); }); /** * The purpose of this test is to verify that `validateQuery` understands the syntax * formats in filter definitions, accepts valid queries and rejects invalid syntax. * We use well-known filters from the D2 search config for this, but testing all filters * exhaustively is not a goal of this test. */ describe('validateQuery', () => { const searchConfig = buildItemFiltersMap(2); const simpleCases: [filterString: string, valid: boolean][] = [ ['is:crafted', true], ['not:crafted', true], ['crafted:is', false], ]; test.each(simpleCases)('is: filter %s - validity %s', (filterString, valid) => expect(parseAndValidateQuery(filterString, searchConfig).valid).toBe(valid), ); const queryCases: [filterString: string, valid: boolean][] = [ ['tag:favorite', true], ['tag:none', true], ['tag:any', false], ['is:tag', false], ['tag:<5', false], ['tag:recovery:17', false], ]; test.each(queryCases)('query filter %s - validity %s', (filterString, valid) => expect(parseAndValidateQuery(filterString, searchConfig).valid).toBe(valid), ); const freeformCases: [filterString: string, valid: boolean][] = [ ['notes:#hashtag', true], ['notes:verbatim:colon', true], ['notes"with spaces"', true], ['notes:<5', true], ['is:notes', false], ]; test.each(freeformCases)('freeform filter %s - validity %s', (filterString, valid) => expect(parseAndValidateQuery(filterString, searchConfig).valid).toBe(valid), ); // `masterwork` is a complicated filter with three different formats const mixedCases: [filterString: string, valid: boolean][] = [ ['is:masterwork', true], ['masterwork:range', true], ['masterwork:<5', true], ['masterwork:=5', true], ['masterwork:5', true], ['masterwork:rnage', false], ['masterwork:<range', false], ['masterwork:range:5', false], ]; test.each(mixedCases)('mixed filter %s - validity %s', (filterString, valid) => expect(parseAndValidateQuery(filterString, searchConfig).valid).toBe(valid), ); const statCases: [filterString: string, valid: boolean][] = [ ['stat:recovery:5', true], ['stat:recovery:<=5', true], ['stat:recovery+discipline:>=5', true], ['stat:highest&secondhighest:>20', true], ['stat:highest&recovery+strength&strength&range:>20', true], ['basestat:range:>50', true], ['stat:badstat:>20', false], ['stat:badstat&badcombo:>20', false], ['stat:too&+many:>20', false], ['is:stat', false], ['stat:recovery', false], ['stat:=5', false], ]; test.each(statCases)('stat filter %s - validity %s', (filterString, valid) => expect(parseAndValidateQuery(filterString, searchConfig).valid).toBe(valid), ); const rangeCases: [filterString: string, valid: boolean][] = [ ['count:2', true], ['count:=2', true], ['count:<=2', true], ['count:<=2.5', true], ['is:count', false], ['count:count', false], ['count:recovery', false], ['count:<=2:=2', false], ['count:<2>', false], ]; test.each(rangeCases)('search string %s - validity %s', (filterString, valid) => expect(parseAndValidateQuery(filterString, searchConfig).valid).toBe(valid), ); const overloadRangeCases: [filterString: string, valid: boolean][] = [ ['season:worthy', true], ['season:<=worthy', true], ['season:=worthy', true], ['season:>=arrivals', true], ['season:222', true], ['season:=10', true], ['season:>2.5', true], ['is:season', false], // DIM used to parse this as `season:11` because `redwar` is `1`... ['season:1redwar', false], ['season:season', false], ['season:arrival', false], ['season:arrivalworthy', false], ]; test.each(overloadRangeCases)('search string %s - validity %s', (filterString, valid) => expect(parseAndValidateQuery(filterString, searchConfig).valid).toBe(valid), ); }); ================================================ FILE: src/app/search/search-config.ts ================================================ import { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { DimLanguage } from 'app/i18n'; import { FilterDefinition, canonicalFilterFormats } from './filter-types'; import { generateSuggestionsForFilter } from './suggestions-generation'; import { plainString } from './text-utils'; // // SearchConfig // export interface FiltersMap<I, FilterCtx, SuggestionsCtx> { allFilters: FilterDefinition<I, FilterCtx, SuggestionsCtx>[]; /* `is:keyword` filters */ isFilters: Record<string, FilterDefinition<I, FilterCtx, SuggestionsCtx>>; /* `keyword:value` filters */ kvFilters: Record<string, FilterDefinition<I, FilterCtx, SuggestionsCtx>>; } export interface Suggestion { /** The original suggestion text. */ rawText: string; /** The plainString'd version (with diacritics removed, if applicable). */ plainText: string; } export interface SearchConfig<I, FilterCtx, SuggestionsCtx> { filtersMap: FiltersMap<I, FilterCtx, SuggestionsCtx>; language: DimLanguage; suggestions: Suggestion[]; } export function buildFiltersMap<I, FilterCtx, SuggestionsCtx>( destinyVersion: DestinyVersion, allFilters: FilterDefinition<I, FilterCtx, SuggestionsCtx>[], ): FiltersMap<I, FilterCtx, SuggestionsCtx> { const isFilters: Record<string, FilterDefinition<I, FilterCtx, SuggestionsCtx>> = {}; const kvFilters: Record<string, FilterDefinition<I, FilterCtx, SuggestionsCtx>> = {}; const allApplicableFilters: FilterDefinition<I, FilterCtx, SuggestionsCtx>[] = []; for (const filter of allFilters) { if (!filter.destinyVersion || filter.destinyVersion === destinyVersion) { allApplicableFilters.push(filter); const filterKeywords = Array.isArray(filter.keywords) ? filter.keywords : [filter.keywords]; const filterFormats = canonicalFilterFormats(filter.format); const hasSimple = filterFormats.some((f) => f === 'simple'); const hasKv = filterFormats.some((f) => f !== 'simple'); for (const keyword of filterKeywords) { if (hasSimple) { if ($DIM_FLAVOR === 'test' && isFilters[keyword]) { throw new Error( `Conflicting is:${keyword} filter -- only the last inserted filter will work.`, ); } isFilters[keyword] = filter; } if (hasKv) { if ($DIM_FLAVOR === 'test' && kvFilters[keyword]) { throw new Error( `Conflicting ${keyword}:value filter -- only the last inserted filter will work.`, ); } kvFilters[keyword] = filter; } } } } return { isFilters, kvFilters, allFilters: allApplicableFilters, }; } /** Builds an object that describes the available search keywords and filter definitions. */ export function buildSearchConfig<I, FilterCtx, SuggestionsCtx>( language: DimLanguage, suggestionsContext: SuggestionsCtx, filtersMap: FiltersMap<I, FilterCtx, SuggestionsCtx>, ): SearchConfig<I, FilterCtx, SuggestionsCtx> { const suggestions = new Set<string>(); for (const filter of filtersMap.allFilters) { for (const suggestion of generateSuggestionsForFilter(filter, suggestionsContext)) { suggestions.add(suggestion); } } return { filtersMap, suggestions: Array.from(suggestions, (rawText) => ({ rawText, plainText: plainString(rawText, language), })), language, }; } ================================================ FILE: src/app/search/search-filter-values.test.ts ================================================ import { itemStatAllowList } from 'app/inventory/store/stats'; import { statHashByName } from 'app/search/search-filter-values'; import { invert } from 'app/utils/collections'; it('Should have a search filter for every allowed stat', () => { const searchFilterForStat: Record<number, string> = invert(statHashByName); for (const stat of itemStatAllowList) { expect(searchFilterForStat).toHaveProperty(stat.toString()); } }); ================================================ FILE: src/app/search/search-filter-values.ts ================================================ import { StringLookup } from 'app/utils/util-types'; import { DamageType } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { D1BucketHashes, D1LightStats } from './d1-known-values'; import { D2ArmorStatHashByName, D2LightStats, D2WeaponStatHashByName, TOTAL_STAT_HASH, armorStats, realD2ArmorStatHashByName, } from './d2-known-values'; // ✨ magic values ✨ // this file has non-programatically decided information // hashes, names, & enums, hand-crafted and chosen by us // this correlation is solely for element filter names export const damageNamesByEnum: { [key in DamageType]: string | undefined } = { [DamageType.None]: undefined, [DamageType.Kinetic]: 'kinetic', [DamageType.Arc]: 'arc', [DamageType.Thermal]: 'solar', [DamageType.Void]: 'void', [DamageType.Raid]: 'raid', [DamageType.Stasis]: 'stasis', [DamageType.Strand]: 'strand', }; // typescript doesn't understand array.filter export const damageTypeNames = Object.values(damageNamesByEnum).filter( (d) => d && d !== 'raid', ) as string[]; /** * these stats exist on DIM armor. the 6 real API ones, supplemented by a synthetic Total stat. * these are the armor stats that can be looked up by name */ export const dimArmorStatHashByName: StringLookup<number> = { ...D2ArmorStatHashByName, total: TOTAL_STAT_HASH, }; /** stats names used to create armor-specific filters, real ones plus an "any" keyword */ export const searchableArmorStatNames = [...Object.keys(dimArmorStatHashByName), 'any']; /** armor stat hashes to check for the "any" keyword */ export const armorAnyStatHashes = armorStats; /** armor 3.0 stat names including "primary" "secondary" and "tertiary" for filtering */ export const armor3OrdinalIndexByName: StringLookup<number> = { primary: 0, secondary: 1, tertiary: 2, }; export const searchableD2Armor3StatNames = [ ...Object.keys(realD2ArmorStatHashByName), ...Object.keys(armor3OrdinalIndexByName), 'unfocused', ]; /** stat hashes to calculate max values for */ export const armorStatHashes = Object.values(dimArmorStatHashByName) as number[]; /** all-stat table, for looking up stat hashes given a queried stat name */ export const statHashByName: Record<string, number> = { ...D2WeaponStatHashByName, ...dimArmorStatHashByName, }; /** Lowercase, sometimes-abbreviated stat names, used in search filters. */ export const weaponStatNames = Object.keys(D2WeaponStatHashByName); /** all-stat list, to generate filters from */ export const allStatNames = [...Object.keys(statHashByName), 'any']; // Support (for armor) these aliases for the stat in the nth rank export const est = { highest: 0, secondhighest: 1, thirdhighest: 2, fourthhighest: 3, fifthhighest: 4, sixthhighest: 5, } as const; export const estStatNames = Object.keys(est); export const statOrdinals: StringLookup<number> = { primarystat: 0, secondarystat: 1, tertiarystat: 2, }; export const allAtomicStats = [...allStatNames, ...estStatNames]; export const lightStats = [...D2LightStats, ...D1LightStats]; /** compare against DimItem.bucket.hash */ export const cosmeticTypes: (BucketHashes | D1BucketHashes)[] = [ BucketHashes.Modifications, BucketHashes.Emotes, BucketHashes.Emblems, BucketHashes.Vehicle, D1BucketHashes.Horn, BucketHashes.Ships, BucketHashes.Finishers, ]; ================================================ FILE: src/app/search/search-filter.scss ================================================ @use '../variables.scss' as *; @layer base { .search-filter { display: flex; flex-direction: row; align-items: center; background-color: var(--theme-sheet-search-bg); padding-left: 5px; padding-right: 5px; height: $search-bar-height; border-radius: $theme-corner-radius-search; min-width: 350px; box-sizing: border-box; @include phone-portrait { width: auto; height: 34px; } @include interactive($hover: true, $focusWithin: true) { outline: none; box-shadow: inset 0 0 0 1px var(--theme-search-dropdown-border); } ::placeholder { color: #999; } > input[type='text'] { all: initial; outline: none; flex: 1; width: 0; padding: 7px 0; font-size: inherit; font-family: inherit; color: inherit; caret-color: var(--theme-accent-primary); } } .search-bar-icon { color: #999; margin: 0 6px 0 4px; font-size: 12px !important; } .mobile-search-link { display: block; width: 100%; margin: 0 !important; box-sizing: border-box; padding: 0 4px; background: var(--theme-mobile-background); @include phone-portrait { padding-bottom: 4px; } } } ================================================ FILE: src/app/search/search-filter.test.ts ================================================ import { ItemFilterDefinition } from './items/item-filter-types'; import { rangeStringToComparator } from './search-filter'; import { allStatNames, searchableArmorStatNames } from './search-filter-values'; import { generateSuggestionsForFilter } from './suggestions-generation'; describe('generateSuggestionsForFilter', () => { const cases: [ format: ItemFilterDefinition['format'], keywords: ItemFilterDefinition['keywords'], suggestions: ItemFilterDefinition['suggestions'], overload: { [key: string]: number } | undefined, ][] = [ [undefined, ['a', 'b', 'c'], undefined, undefined], ['query', 'a', ['b', 'c'], undefined], ['stat', 'a', ['b', 'c'], undefined], ['range', 'a', undefined, undefined], ['range', 'a', undefined, { worthy: 10, arrivals: 11 }], ['freeform', 'a', ['b', 'c'], undefined], [undefined, ['a'], undefined, undefined], ['stat', 'stat', allStatNames, undefined], ['query', 'maxstatvalue', searchableArmorStatNames, undefined], ['query', 'maxstatvalue', searchableArmorStatNames, undefined], ['range', 'energycapacity', undefined, undefined], ]; test.each(cases)( "full suggestions for filter format '%s', keyword '%s' with suggestions %s, overload %s", ( format: ItemFilterDefinition['format'], keywords: string | string[], suggestions?: string[], overload?: { [key: string]: number }, ) => { const candidates = generateSuggestionsForFilter( { format, keywords, suggestions, overload, }, {}, ); expect(candidates).toMatchSnapshot(); }, ); }); describe('rangeStringToComparator', () => { const cases: [input: string, reference: number, result: boolean][] = [ ['<10', 8, true], ['<10', 10, false], ['>10', 10, false], ['>10', 15, true], ['<=10', 10, true], ['>=10', 10, true], ['<=10', 8, true], ['>=10', 15, true], ['<=10', 15, false], ['>=10', 8, false], ['=10', 10, true], ['=10', 15, false], ['10', 10, true], ]; test.each(cases)( "rangeStringToComparator('%s')(%d) === %s", (input: string, reference: number, result: boolean) => { const fn = rangeStringToComparator(input); expect(fn(reference)).toBe(result); }, ); }); describe('rangeStringToComparatorWithOverloads', () => { const overloads = { worthy: 10, arrivals: 11 }; const cases: [input: string, reference: number, result: boolean][] = [ ['<worthy', 8, true], ['<worthy', 10, false], ['>10', 10, false], ['>10', 15, true], ['<=worthy', 10, true], ['>=worthy', 10, true], ['=worthy', 10, true], ['=worthy', 15, false], ['arrivals', 11, true], ['arrivals', 10, false], ]; test.each(cases)( "rangeStringToComparatorWithOverloads('%s')(%d) === %s", (input: string, reference: number, result: boolean) => { const fn = rangeStringToComparator(input, overloads); expect(fn(reference)).toBe(result); }, ); }); ================================================ FILE: src/app/search/search-filter.ts ================================================ import { filterMap } from 'app/utils/collections'; import { stubTrue } from 'app/utils/functions'; import { errorLog } from 'app/utils/log'; import { FilterDefinition, ItemFilter, canonicalFilterFormats } from './filter-types'; import { QueryAST, canonicalizeQuery, parseQuery } from './query-parser'; import { FiltersMap, SearchConfig } from './search-config'; /** Build a function that can take query text and return a filter function from it. */ export function makeSearchFilterFactory<I, FilterCtx, SuggestionsCtx>( { filtersMap: { isFilters, kvFilters } }: SearchConfig<I, FilterCtx, SuggestionsCtx>, filterContext: FilterCtx, ) { return (query: string): ItemFilter<I> => { query = query.trim().toLowerCase(); if (!query.length) { // By default, show anything that doesn't have the archive tag return stubTrue; } const parsedQuery = parseQuery(query); // Transform our query syntax tree into a filter function by recursion. const transformAST = (ast: QueryAST): ItemFilter<I> | undefined => { switch (ast.op) { case 'and': { const fns = filterMap(ast.operands, transformAST); // Propagate filter errors return fns.length === ast.operands.length ? (item) => { for (const fn of fns) { if (!fn(item)) { return false; } } return true; } : undefined; } case 'or': { const fns = filterMap(ast.operands, transformAST); // Propagate filter errors return fns.length === ast.operands.length ? (item) => { for (const fn of fns) { if (fn(item)) { return true; } } return false; } : undefined; } case 'not': { const fn = transformAST(ast.operand); return fn && ((item) => !fn(item)); } case 'filter': { const filterName = ast.type; const filterValue = ast.args; if (filterName === 'is') { // "is:" filters are slightly special cased const filterDef = isFilters[filterValue]; if (filterDef) { try { return filterDef.filter({ lhs: filterName, filterValue, ...filterContext }); } catch (e) { // An `is` filter really shouldn't throw an error on filter construction... errorLog( 'search', 'internal error: filter construction threw exception', filterName, filterValue, e, ); } } return undefined; } else { const filterDef = kvFilters[filterName]; const matchedFilter = filterDef && matchFilter(filterDef, filterName, filterValue, filterContext); if (matchedFilter) { try { return matchedFilter(filterContext); } catch (e) { // If this happens, a filter declares more syntax valid than it actually accepts, which // is a bug in the filter declaration. errorLog( 'search', 'internal error: filter construction threw exception', filterName, filterValue, e, ); } } return undefined; } } case 'noop': return undefined; } }; // If our filter has any invalid parts, the search filter should match no items return transformAST(parsedQuery) ?? (() => false); }; } /** Matches a non-`is` filter syntax and returns a way to actually create the matched filter function. */ function matchFilter<I, FilterCtx, SuggestionsCtx>( filterDef: FilterDefinition<I, FilterCtx, SuggestionsCtx>, lhs: string, filterValue: string, currentFilterContext?: FilterCtx, ): ((args: FilterCtx) => ItemFilter<I>) | undefined { for (const format of canonicalFilterFormats(filterDef.format)) { switch (format) { case 'simple': { break; } case 'query': case 'multiquery': { if ( filterDef.suggestions!.includes(filterValue) || (format === 'multiquery' && filterValue.split('+').every((s) => filterDef.suggestions?.includes(s))) ) { return (filterContext) => filterDef.filter({ lhs, filterValue, ...filterContext, }); } else { break; } } case 'freeform': { return (filterContext) => filterDef.filter({ lhs, filterValue, ...filterContext }); } case 'range': { try { const compare = rangeStringToComparator(filterValue, filterDef.overload); return (filterContext) => filterDef.filter({ lhs, filterValue: '', compare, ...filterContext, }); } catch { break; } } case 'stat': { const [stat, rangeString] = filterValue.split(':', 2); try { const compare = rangeStringToComparator(rangeString, filterDef.overload); const validator = filterDef.validateStat?.(currentFilterContext); if (!validator || validator(stat)) { return (filterContext) => filterDef.filter({ lhs, filterValue: stat, compare, ...filterContext, }); } else { break; } } catch { break; } } case 'custom': // TODO: nothing here means that custom format filters CANNOT currently work. break; } } } const rangeStringRegex = /^([<=>]{0,2})(\d+(?:\.\d+)?)$/; const overloadedRangeStringRegex = /^([<=>]{0,2})(\w+)$/; /** * This turns a string like "<=2" into a function like (x)=>x <= 2. * The produced function returns false if it was fed undefined. */ export function rangeStringToComparator( rangeString?: string, overloads?: { [key: string]: number }, ) { if (!rangeString) { throw new Error('Missing range comparison'); } const [operator, comparisonValue] = extractOpAndValue(rangeString, overloads); switch (operator) { case '=': case '': return (compare: number | undefined) => compare !== undefined && compare === comparisonValue; case '<': return (compare: number | undefined) => compare !== undefined && compare < comparisonValue; case '<=': return (compare: number | undefined) => compare !== undefined && compare <= comparisonValue; case '>': return (compare: number | undefined) => compare !== undefined && compare > comparisonValue; case '>=': return (compare: number | undefined) => compare !== undefined && compare >= comparisonValue; } throw new Error(`Unknown range operator ${operator}`); } function extractOpAndValue(rangeString: string, overloads?: { [key: string]: number }) { const matchedOverloadString = rangeString.match(overloadedRangeStringRegex); if (matchedOverloadString && overloads && matchedOverloadString[2] in overloads) { return [matchedOverloadString[1], overloads[matchedOverloadString[2]]] as const; } const matchedRangeString = rangeString.match(rangeStringRegex); if (matchedRangeString) { return [matchedRangeString[1], parseFloat(matchedRangeString[2])] as const; } throw new Error("Doesn't match our range comparison syntax, or invalid overload"); } /** * Given a query and some configuration, parse the query and see if it's valid. This includes checking that the filters actually exist in the filtersMap. */ export function parseAndValidateQuery<I, FilterCtx, SuggestionsCtx>( query: string, filtersMap: FiltersMap<I, FilterCtx, SuggestionsCtx>, filterContext?: FilterCtx, ): { /** Is the query valid at all? */ valid: boolean; /** Can the user save this query? */ saveable: boolean; /** Should we automatically save this in search history? */ saveInHistory: boolean; /** The canonicalized version of the query */ canonical: string; } { let valid = true; let saveable = true; let saveInHistory = true; let canonical = query; try { const ast = parseQuery(query); if (!validateQuery(ast, filtersMap, filterContext)) { valid = false; } else { if (ast.op === 'noop' || (ast.op === 'filter' && ast.type === 'keyword')) { // don't save "trivial" single-keyword filters saveInHistory = false; } // Some sites have people save big lists of item IDs. Even if these aren't too long, don't save them automatically if (ast.op === 'or' && ast.operands.every((op) => op.op === 'filter' && op.type === 'id')) { saveInHistory = false; } canonical = canonicalizeQuery(ast); saveable = canonical.length <= 2048 && canonical.length > 0; } } catch { valid = false; } return { valid, saveable: valid && saveable, saveInHistory: valid && saveable && saveInHistory, canonical, }; } /** * Return whether the query is completely valid - syntactically, and where every term matches a known filter * and every filter RHS matches the declared format and options for the filter syntax. */ function validateQuery<I, FilterCtx, SuggestionsCtx>( query: QueryAST, filtersMap: FiltersMap<I, FilterCtx, SuggestionsCtx>, filterContext?: FilterCtx, ): boolean { if (query.error) { return false; } switch (query.op) { case 'filter': { const filterName = query.type; const filterValue = query.args; // "is:" filters are slightly special cased if (filterName === 'is') { return Boolean(filtersMap.isFilters[filterValue]); } else { const filterDef = filtersMap.kvFilters[filterName]; return Boolean(filterDef && matchFilter(filterDef, filterName, filterValue, filterContext)); } } case 'not': return validateQuery(query.operand, filtersMap, filterContext); case 'and': case 'or': { return query.operands.every((q) => validateQuery(q, filtersMap, filterContext)); } case 'noop': return true; } } ================================================ FILE: src/app/search/specialty-modslots.ts ================================================ import { LookupTable } from 'app/utils/util-types'; import { PlugCategoryHashes } from 'data/d2/generated-enums'; export interface ModSocketMetadata { /** we use these two to match with search filters (modslot) */ slotTag: string; /** armor items have sockets, and sockets have a socketTypeHash */ socketTypeHashes: number[]; /** mod items have a plugCategoryHash. this mod slot can hold these plugCategoryHashes */ compatiblePlugCategoryHashes: number[]; /** this helps us look up an "empty socket" definition, for its name only */ emptyModSocketHash: number; /** * the year is 2022. the raid is Vow of the Disciple. bungie forgot to give raid mods a itemTypeDisplayName. * let's use this Activity name instead. * NB this was fixed but may prove useful in the future if it happens again, so let's keep this around? */ modGroupNameOverrideActivityHash?: number; /** * The milestone hash for the activity that has the best icon for this mod * type. Usually this is a raid activity. */ milestoneHash?: number; /** * The activity mode hash for the activity that has the best icon for this mod * type. */ activityModeHash?: number; /** * The icon hash to use for this mod type, if not the empty socket icon. */ iconHash?: number; } export const modTypeTagByPlugCategoryHash: LookupTable<PlugCategoryHashes, string> = { [PlugCategoryHashes.EnhancementsSeasonOutlaw]: 'lastwish', [PlugCategoryHashes.EnhancementsSeasonMaverick]: 'nightmare', [PlugCategoryHashes.EnhancementsRaidGarden]: 'gardenofsalvation', [PlugCategoryHashes.EnhancementsRaidDescent]: 'deepstonecrypt', [PlugCategoryHashes.EnhancementsRaidV520]: 'vaultofglass', [PlugCategoryHashes.EnhancementsRaidV600]: 'vowofthedisciple', [PlugCategoryHashes.EnhancementsRaidV620]: 'kingsfall', [PlugCategoryHashes.EnhancementsArtifice]: 'artifice', [PlugCategoryHashes.EnhancementsRaidV700]: 'rootofnightmares', [PlugCategoryHashes.EnhancementsRaidV720]: 'crotasend', [PlugCategoryHashes.EnhancementsRaidV800]: 'salvationsedge', }; export const modSocketMetadata: ModSocketMetadata[] = [ { slotTag: 'lastwish', socketTypeHashes: [1444083081], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsSeasonOutlaw], emptyModSocketHash: 1679876242, // ARGH, this is the wrong image in the game/manifest milestoneHash: 3181387331, }, { slotTag: 'gardenofsalvation', socketTypeHashes: [1764679361], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsRaidGarden], emptyModSocketHash: 706611068, milestoneHash: 2712317338, }, { slotTag: 'deepstonecrypt', socketTypeHashes: [1269555732], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsRaidDescent], emptyModSocketHash: 4055462131, milestoneHash: 541780856, }, { slotTag: 'vaultofglass', socketTypeHashes: [3372624220], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsRaidV520], emptyModSocketHash: 3738398030, milestoneHash: 1888320892, }, { slotTag: 'vowofthedisciple', socketTypeHashes: [2381877427], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsRaidV600], emptyModSocketHash: 2447143568, milestoneHash: 2136320298, }, { slotTag: 'kingsfall', socketTypeHashes: [3344538838], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsRaidV620], emptyModSocketHash: 1728096240, milestoneHash: 292102995, }, { slotTag: 'rootofnightmares', socketTypeHashes: [1956816524], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsRaidV700], emptyModSocketHash: 4144354978, milestoneHash: 3699252268, }, { slotTag: 'crotasend', socketTypeHashes: [2804745000], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsRaidV720], emptyModSocketHash: 717667840, milestoneHash: 540415767, }, { slotTag: 'salvationsedge', socketTypeHashes: [1252302330], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsRaidV800], emptyModSocketHash: 4059283783, milestoneHash: 4196566271, }, { slotTag: 'nightmare', socketTypeHashes: [2701840022], compatiblePlugCategoryHashes: [PlugCategoryHashes.EnhancementsSeasonMaverick], emptyModSocketHash: 1180997867, activityModeHash: 332181804, }, ]; export const artificeDisplayStub = { emptyModSocketHash: 4173924323, iconHash: 3727270518, } as ModSocketMetadata; ================================================ FILE: src/app/search/suggestions-generation.ts ================================================ import { FilterDefinition, canonicalFilterFormats } from './filter-types'; const operators = ['<', '>', '<=', '>=']; // TODO: add "none"? remove >=, <=? /** * Generates all the possible suggested keywords for the given filter * * Accepts partial filters with as little as just a "keywords" property, * if you want to generate some keywords without a full valid filter */ export function generateSuggestionsForFilter<I, FilterCtx, SuggestionsCtx>( filterDefinition: Pick< FilterDefinition<I, FilterCtx, SuggestionsCtx>, 'keywords' | 'suggestions' | 'format' | 'overload' | 'deprecated' | 'suggestionsGenerator' >, suggestionsContext: SuggestionsCtx, ) { return generateGroupedSuggestionsForFilter(filterDefinition, suggestionsContext, false).flatMap( ({ keyword, ops }) => { if (ops) { return [keyword].concat(ops.map((op) => `${keyword}${op}`)); } else { return [keyword]; } }, ); } export function generateGroupedSuggestionsForFilter<I, FilterCtx, SuggestionsCtx>( filterDefinition: Pick< FilterDefinition<I, FilterCtx, SuggestionsCtx>, 'keywords' | 'suggestions' | 'format' | 'overload' | 'deprecated' | 'suggestionsGenerator' >, suggestionsContext: SuggestionsCtx, forHelp?: boolean, ): { keyword: string; ops?: string[] }[] { if (filterDefinition.deprecated) { return []; } const allSuggestions = []; // Use suggestionsGenerator if it exists if (filterDefinition.suggestionsGenerator) { for (const suggestion of filterDefinition.suggestionsGenerator(suggestionsContext) ?? []) { if (typeof suggestion === 'string') { allSuggestions.push({ keyword: suggestion }); } else { allSuggestions.push(suggestion); } } } else { // Otherwise, generate suggestions according to the filter format. const { suggestions, keywords } = filterDefinition; const thisFilterKeywords = Array.isArray(keywords) ? keywords : [keywords]; const filterSuggestions = suggestions === undefined ? [] : suggestions; const expandFlat = (stringGroups: string[][], minDepth = 0) => expandStringCombinations(stringGroups) .slice(minDepth) .flat() .map((s) => ({ keyword: s })); // We delay expanding ops because ops on their own expand the filters list significantly. // For autocompletion `generateSuggestionsForFilter` above expands the ops, but the filters // help has some special display to group the operator variants. const expandOps = (stringGroups: string[][], ops: string[]) => { const combinations = expandStringCombinations([...stringGroups, []]); const partialSuggestions = combinations .slice(0, stringGroups.length - 1) .flat() .map((s) => ({ keyword: s })); const opSuggestions = combinations[stringGroups.length - 1].map((s) => ({ keyword: s, ops })); return partialSuggestions.concat(opSuggestions); }; for (const format of canonicalFilterFormats(filterDefinition.format)) { switch (format) { case 'simple': // Pass minDepth 1 to not generate "is:" and "not:" suggestions. Only generate `is:` for filters help allSuggestions.push( ...expandFlat([forHelp ? ['is'] : ['is', 'not'], thisFilterKeywords], 1), ); break; case 'query': case 'multiquery': // `query` is exhaustive, so only include keyword: for autocompletion, not filters help allSuggestions.push( ...expandFlat([thisFilterKeywords, filterSuggestions], forHelp ? 1 : 0), ); break; case 'freeform': allSuggestions.push(...expandFlat([thisFilterKeywords, []])); break; case 'range': allSuggestions.push(...expandOps([thisFilterKeywords], operators)); if (filterDefinition.overload) { const overloadNames = Object.keys(filterDefinition.overload); allSuggestions.push(...expandFlat([thisFilterKeywords, overloadNames], 1)); // Outside of filter help (i.e. only for autocompletion), also expand overloaded ranges like season:<current if (!forHelp) { allSuggestions.push( ...expandFlat( [ thisFilterKeywords, operators.flatMap((op) => overloadNames.map((overloadName) => `${op}${overloadName}`), ), ], 1, ), ); } } break; case 'stat': // stat lists aren't exhaustive allSuggestions.push(...expandOps([thisFilterKeywords, filterSuggestions], operators)); break; case 'custom': break; } } } return allSuggestions; } /** * loops through collections of strings (filter segments), * generating combinations grouped by number of segments * * for example, with * `[ [a], [b,c], [d,e] ]` * as an input, this generates * * `[ [a:], [a:b:, a:c:], [a:b:d, a:b:e, a:c:d, a:c:e] ]` */ function expandStringCombinations(stringGroups: string[][]) { const results: string[][] = []; for (let i = 0; i < stringGroups.length; i++) { const stringGroup = stringGroups[i]; const stems = results.length ? results.at(-1)! : undefined; const newResults = stringGroup.flatMap((suffix) => stems ? stems.map( (stem) => (stem ? `${stem}${suffix}` : suffix) + (i === stringGroups.length - 1 ? '' : ':'), ) : [`${suffix}:`], ); results.push(newResults); } return results; } ================================================ FILE: src/app/search/text-utils.ts ================================================ /* Utilities for text matching in filters. */ import { DIM_LANG_INFOS, DimLanguage } from 'app/i18n'; /** global language bool. "latin" character sets are the main driver of string processing changes */ const isLatinBased = (language: DimLanguage) => DIM_LANG_INFOS[language].latinBased; /** escape special characters for a regex */ function escapeRegExp(s: string) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** Remove diacritics from latin-based string */ function latinize(s: string, language: DimLanguage) { return isLatinBased(language) ? s.normalize('NFD').replace(/\p{Diacritic}/gu, '') : s; } /** Make a Regexp that searches starting at a word boundary */ export function startWordRegexp(s: string, language: DimLanguage) { return new RegExp( `${ // Only some languages effectively use the \b regex word boundary isLatinBased(language) && // Don't force a word boundary if the string doesn't start with a word /^\w/.test(s) ? '\\b' : '' }${escapeRegExp(s)}`, 'i', ); } /** returns input string toLower, and stripped of accents if it's a latin language */ export const plainString = (s: string, language: DimLanguage): string => latinize(s, language).toLowerCase(); /** * Create a case-/diacritic-insensitive matching predicate for name / perkname filters. * * Requires an exact match if `exact`, otherwise partial. */ export function matchText(value: string, language: DimLanguage, exact: boolean) { const normalized = plainString(value, language); if (exact) { // Quotes must be normalized on the tested term `s` // because the `value` provided by the query parser has normalized quotes. return (s: string) => normalized === plainString(normalizeQuotes(s), language); } else { const startWord = startWordRegexp(normalized, language); return (s: string) => startWord.test(plainString(normalizeQuotes(s), language)); } } /** * feed in an object with a `name` and a `description` property, * to get an array of just those strings */ export function testStringsFromDisplayProperties( test: (str: string) => boolean, displayProperties?: { name: string; description: string }, includeDescription = true, ): boolean { if (!displayProperties) { return false; } return Boolean( (displayProperties.name && test(displayProperties.name)) || (includeDescription && displayProperties.description && test(displayProperties.description)), ); } /** * feed in an object or objects with a `name` and a `description` property */ export function testStringsFromDisplayPropertiesMap( test: (str: string) => boolean, displayProperties?: | { name: string; description: string } | { name: string; description: string }[] | null, includeDescription = true, ): boolean { if (!displayProperties) { return false; } if (!Array.isArray(displayProperties)) { return testStringsFromDisplayProperties(test, displayProperties, includeDescription); } return displayProperties.some((d) => testStringsFromDisplayProperties(test, d, includeDescription), ); } // http://blog.tatedavies.com/2012/08/28/replace-microsoft-chars-in-javascript/ const singleQuoteLikeCharacters = /[\u2018-\u201A]/g; const doubleQuoteLikeCharacters = /[\u201C-\u201E]/g; // Turn quote variants into their boring ASCII equivalents for parsing. export function normalizeQuotes(str: string) { return str.replace(singleQuoteLikeCharacters, "'").replace(doubleQuoteLikeCharacters, '"'); } // These aren't global so they aren't stateful, and can be used to repeatedly .test() export const unescapedSingleQuoteCharacters = /(?<!\\)[\u2018-\u201A']/; export const unescapedDoubleQuoteCharacters = /(?<!\\)[\u201C-\u201E"]/; export function escapeQuotes(str: string, onlyDouble = false) { if (!onlyDouble) { str = str.replace(new RegExp(unescapedSingleQuoteCharacters, 'g'), '\\$&'); } str = str.replace(new RegExp(unescapedDoubleQuoteCharacters, 'g'), '\\$&'); return str; } ================================================ FILE: src/app/settings/CharacterOrderEditor.m.scss ================================================ @use '../variables.scss' as *; .editor { display: flex; flex-direction: row; } .item { width: calc(400px - 25px / 3); background: black; padding: 8px; text-align: center; margin: 8px; } .character { img { height: 44px; width: 44px; pointer-events: none; } } .powerLevel { color: $power; &::before { content: '\2726'; /*  */ display: inline-block; vertical-align: 25%; font-size: 50%; } } ================================================ FILE: src/app/settings/CharacterOrderEditor.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'character': string; 'editor': string; 'item': string; 'powerLevel': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/settings/CharacterOrderEditor.tsx ================================================ import { DimStore } from 'app/inventory/store-types'; import { emptyArray } from 'app/utils/empty'; import { MotionStyle, Reorder, TargetAndTransition } from 'motion/react'; import { useState } from 'react'; import { useSelector } from 'react-redux'; import { sortedStoresSelector } from '../inventory/selectors'; import { AppIcon, refreshIcon } from '../shell/icons'; import * as styles from './CharacterOrderEditor.m.scss'; const regularStyle: MotionStyle = { cursor: 'grab' }; const draggingStyle: TargetAndTransition = { cursor: 'grabbing' }; /** * An editor for character orders, with drag and drop. */ export default function CharacterOrderEditor({ onSortOrderChanged, }: { onSortOrderChanged: (order: string[]) => void; }) { const characters = useSelector(sortedStoresSelector); const nonVaultCharacters = characters.filter((c) => !c.isVault); const [draggingOrder, setDraggingOrder] = useState<DimStore[]>(emptyArray); const handleReorder = (newOrder: typeof nonVaultCharacters) => { setDraggingOrder(newOrder); }; const handleDragEnd = () => { onSortOrderChanged(draggingOrder.map((c) => c.id)); setDraggingOrder(emptyArray()); }; if (!characters.length) { return ( <div className={styles.editor}> <AppIcon icon={refreshIcon} spinning={true} /> Loading characters... </div> ); } // When dragging, show the order in state, then switch back to the one from props const displayCharacters = draggingOrder.length > 0 ? draggingOrder : nonVaultCharacters; return ( <Reorder.Group axis="x" values={nonVaultCharacters} onReorder={handleReorder} className={styles.editor} as="div" > {displayCharacters.map((character) => ( <Reorder.Item key={character.id} value={character} className={styles.item} style={regularStyle} whileDrag={draggingStyle} onDragEnd={handleDragEnd} as="div" > <div className={styles.character}> <img src={character.icon} /> <div> <span className={styles.powerLevel}>{character.powerLevel}</span>{' '} {character.className} </div> </div> </Reorder.Item> ))} </Reorder.Group> ); } ================================================ FILE: src/app/settings/Checkbox.tsx ================================================ import Switch from 'app/dim-ui/Switch'; import HelpLink from '../dim-ui/HelpLink'; import { horizontalClass } from './SettingsPage'; import { Settings } from './initial-settings'; export default function Checkbox({ label, title, value, helpLink, name, onChange, }: { label: React.ReactNode; value: boolean; title?: string; helpLink?: string; name: keyof Settings; onChange: (checked: boolean, name: keyof Settings) => void; }) { return ( <div className={horizontalClass}> <label htmlFor={name} title={title}> {label} </label> {helpLink && <HelpLink helpLink={helpLink} />} <Switch name={name} checked={value} onChange={onChange} /> </div> ); } ================================================ FILE: src/app/settings/CustomStatsSettings.m.scss ================================================ @use '../variables.scss' as *; .headerRow { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; label { flex-grow: 1; } } .customDesc { composes: fineprint from './SettingsPage.m.scss'; } // a style for inputs or input wrappers .inputlike { box-sizing: border-box; flex-grow: 1; border: 0; @include interactive($hover: true, $focus: true) { outline: none; box-shadow: inset 0 0 0 1px var(--theme-search-dropdown-border); } } .statOption { background: #4443; border: 1px solid #0000; border-radius: 4px; box-sizing: border-box; color: var(--theme-text); flex: 0 !important; gap: 4px; padding: 3px 0; display: flex !important; align-items: center; margin: 0; flex-direction: row; @include phone-portrait { flex-direction: column; } } .classOption { display: flex; gap: 7px; font-size: 16px !important; align-items: center; cursor: default; user-select: none; } .classDropdownIcon { margin: 0; height: 18px; width: 18px; font-size: 18px !important; } // editor for a single stat .customStatEditor { composes: flexColumn from '../dim-ui/common.m.scss'; border: 1px solid; margin: 8px 0; padding: 6px; animation: pulse-border 1s ease-in-out 0s infinite alternate; gap: 4px; } .identifyingInfo { composes: flexWrap from '../dim-ui/common.m.scss'; align-items: center; justify-content: flex-end; gap: 8px; img { height: 1.5em; } code { background-color: #000; } } .label { flex-grow: 1; text-wrap: balance; } .classIcon { font-size: 20px; width: 1.4em; text-align: center; } .filter { flex-grow: 1; color: var(--theme-text-secondary); } .zero { opacity: 0.4; } .editableStatsRow { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; justify-content: space-between; gap: 4px; img { height: 1.5em; } } // all info for a custom stat, without the editing controls .customStatView { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; gap: 8px; } @keyframes pulse-border { 0% { border-color: #fff8; } 100% { border-color: #fff4; } } ================================================ FILE: src/app/settings/CustomStatsSettings.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'classDropdownIcon': string; 'classIcon': string; 'classOption': string; 'customDesc': string; 'customStatEditor': string; 'customStatView': string; 'editableStatsRow': string; 'filter': string; 'headerRow': string; 'identifyingInfo': string; 'inputlike': string; 'label': string; 'pulseBorder': string; 'statOption': string; 'zero': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/settings/CustomStatsSettings.tsx ================================================ import { CustomStatDef, CustomStatWeights } from '@destinyitemmanager/dim-api-types'; import { customStatsSelector } from 'app/dim-api/selectors'; import BungieImage from 'app/dim-ui/BungieImage'; import ClassIcon from 'app/dim-ui/ClassIcon'; import { CustomStatWeightsDisplay } from 'app/dim-ui/CustomStatWeights'; import Select from 'app/dim-ui/Select'; import Switch from 'app/dim-ui/Switch'; import useConfirm from 'app/dim-ui/useConfirm'; import { t } from 'app/i18next-t'; import { getClassTypeNameLocalized } from 'app/inventory/store/d2-item-factory'; import { useD2Definitions } from 'app/manifest/selectors'; import { showNotification } from 'app/notifications/notifications'; import { CUSTOM_TOTAL_STAT_HASH, armorStats, customStatClasses, evenStatWeights, } from 'app/search/d2-known-values'; import { allAtomicStats } from 'app/search/search-filter-values'; import { AppIcon, addIcon, banIcon, deleteIcon, editIcon, saveIcon } from 'app/shell/icons'; import { chainComparator, compareBy } from 'app/utils/comparators'; import { isClassCompatible } from 'app/utils/item-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import React, { useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { count } from 'app/utils/collections'; import * as styles from './CustomStatsSettings.m.scss'; import { useSetSetting } from './hooks'; /** * a list of user-defined custom stat displays. each can be switched into editing mode. */ export function CustomStatsSettings() { const customStatList = useSelector(customStatsSelector); // which custom stat is currently being edited (identified by its hash) const [editing, setEditing] = useState(0); // disabled by a feature flag right now. really don't trust the math of this const [weightsMode, setWeightsMode] = useState(false); // if a stat is pending its first save, it lives here, not in customStatList const [provisionalStat, setProvisionalStat] = useState<CustomStatDef>(); // this component lives on the settings page, which can load // before definitions. without them, don't bother rendering if (!useD2Definitions()) { return null; } // the provisional stat, if there is one, is displayed above the // others in the list, and hasn't been saved to settings yet const onAddNew = () => { const newStat = createNewStat(customStatList); setProvisionalStat(newStat); setEditing(newStat.statHash); }; // children components call this, to end editing mode const onDoneEditing = () => { setEditing(0); setProvisionalStat(undefined); }; return ( <> <div className={styles.headerRow}> <label htmlFor="">{t('Settings.CustomStatTitle')}</label> {$featureFlags.customStatWeights && ( <span> stat weights{' '} <Switch checked={weightsMode} name="weightsMode" onChange={() => setWeightsMode(!weightsMode)} /> </span> )} <button type="button" className="dim-button" onClick={onAddNew} disabled={Boolean(editing)} title={t('Settings.CustomStatCreate')} > <AppIcon icon={addIcon} /> </button> </div> <div className={styles.customDesc}> {t('Settings.CustomStatDesc1')} {t('Settings.CustomStatDesc3')} </div> {[...(provisionalStat ? [provisionalStat] : []), ...customStatList].map((c) => c.statHash === editing ? ( <CustomStatEditor onDoneEditing={onDoneEditing} weightsMode={$featureFlags.customStatWeights && weightsMode} statDef={c} key={c.statHash} /> ) : ( <CustomStatView setEditing={setEditing} statDef={c} key={c.statHash} /> ), )} </> ); } /** the editing view for a single custom stat */ function CustomStatEditor({ statDef, className, onDoneEditing, weightsMode, }: { statDef: CustomStatDef; className?: string; // used to alert upstream that we are done editing this stat onDoneEditing: () => void; // if false, this editor only lets you toggle each armor stat on and off (weight 0 and weight 1) weightsMode: boolean; }) { const defs = useD2Definitions()!; const [classType, setClassType] = useState(statDef.class); const [label, setLabel] = useState(statDef.label); const [weights, setWeight] = useStatWeightsEditor(statDef.weights); const originalWeights = useRef(JSON.stringify(weights)); const originalLabel = useRef(statDef.label); const originalClass = useRef(statDef.class); const saveStat = useSaveStat(); const [removeStat, removeStatDialog] = useRemoveStat(); const options = customStatClasses.map((c) => ({ key: `${c}`, content: ( <div className={styles.classOption}> <ClassIcon classType={c} className={styles.classDropdownIcon} /> {getClassTypeNameLocalized(defs)(c)} </div> ), value: c, })); const onLabelChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => setLabel(target.value.slice(0, 30)); const shortLabel = simplifyStatLabel(label); // controls whether "save" button shows up, or just "cancel editing" const somethingChanged = JSON.stringify(weights) !== originalWeights.current || originalLabel.current !== label.trim() || originalClass.current !== classType; const isNewStat = originalLabel.current === ''; const weightedStatCount = count(Object.values(weights), Boolean); return ( <div className={clsx(className, styles.customStatEditor)}> {removeStatDialog} <div className={styles.identifyingInfo}> <Select options={options} onChange={(c) => setClassType(c ?? DestinyClass.Unknown)} value={classType} hideSelected > <ClassIcon classType={classType} className={styles.classDropdownIcon} /> </Select> <input type="text" placeholder={t('Settings.CustomStatChooseName')} className={styles.inputlike} value={label} onChange={onLabelChange} /> </div> <div className={styles.editableStatsRow}> {armorStats.map((statHash) => { const stat = defs.Stat.get(statHash); const weight = weights[statHash] || 0; const onVal = ({ target }: React.ChangeEvent<HTMLInputElement>) => setWeight(statHash, target.value); const className = weight ? 'stat-icon' : styles.zero; return ( <label className={styles.statOption} key={statHash} title={stat.displayProperties.name}> <BungieImage className={className} src={stat.displayProperties.icon} /> {weightsMode ? ( <input type="number" max={9} min={0} maxLength={30} value={weight} onChange={onVal} /> ) : ( <Switch name={`${statHash}_toggle`} checked={Boolean(weights[statHash])} onChange={(on) => setWeight(statHash, on ? '1' : '0')} /> )} </label> ); })} </div> <div className={styles.identifyingInfo}> <span className={styles.filter}> {shortLabel.length > 0 && ( <> {t('Filter.Filter')} {': '} <code>{`stat:${shortLabel}:>=30`}</code> </> )} </span> {(isNewStat || somethingChanged) && ( <button type="button" className="dim-button" onClick={() => { // try saving the proposed new custom stat, with newly set label, class, and weights saveStat({ ...statDef, class: classType, label, shortLabel, weights }) && onDoneEditing(); }} title={t('Loadouts.Update')} disabled={!label || weightedStatCount < 2 || weightedStatCount > 5} > <AppIcon icon={saveIcon} /> </button> )} <button type="button" className="dim-button" onClick={onDoneEditing} title={t('Loadouts.CancelEditing')} > <AppIcon icon={banIcon} /> </button> {!isNewStat && ( <button type="button" className="dim-button danger" onClick={async () => (await removeStat(statDef)) && onDoneEditing()} title={t('Settings.CustomStatDelete')} > <AppIcon icon={deleteIcon} /> </button> )} </div> </div> ); } /** a state manager for a single set of stat weights */ function useStatWeightsEditor(w: CustomStatWeights) { const [weights, setWeights] = useState(w); return [ weights, (statHash: number, value: string) => setWeights((old) => ({ ...old, [statHash]: parseInt(value, 10) || 0 })), ] as const; } /** * the display view for a single stat. * it can send a signal upstream to initiate edit mode, * replacing itself with CustomStatEditor */ function CustomStatView({ statDef, setEditing, }: { statDef: CustomStatDef; // used to alert upstream that we want to edit this stat setEditing: React.Dispatch<React.SetStateAction<number>>; }) { return ( <div className={styles.customStatView}> <button type="button" className="dim-button" onClick={() => setEditing(statDef.statHash)} title={t('Loadouts.EditBrief')} > <AppIcon icon={editIcon} /> </button> <ClassIcon proportional className={styles.classIcon} classType={statDef.class} /> <span className={styles.label}>{statDef.label}</span> <CustomStatWeightsDisplay customStat={statDef} /> </div> ); } // custom stat retrieval from state/settings needs to be in a stable order, // between stat generation (stats.ts) and display (ItemStat.tsx) // so let's neatly sort them as we commit them to settings. const customStatSort = chainComparator( compareBy((customStat: CustomStatDef) => customStat.class), compareBy((customStat: CustomStatDef) => customStat.label), ); function useSaveStat() { const setSetting = useSetSetting(); const customStatList = useSelector(customStatsSelector); return (newStat: CustomStatDef) => { newStat.label = newStat.label.trim(); // when trying to save, update the short label to match the submitted long label newStat.shortLabel = simplifyStatLabel(newStat.label); const weightValues = Object.values(newStat.weights); const everyValueValid = weightValues.every( (v) => v !== undefined && Number.isInteger(v) && v >= 0, ); if ( // if there's any invalid values !everyValueValid || // or too few included stats count(weightValues, Boolean) < 2 ) { warnInvalidCustomStat(t('Settings.CustomErrorValues')); return false; } const allOtherStats = customStatList.filter((s) => s.statHash !== newStat.statHash); if ( // if there's not enough label !newStat.shortLabel || // or there's an existing stat with an overlapping label & class allOtherStats.some( (s) => s.shortLabel === newStat.shortLabel && isClassCompatible(s.class, newStat.class), ) || // or this shortLabel conflicts with a real stat. // don't name your custom stat discipline!! allAtomicStats.includes(newStat.shortLabel) ) { warnInvalidCustomStat(t('Settings.CustomErrorLabel')); return false; } if (isLegacyStat(newStat)) { // upgrade its statHash to a non-legacy const statHash = createNewStatHash(customStatList); newStat = { ...newStat, statHash }; } // commit this new stat to settings setSetting( 'customStats', [...allOtherStats.filter((s) => s.statHash), newStat].sort(customStatSort), ); return true; }; } function useRemoveStat() { const setSetting = useSetSetting(); const customStatList = useSelector(customStatsSelector); const [removeStatDialog, confirm] = useConfirm(); const removeStat = async (stat: CustomStatDef) => { if ( // user is deleting a provisional stat, or already cleared out the name field stat.label === '' || // user is deleting a full-fledged stat, let's confirm whether they are sure (await confirm(t('Settings.CustomStatDeleteConfirm'))) ) { setSetting( 'customStats', customStatList.filter((s) => s.statHash !== stat.statHash).sort(customStatSort), ); return true; } // reached here if they clicked NO on the confirm return false; }; return [removeStat, removeStatDialog] as const; } function isLegacyStat(stat: CustomStatDef) { // converted old stats live in this numeric range return ( stat.statHash > CUSTOM_TOTAL_STAT_HASH && stat.statHash < CUSTOM_TOTAL_STAT_HASH + 6 && // converted old stats are never global stat.class !== DestinyClass.Unknown ); } function createNewStat(customStatList: CustomStatDef[]): CustomStatDef { const statHash = createNewStatHash(customStatList); return { label: '', shortLabel: '', class: DestinyClass.Unknown, weights: { ...evenStatWeights }, statHash, }; } function createNewStatHash(existingCustomStats: CustomStatDef[]) { const existingStatHashes = existingCustomStats.map((c) => c.statHash); let lowestStatHash = existingStatHashes.length ? Math.min(...existingStatHashes) : CUSTOM_TOTAL_STAT_HASH; // catch some impossible cases: somehow it got above 111000, // or we decremented past negative integer limit... // why has the user generated 9 quadrillion different custom swtats? if (lowestStatHash < CUSTOM_TOTAL_STAT_HASH || lowestStatHash <= Number.MIN_SAFE_INTEGER) { lowestStatHash = CUSTOM_TOTAL_STAT_HASH; } let statHash = lowestStatHash - 1; while (existingStatHashes.includes(statHash)) { statHash--; } return statHash; } function warnInvalidCustomStat(errorMsg: string) { showNotification({ type: 'warning', title: t('Settings.CustomStatTitle'), body: errorMsg, duration: 5000, }); } function simplifyStatLabel(s: string) { s = s.trim(); // do a special intercession here: if it's the default name // "Custom Total" (or i18n'd equivalent) then return just "custom" // so that people's saved `stat:custom:>30` filters work as they used to if (s === t('Stats.Custom')) { return 'custom'; } return s.toLocaleLowerCase().replace(/[^\p{L}\p{N}_]/gu, ''); } ================================================ FILE: src/app/settings/LanguageSetting.tsx ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { getDefinitions as getDefinitionsD1 } from 'app/destiny1/d1-definitions'; import { getDefinitions } from 'app/destiny2/d2-definitions'; import { settingsSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { clearStores } from 'app/inventory/actions'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import i18next from 'i18next'; import React from 'react'; import { useSelector } from 'react-redux'; import Select, { mapToOptions } from './Select'; import { useSetSetting } from './hooks'; const languageOptions = mapToOptions({ de: 'Deutsch', en: 'English', es: 'Español (España)', 'es-mx': 'Español (México)', fr: 'Français', it: 'Italiano', ko: '한국어', pl: 'Polski', 'pt-br': 'Português (Brasil)', ru: 'Русский', ja: '日本語', 'zh-cht': '繁體中文', // Chinese (Traditional) 'zh-chs': '简体中文', // Chinese (Simplified) }); export default function LanguageSetting() { const dispatch = useThunkDispatch(); const settings = useSelector(settingsSelector); const currentAccount = useSelector(currentAccountSelector); const setSetting = useSetSetting(); const changeLanguage = async (e: React.ChangeEvent<HTMLSelectElement>) => { const language = e.target.value; await i18next.changeLanguage(language); setSetting('language', language); if (currentAccount?.destinyVersion === 2) { await dispatch(getDefinitions(true)); } else if (currentAccount?.destinyVersion === 1) { await dispatch(getDefinitionsD1(false)); } dispatch(clearStores()); }; return ( <Select label={t('Settings.Language')} name="language" value={settings.language} options={languageOptions} onChange={changeLanguage} /> ); } ================================================ FILE: src/app/settings/Select.m.scss ================================================ .select { composes: horizontal from './SettingsPage.m.scss'; > select { max-width: 60%; } } ================================================ FILE: src/app/settings/Select.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'select': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/settings/Select.tsx ================================================ import React from 'react'; import * as styles from './Select.m.scss'; import { Settings } from './initial-settings'; export default function Select({ label, value, name, onChange, options, }: { label: string; value: string | number; options: { name?: string; value: string | number; }[]; name: keyof Settings; onChange: React.ChangeEventHandler<HTMLSelectElement>; }) { return ( <div className={styles.select}> <label htmlFor={name}>{label}</label> <select name={name} value={value} required={true} onChange={onChange}> {options.map((option) => ( <option key={option.value} value={option.value}> {option.name ? option.name : option.value} </option> ))} </select> </div> ); } export function mapToOptions(map: { [key: string]: string }) { return Object.entries(map).map(([key, value]) => ({ name: value, value: key, })); } ================================================ FILE: src/app/settings/SettingsPage.m.scss ================================================ @use '../variables.scss' as *; .settings { font-size: 14px; max-width: 500px; padding: 0 10px; form { display: flex; flex-direction: column; gap: 2em; } h1 { margin: 0 0 16px 0; font-size: 20px; @include phone-portrait { margin: 16px 0; } } h2 { text-transform: uppercase; font-size: 16px; font-weight: normal; letter-spacing: 1px; margin: 0; } section { display: flex; flex-direction: column; gap: 8px; } label { flex: 1; display: block; text-wrap: balance; } input[type='checkbox'] { font-size: 16px; } } .newItem { position: static; display: inline-block; transform: scale(1.5); margin-right: 8px; } .setting { composes: flexColumn from '../dim-ui/common.m.scss'; gap: 8px; padding: 8px; background-color: var(--theme-section-bg); @include phone-portrait { margin-left: -10px; margin-right: -10px; padding: 10px; } > .setting { padding: 0; margin: 0; background-color: transparent; } > button { align-self: flex-start; } } .fineprint { font-size: 0.85em; color: var(--theme-text-secondary); text-wrap: pretty; } .horizontal { composes: flexWrap from '../dim-ui/common.m.scss'; align-items: center; justify-content: space-between; gap: 1em; } .itemSize { composes: horizontal; gap: 1em; label { flex: 0; white-space: nowrap; margin-right: 0; } input { flex: 1; } } .radioOptions { list-style: none; padding: 0; margin: 4px 0 0 4px; display: flex; flex-direction: column; gap: 8px; label { vertical-align: text-top; display: flex; flex-direction: row; align-items: center; gap: 0.25em; } input { margin: 0 4px 0 0; } } .autoTagTable { width: min-content; td:first-child { display: flex; gap: 4px; } } ================================================ FILE: src/app/settings/SettingsPage.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'autoTagTable': string; 'fineprint': string; 'horizontal': string; 'itemSize': string; 'newItem': string; 'radioOptions': string; 'setting': string; 'settings': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/settings/SettingsPage.tsx ================================================ import { OrnamentDisplay, VaultWeaponGroupingStyle } from '@destinyitemmanager/dim-api-types'; import { currentAccountSelector, hasD1AccountSelector } from 'app/accounts/selectors'; import { clarityDiscordLink, clarityLink } from 'app/clarity/about'; import { settingsSelector } from 'app/dim-api/selectors'; import PageWithMenu from 'app/dim-ui/PageWithMenu'; import { t } from 'app/i18next-t'; import NewItemIndicator from 'app/inventory/NewItemIndicator'; import TagIcon from 'app/inventory/TagIcon'; import { clearAllNewItems } from 'app/inventory/actions'; import { itemTagList } from 'app/inventory/dim-item-info'; import { allItemsSelector } from 'app/inventory/selectors'; import { useLoadStores } from 'app/inventory/store/hooks'; import WishListSettings from 'app/settings/WishListSettings'; import { useIsPhonePortrait } from 'app/shell/selectors'; import DimApiSettings from 'app/storage/DimApiSettings'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import StreamDeckSettings from 'app/stream-deck/StreamDeckSettings/StreamDeckSettings'; import { clearAppBadge } from 'app/utils/app-badge'; import { compact } from 'app/utils/collections'; import { compareByIndex } from 'app/utils/comparators'; import { usePageTitle } from 'app/utils/hooks'; import { errorLog } from 'app/utils/log'; import { useMaxParallelCores } from 'app/utils/parallel-cores'; import { range } from 'es-toolkit'; import React from 'react'; import { useSelector } from 'react-redux'; import { Link } from 'react-router'; import ErrorBoundary from '../dim-ui/ErrorBoundary'; import '../inventory-page/StoreBucket.scss'; import InventoryItem from '../inventory/InventoryItem'; import { AppIcon, lockIcon, unlockedIcon } from '../shell/icons'; import CharacterOrderEditor from './CharacterOrderEditor'; import Checkbox from './Checkbox'; import { CustomStatsSettings } from './CustomStatsSettings'; import LanguageSetting from './LanguageSetting'; import Select, { mapToOptions } from './Select'; import * as styles from './SettingsPage.m.scss'; import SortOrderEditor, { SortProperty } from './SortOrderEditor'; import Spreadsheets from './Spreadsheets'; import { TroubleshootingSettings } from './Troubleshooting'; import { setCharacterOrder } from './actions'; import { useSetSetting } from './hooks'; import { Settings } from './initial-settings'; import { itemSortSettingsSelector } from './item-sort'; const TAG = 'settings'; export const settingClass = styles.setting; export const fineprintClass = styles.fineprint; export const horizontalClass = styles.horizontal; const themeOptions = mapToOptions({ default: 'Default (Beyond Light)', classic: 'DIM Classic', dimdark: 'DIM Dark Mode', europa: 'Europa', neomuna: 'Neomuna', pyramid: 'Pyramid Fleet', throneworld: 'Throne World', vexnet: 'Vex Network', }); // Old ornament setting; keeping these strings from being culled // t('Settings.OrnamentDisplayExplanationHide') // t('Settings.OrnamentDisplayExplanationShow') export default function SettingsPage() { usePageTitle(t('Settings.Settings')); const dispatch = useThunkDispatch(); const settings = useSelector(settingsSelector); const currentAccount = useSelector(currentAccountSelector); const hasD1Account = useSelector(hasD1AccountSelector); const isPhonePortrait = useIsPhonePortrait(); useLoadStores(currentAccount); const setSetting = useSetSetting(); const allItems = useSelector(allItemsSelector); const [maxParallelCores, setMaxParallelCores] = useMaxParallelCores(); const exampleWeapon = allItems.find( (i) => i.bucket.inWeapons && !i.isExotic && !i.masterwork && !i.deepsightInfo, ); // Include a masterworked item because they look different in some themes const exampleWeaponMasterworked = allItems.find( (i) => i.bucket.inWeapons && !i.isExotic && i.masterwork && !i.deepsightInfo, ); const exampleArmor = allItems.find((i) => i.bucket.inArmor && !i.isExotic); const exampleOrnament = allItems.find( (i) => i !== exampleArmor && i.bucket.inArmor && i.isExotic && i.ornamentIconDef, ) || allItems.find((i) => i !== exampleArmor && i.ornamentIconDef); const exampleArchivedArmor = allItems.find( (i) => i !== exampleArmor && i !== exampleOrnament && i.bucket.inArmor && !i.isExotic, ); const godRoll = { wishListPerks: new Set<number>(), notes: undefined, isUndesirable: false, }; const onCheckChange = (checked: boolean, name: keyof Settings) => { if (name.length === 0) { errorLog(TAG, new Error('You need to have a name on the form input')); } setSetting(name, checked); }; const onChange: React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> = (e) => { if (e.target.name.length === 0) { errorLog(TAG, new Error('You need to have a name on the form input')); } if (isInputElement(e.target) && e.target.type === 'checkbox') { setSetting(e.target.name as keyof Settings, e.target.checked); } else { setSetting(e.target.name as keyof Settings, e.target.value); } }; const onChangeNumeric: React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> = (e) => { if (e.target.name.length === 0) { errorLog(TAG, new Error('You need to have a name on the form input')); } setSetting(e.target.name as keyof Settings, parseInt(e.target.value, 10)); }; const onBadgePostmasterChanged = (checked: boolean, name: keyof Settings) => { if (!checked) { clearAppBadge(); } onCheckChange(checked, name); }; const changeTheme = (e: React.ChangeEvent<HTMLSelectElement>) => { const theme = e.target.value; setSetting('theme', theme); }; const changeDescriptionDisplay = (e: React.ChangeEvent<HTMLSelectElement>) => { setSetting('descriptionsToDisplay', e.target.value); }; const resetItemSize = (e: React.MouseEvent) => { e.preventDefault(); setSetting('itemSize', 50); return false; }; const changeVaultWeaponGrouping = (e: React.ChangeEvent<HTMLSelectElement>) => { const vaultWeaponGrouping = e.target.value; setSetting('vaultWeaponGrouping', vaultWeaponGrouping); }; const onMaxParallelCoresChanged: React.ChangeEventHandler<HTMLInputElement> = (e) => { setMaxParallelCores(parseInt(e.target.value, 10)); }; const itemSortOrderChanged = (sortOrder: SortProperty[]) => { setSetting( 'itemSortOrderCustom', sortOrder.filter((o) => o.enabled).map((o) => o.id), ); setSetting( 'itemSortReversals', sortOrder.filter((o) => o.reversed).map((o) => o.id), ); }; const characterSortOrderChanged = (order: string[]) => { dispatch(setCharacterOrder(order)); }; const tagLabelList = itemTagList.map((tagLabel) => t(tagLabel.label)); const listSeparator = ['ja', 'zh-cht', 'zh-chs'].includes(settings.language) ? '、' : ', '; const tagListString = tagLabelList.join(listSeparator); const itemSortProperties = { typeName: t('Settings.SortByType'), rarity: t('Settings.SortByRarity'), primStat: t('Settings.SortByPrimary'), amount: t('Settings.SortByAmount'), rating: t('Settings.SortByRating'), classType: t('Settings.SortByClassType'), ammoType: t('Settings.SortByAmmoType'), name: t('Settings.SortName'), tag: t('Settings.SortByTag', { taglist: tagListString }), season: t('Settings.SortBySeason'), acquisitionRecency: t('Settings.SortByRecent'), elementWeapon: t('Settings.SortByWeaponElement'), masterworked: t('Settings.Masterworked'), crafted: t('Settings.SortByCrafted'), deepsight: t('Settings.SortByDeepsight'), featured: t('Settings.SortByFeatured'), tier: t('Settings.SortByTier'), armorArchetype: t('Settings.ArmorArchetypeModslot'), weaponFrame: t('Settings.WeaponFrame'), }; const vaultWeaponGroupingOptions = mapToOptions({ '': t('Settings.VaultGroupingNone'), typeName: t('Settings.SortByType'), rarity: t('Settings.SortByRarity'), ammoType: t('Settings.SortByAmmoType'), tag: t('Settings.SortByTag', { taglist: tagListString }), elementWeapon: t('Settings.SortByWeaponElement'), }); const descriptionDisplayOptions = mapToOptions({ both: t('Settings.BothDescriptions'), bungie: t('Settings.BungieDescriptionOnly'), community: t('Settings.CommunityDescriptionOnly'), }); const charColOptions = range(2, 6).map((num) => ({ value: num, name: t('Settings.ColumnSize', { num }), })); const numberOfSpacesOptions = range(1, 10).map((count) => ({ value: count, name: t('Settings.SpacesSize', { count }), })); const vaultColOptions = range(5, 21).map((num) => ({ value: num, name: t('Settings.ColumnSize', { num }), })); vaultColOptions.unshift({ value: 999, name: t('Settings.ColumnSizeAuto') }); const sortSettings = useSelector(itemSortSettingsSelector); const itemSortCustom = Object.entries(itemSortProperties) .map( ([id, displayName]): SortProperty => ({ id, displayName, enabled: sortSettings.sortOrder.includes(id), reversed: sortSettings.sortReversals.includes(id), }), ) .sort(compareByIndex(sortSettings.sortOrder, (o) => o.id)); const menuItems = compact([ { id: 'appearance', title: t('Settings.Appearance') }, { id: 'inventory', title: t('Settings.Inventory') }, { id: 'items', title: t('Settings.Items') }, $featureFlags.wishLists ? { id: 'wishlist', title: t('WishListRoll.Header') } : undefined, { id: 'storage', title: t('Storage.MenuTitle') }, { id: 'spreadsheets', title: t('Settings.Data') }, $featureFlags.elgatoStreamDeck && !isPhonePortrait ? { id: 'stream-deck', title: 'Elgato Stream Deck' } : undefined, { id: 'troubleshooting', title: t('Settings.Troubleshooting') }, ]); return ( <PageWithMenu> <PageWithMenu.Menu> {!isPhonePortrait && menuItems.map((menuItem) => ( <PageWithMenu.MenuButton key={menuItem.id} anchor={menuItem.id}> <span>{menuItem.title}</span> </PageWithMenu.MenuButton> ))} </PageWithMenu.Menu> <PageWithMenu.Contents className={styles.settings}> <form> <section id="appearance"> <h2>{t('Settings.Appearance')}</h2> <div className={styles.setting}> <LanguageSetting /> </div> <div className={styles.setting}> <Select label={t('Settings.Theme')} name="theme" value={settings.theme} options={themeOptions} onChange={changeTheme} /> </div> </section> <section id="inventory"> <h2>{t('Settings.Inventory')}</h2> <div className={styles.setting}> <Checkbox label={t('Settings.SingleCharacter')} name="singleCharacter" value={settings.singleCharacter} onChange={onCheckChange} /> <div className={styles.fineprint}>{t('Settings.SingleCharacterExplanation')}</div> </div> {!settings.singleCharacter && ( <div className={styles.setting}> <label>{t('Settings.CharacterOrder')}</label> <ul className={styles.radioOptions}> <li> <label> <input type="radio" name="characterOrder" checked={settings.characterOrder === 'mostRecent'} value="mostRecent" onChange={onChange} /> <span>{t('Settings.CharacterOrderRecent')}</span> </label> </li> <li> <label> <input type="radio" name="characterOrder" checked={settings.characterOrder === 'mostRecentReverse'} value="mostRecentReverse" onChange={onChange} /> <span>{t('Settings.CharacterOrderReversed')}</span> </label> </li> <li> <label> <input type="radio" name="characterOrder" checked={settings.characterOrder === 'fixed'} value="fixed" onChange={onChange} /> <span>{t('Settings.CharacterOrderFixed')}</span> </label> </li> <li> <label> <input type="radio" name="characterOrder" checked={settings.characterOrder === 'custom'} value="custom" onChange={onChange} /> <span>{t('Settings.SortCustom')}</span> </label> </li> {settings.characterOrder === 'custom' && ( <CharacterOrderEditor onSortOrderChanged={characterSortOrderChanged} /> )} </ul> </div> )} <div className={styles.setting}> <Select label={t('Settings.SetVaultWeaponGrouping')} name="vaultWeaponGrouping" value={settings.vaultWeaponGrouping} options={vaultWeaponGroupingOptions} onChange={changeVaultWeaponGrouping} /> {settings.vaultWeaponGrouping && ( <Checkbox label={t('Settings.VaultWeaponGroupingStyle')} name="vaultWeaponGroupingStyle" value={settings.vaultWeaponGroupingStyle !== VaultWeaponGroupingStyle.Inline} onChange={(checked, setting) => setSetting( setting, checked ? VaultWeaponGroupingStyle.Lines : VaultWeaponGroupingStyle.Inline, ) } /> )} <Checkbox label={t('Settings.VaultArmorGroupingStyle')} name="vaultArmorGroupingStyle" value={settings.vaultArmorGroupingStyle !== VaultWeaponGroupingStyle.Inline} onChange={(checked, setting) => setSetting( setting, checked ? VaultWeaponGroupingStyle.Lines : VaultWeaponGroupingStyle.Inline, ) } /> </div> <div className={styles.setting}> <Checkbox label={t('Settings.VaultUnder')} name="vaultBelow" value={settings.vaultBelow} onChange={onCheckChange} /> </div> <div className={styles.setting}> <label htmlFor="itemSort">{t('Settings.SetSort')}</label> <SortOrderEditor order={itemSortCustom} onSortOrderChanged={itemSortOrderChanged} /> <div className={styles.fineprint}>{t('Settings.DontForgetDupes')}</div> </div> <div className={styles.setting}> {isPhonePortrait ? ( <> <Select label={t('Settings.InventoryColumnsMobile')} name="charColMobile" value={settings.charColMobile} options={charColOptions} onChange={onChangeNumeric} /> <div className={styles.fineprint}> {t('Settings.InventoryColumnsMobileLine2')} </div> </> ) : ( <Select label={t('Settings.InventoryColumns')} name="charCol" value={settings.charCol} options={charColOptions} onChange={onChangeNumeric} /> )} </div> <div className={styles.setting}> <Checkbox label={t('Settings.HidePullFromPostmaster')} name="hidePullFromPostmaster" value={settings.hidePullFromPostmaster} onChange={onCheckChange} /> </div> <div className={styles.setting}> <Checkbox label={t('Settings.BadgePostmaster')} name="badgePostmaster" value={settings.badgePostmaster} onChange={onBadgePostmasterChanged} /> <div className={styles.fineprint}>{t('Settings.BadgePostmasterExplanation')}</div> </div> <div className={styles.setting}> <Select label={t('Settings.InventoryNumberOfSpacesToClear')} name="inventoryClearSpaces" value={settings.inventoryClearSpaces} options={numberOfSpacesOptions} onChange={onChangeNumeric} /> </div> </section> <section id="items"> <h2>{t('Settings.Items')}</h2> <div className="sub-bucket"> {exampleWeapon && ( <InventoryItem item={exampleWeapon} isNew={settings.showNewItems} tag="keep" wishlistRoll={godRoll} autoLockTagged={settings.autoLockTagged} /> )} {exampleWeaponMasterworked && ( <InventoryItem item={exampleWeaponMasterworked} isNew={settings.showNewItems} tag="favorite" wishlistRoll={godRoll} autoLockTagged={settings.autoLockTagged} /> )} {exampleArmor && ( <InventoryItem item={exampleArmor} isNew={settings.showNewItems} autoLockTagged={settings.autoLockTagged} /> )} {exampleOrnament && ( <InventoryItem item={exampleOrnament} isNew={settings.showNewItems} autoLockTagged={settings.autoLockTagged} /> )} {exampleArchivedArmor && ( <InventoryItem item={exampleArchivedArmor} isNew={settings.showNewItems} tag="archive" searchHidden={true} autoLockTagged={settings.autoLockTagged} /> )} </div> {!isPhonePortrait && ( <div className={styles.setting}> <div className={styles.itemSize}> <label htmlFor="itemSize">{t('Settings.SizeItem')}</label> <input value={settings.itemSize} type="range" min="48" max="66" name="itemSize" onChange={onChangeNumeric} /> {Math.max(48, settings.itemSize)}px <button type="button" className="dim-button" onClick={resetItemSize}> {t('Settings.ResetToDefault')} </button> </div> <div className={styles.fineprint}>{t('Settings.DefaultItemSizeNote')}</div> </div> )} <div className={styles.setting}> <Checkbox label={t('Settings.OrnamentDisplay')} name="ornamentDisplay" value={settings.ornamentDisplay === OrnamentDisplay.All} onChange={(checked, name) => setSetting(name, checked ? OrnamentDisplay.All : OrnamentDisplay.None) } /> <div className={styles.fineprint}> {settings.ornamentDisplay === OrnamentDisplay.All ? t('Settings.OrnamentDisplayExplanationEnabled') : t('Settings.OrnamentDisplayExplanationDisabled')} </div> </div> {$featureFlags.newItems && ( <div className={styles.setting}> <Checkbox label={t('Settings.ShowNewItems')} name="showNewItems" value={settings.showNewItems} onChange={onCheckChange} /> <button type="button" className="dim-button" onClick={() => dispatch(clearAllNewItems())} > <NewItemIndicator className={styles.newItem} />{' '} <span>{t('Hotkey.ClearNewItems')}</span> </button> </div> )} {$featureFlags.clarityDescriptions && ( <div className={styles.setting}> <Select label={t('Settings.CommunityData')} name="descriptionsToDisplay" value={settings.descriptionsToDisplay} options={descriptionDisplayOptions} onChange={changeDescriptionDisplay} /> <div className={styles.fineprint} dangerouslySetInnerHTML={{ __html: t('Views.About.CommunityInsight', { clarityLink, clarityDiscordLink, }), }} /> </div> )} <div className={styles.setting}> <Checkbox label={t('Settings.AutoLockTagged')} name="autoLockTagged" value={settings.autoLockTagged} onChange={onCheckChange} /> <div className={styles.fineprint}>{t('Settings.AutoLockTaggedExplanation')}</div> <table className={styles.autoTagTable}> <tbody> <tr> <td> <TagIcon tag="favorite" /> <TagIcon tag="keep" /> <TagIcon tag="archive" /> </td> <td>→</td> <td> <AppIcon icon={lockIcon} /> </td> </tr> <tr> <td> <TagIcon tag="junk" /> <TagIcon tag="infuse" /> </td> <td>→</td> <td> <AppIcon icon={unlockedIcon} /> </td> </tr> </tbody> </table> </div> <div className={styles.setting}> <CustomStatsSettings /> </div> {hasD1Account && ( <div className={styles.setting}> <Checkbox label={t('Settings.EnableAdvancedStats')} name="itemQuality" value={settings.itemQuality} onChange={onCheckChange} /> </div> )} </section> {$featureFlags.wishLists && <WishListSettings />} <ErrorBoundary name="StorageSettings"> <DimApiSettings /> </ErrorBoundary> <Spreadsheets /> {$featureFlags.elgatoStreamDeck && !isPhonePortrait && <StreamDeckSettings />} <section id="troubleshooting"> <h2>{t('Settings.Troubleshooting')}</h2> <div className={styles.setting}> <Link to="/debug" className="dim-button"> Debug Info </Link> </div> <div className={styles.setting}> <label htmlFor="maxParallelCores">{t('Settings.MaxParallelCores')}</label> <div className={styles.itemSize}> <input value={maxParallelCores} type="range" min="1" max={navigator.hardwareConcurrency || 4} name="maxParallelCores" onChange={onMaxParallelCoresChanged} /> {maxParallelCores} {maxParallelCores === 1 ? 'core' : 'cores'} </div> <div className={styles.fineprint}>{t('Settings.MaxParallelCoresExplanation')}</div> </div> {currentAccount?.destinyVersion === 2 && ( <div className={styles.setting}> <TroubleshootingSettings /> </div> )} </section> </form> </PageWithMenu.Contents> </PageWithMenu> ); } function isInputElement(element: HTMLElement): element is HTMLInputElement { return element.nodeName === 'INPUT'; } ================================================ FILE: src/app/settings/SortOrderEditor.m.scss ================================================ @use '../variables' as *; .editor { composes: flexColumn from '../dim-ui/common.m.scss'; gap: 4px; padding: 4px 0 0 0; } .item { background: black; padding: 4px 8px 4px 0; display: flex; flex-direction: row; align-items: center; user-select: none; } .grabHandle { cursor: grab; touch-action: none; &:active { cursor: grabbing; } } .grip { composes: flexRow from '../dim-ui/common.m.scss'; composes: grabHandle; opacity: 0.5; font-size: 10px; padding-left: 10px; align-self: stretch; align-items: center; } .name { composes: grabHandle; flex: 1; } .sortForward { color: var(--theme-accent-primary); } .sortReverse { color: var(--theme-accent-secondary); } .button { composes: resetButton from '../dim-ui/common.m.scss'; opacity: 0.9; padding: 4px 6px; } .disabled { opacity: 0.5; } ================================================ FILE: src/app/settings/SortOrderEditor.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'button': string; 'disabled': string; 'editor': string; 'grabHandle': string; 'grip': string; 'item': string; 'name': string; 'sortForward': string; 'sortReverse': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/settings/SortOrderEditor.tsx ================================================ import { t } from 'app/i18next-t'; import { reorder } from 'app/utils/collections'; import clsx from 'clsx'; import { clamp } from 'es-toolkit'; import { Reorder, useDragControls } from 'motion/react'; import { useState } from 'react'; import { AppIcon, dragHandleIcon, faCheckSquare, faSquare, maximizeIcon, minimizeIcon, moveDownIcon, moveUpIcon, } from '../shell/icons'; import * as styles from './SortOrderEditor.m.scss'; export interface SortProperty { readonly id: string; readonly displayName: string; readonly enabled: boolean; readonly reversed: boolean; } type OnCommandHandler = ( e: React.MouseEvent, index: number, command: 'up' | 'down' | 'toggle' | 'direction-toggle', ) => void; /** * An editor for sort-orders, with drag and drop. * * This is a "controlled component" - it fires an event when the order changes, and * must then be given back the new order by its parent. */ export default function SortOrderEditor({ order, onSortOrderChanged, }: { order: SortProperty[]; onSortOrderChanged: (order: SortProperty[]) => void; }) { // Local state for dragging - use the prop order as initial value const [draggingOrder, setDraggingOrder] = useState<SortProperty[] | undefined>(); const moveItem = (oldIndex: number, newIndex: number, fromDrag = false) => { newIndex = clamp(newIndex, 0, order.length); const newOrder = reorder(order, oldIndex, newIndex); if (fromDrag) { newOrder[newIndex] = { ...newOrder[newIndex], enabled: newIndex === 0 || newOrder[newIndex - 1].enabled, }; } onSortOrderChanged(newOrder); }; const handleReorder = (newOrder: SortProperty[]) => { // During dragging, just update local state without applying business logic setDraggingOrder(newOrder); }; const handleDragEnd = (item: SortProperty) => { if (!draggingOrder) { return; // No dragging in progress } // When drag ends, apply the enabling logic and notify parent const oldIndex = order.findIndex((i) => i.id === item.id); const newIndex = draggingOrder.findIndex((i) => i.id === item.id); moveItem(oldIndex, newIndex, true); setDraggingOrder(undefined); // Reset local state after drag ends }; const toggleItem = (index: number, prop: 'enabled' | 'reversed') => { const orderArr = Array.from(order); orderArr[index] = { ...orderArr[index], [prop]: !orderArr[index][prop] }; onSortOrderChanged(orderArr); }; const onCommand: OnCommandHandler = (e, index, command) => { switch (command) { case 'up': { e.preventDefault(); moveItem(index, index - 1); break; } case 'down': { e.preventDefault(); moveItem(index, index + 1); break; } case 'toggle': { e.preventDefault(); toggleItem(index, 'enabled'); break; } case 'direction-toggle': { e.preventDefault(); toggleItem(index, 'reversed'); break; } default: break; } }; // While dragging, use local state, but when we drop we'll go back to the // order from the prop. const currentOrder = draggingOrder ?? order; return ( <Reorder.Group axis="y" values={currentOrder} onReorder={handleReorder} className={styles.editor} as="div" > {currentOrder.map((item, index) => ( <SortEditorItem key={item.id} item={item} index={index} onCommand={onCommand} onDragEnd={handleDragEnd} /> ))} </Reorder.Group> ); } function SortEditorItem({ index, item, onCommand, onDragEnd, }: { index: number; item: SortProperty; onCommand: OnCommandHandler; onDragEnd: (item: SortProperty) => void; }) { const className = clsx(styles.item, { [styles.disabled]: !item.enabled, }); // We use our own controls to avoid having the entire element be draggable. // Requires dragListener={false} on Reorder.Item. const controls = useDragControls(); // Assign this to onPointerDown to start dragging from this item const startDrag = (e: React.PointerEvent) => controls.start(e); return ( <Reorder.Item value={item} className={className} dragListener={false} dragControls={controls} whileDrag={{ // I guess we can only do inline styles here outline: '1px solid var(--theme-accent-primary)', }} onDragEnd={() => onDragEnd(item)} as="div" > <span tabIndex={-1} onPointerDown={startDrag}> <AppIcon icon={dragHandleIcon} className={styles.grip} /> </span> <button type="button" role="checkbox" aria-checked={item.enabled} className={styles.button} onClick={(e) => onCommand(e, index, 'toggle')} > <AppIcon icon={item.enabled ? faCheckSquare : faSquare} /> </button> <span className={styles.name} onPointerDown={startDrag}> {item.displayName} </span> <button type="button" className={styles.button} onClick={(e) => onCommand(e, index, 'up')}> <AppIcon icon={moveUpIcon} /> </button> <button type="button" className={styles.button} onClick={(e) => onCommand(e, index, 'down')}> <AppIcon icon={moveDownIcon} /> </button> <button type="button" title={t('Settings.ReverseSort')} className={styles.button} onClick={(e) => onCommand(e, index, 'direction-toggle')} > <AppIcon icon={item.reversed ? maximizeIcon : minimizeIcon} className={ item.enabled ? (item.reversed ? styles.sortReverse : styles.sortForward) : undefined } /> </button> </Reorder.Item> ); } ================================================ FILE: src/app/settings/Spreadsheets.m.scss ================================================ .buttons { composes: flexWrap from '../dim-ui/common.m.scss'; gap: 4px; } ================================================ FILE: src/app/settings/Spreadsheets.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttons': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/settings/Spreadsheets.tsx ================================================ import FileUpload from 'app/dim-ui/FileUpload'; import useConfirm from 'app/dim-ui/useConfirm'; import { t } from 'app/i18next-t'; import { storesLoadedSelector } from 'app/inventory/selectors'; import { downloadCsvFiles, importTagsNotesFromCsv } from 'app/inventory/spreadsheets'; import { downloadLoadoutsCsv } from 'app/loadout/spreadsheets'; import { useD2Definitions } from 'app/manifest/selectors'; import { showNotification } from 'app/notifications/notifications'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { errorMessage } from 'app/utils/errors'; import { DropzoneOptions } from 'react-dropzone'; import { useSelector } from 'react-redux'; import { AppIcon, spreadsheetIcon } from '../shell/icons'; import { settingClass } from './SettingsPage'; import * as styles from './Spreadsheets.m.scss'; export default function Spreadsheets() { const dispatch = useThunkDispatch(); const disabled = !useSelector(storesLoadedSelector); const d2Defs = useD2Definitions(); const [confirmDialog, confirm] = useConfirm(); const importCsv: DropzoneOptions['onDrop'] = async (acceptedFiles) => { if (acceptedFiles.length < 1) { showNotification({ type: 'error', title: t('Csv.ImportWrongFileType') }); return; } if (!(await confirm(t('Csv.ImportConfirm')))) { return; } try { const result = await dispatch(importTagsNotesFromCsv(acceptedFiles)); showNotification({ type: 'success', title: t('Csv.ImportSuccess', { count: result }) }); } catch (e) { showNotification({ type: 'error', title: t('Csv.ImportFailed', { error: errorMessage(e) }) }); } }; const downloadCsv = (type: 'armor' | 'weapon' | 'ghost') => dispatch(downloadCsvFiles(type)); return ( <section id="spreadsheets"> {confirmDialog} <h2>{t('Settings.Data')}</h2> <div className={settingClass}> <label htmlFor="spreadsheetLinks" title={t('Settings.ExportSSHelp')}> {t('Settings.ExportSS')} </label> <div className={styles.buttons}> <button type="button" className="dim-button" onClick={() => downloadCsv('weapon')} disabled={disabled} > <AppIcon icon={spreadsheetIcon} /> <span>{t('Bucket.Weapons')}</span> </button>{' '} <button type="button" className="dim-button" onClick={() => downloadCsv('armor')} disabled={disabled} > <AppIcon icon={spreadsheetIcon} /> <span>{t('Bucket.Armor')}</span> </button>{' '} <button type="button" className="dim-button" onClick={() => downloadCsv('ghost')} disabled={disabled} > <AppIcon icon={spreadsheetIcon} /> <span>{t('Bucket.Ghost')}</span> </button> </div> <FileUpload title={t('Settings.CsvImport')} accept={{ 'text/csv': ['.csv'] }} onDrop={importCsv} /> </div> {d2Defs && ( <div className={settingClass}> <label htmlFor="spreadsheetLinks" title={t('Settings.ExportLoadoutSSHelp')}> {t('Settings.ExportLoadoutSS')} </label> <div> <button type="button" className="dim-button" onClick={() => dispatch(downloadLoadoutsCsv())} disabled={disabled} > <AppIcon icon={spreadsheetIcon} /> <span>{t('Loadouts.Loadouts')}</span> </button> </div> </div> )} </section> ); } ================================================ FILE: src/app/settings/Troubleshooting.tsx ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { getStores } from 'app/bungie-api/destiny2-api'; import FileUpload from 'app/dim-ui/FileUpload'; import { t } from 'app/i18next-t'; import { setMockProfileResponse } from 'app/inventory/actions'; import { loadStores } from 'app/inventory/d2-stores'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { ThunkResult } from 'app/store/types'; import { download } from 'app/utils/download'; import { errorMessage } from 'app/utils/errors'; import { DestinyProfileResponse, ServerResponse } from 'bungie-api-ts/destiny2'; import { DropzoneOptions } from 'react-dropzone'; import { useSelector } from 'react-redux'; import './SettingsPage.m.scss'; /** * Allow users to export their Destiny profile and send them to a dev for * debugging. In dev mode, or if you run `enableMockProfile = true` in the * console, you can use that saved JSON profile to debug the app using another * person's data. */ export function TroubleshootingSettings() { const currentAccount = useSelector(currentAccountSelector); const dispatch = useThunkDispatch(); const saveProfileResponse = async () => { if (currentAccount) { download( JSON.stringify(await getStores(currentAccount), null, '\t'), 'profile-data.json', 'application/json', ); } }; const importMockProfile: DropzoneOptions['onDrop'] = async (files) => { if (files.length !== 1) { return; } try { await dispatch(importMockProfileResponse(files[0])); await dispatch(loadStores()); // eslint-disable-next-line no-alert alert('succeeded'); } catch (e) { // eslint-disable-next-line no-alert alert(errorMessage(e)); } }; return ( <> <button type="button" className="dim-button" onClick={saveProfileResponse}> {t('Settings.ExportProfile')} </button> {($DIM_FLAVOR === 'dev' || window.enableMockProfile) && ( <FileUpload title="Upload Profile Response JSON" accept={{ 'application/json': ['.json'] }} onDrop={importMockProfile} /> )} </> ); } function importMockProfileResponse(file: File): ThunkResult { return async (dispatch) => { const fileText = await file.text(); const profileResponseOrWrapped = JSON.parse(fileText) as | DestinyProfileResponse | ServerResponse<DestinyProfileResponse>; // if it's a full copy of the bnet Response wrapper, unwrap it let profileResponse: DestinyProfileResponse; if ('Response' in profileResponseOrWrapped) { profileResponse = profileResponseOrWrapped.Response; } else { profileResponse = profileResponseOrWrapped; } // if it doesn't look like it has what we need, throw if (!profileResponse?.profileInventory) { throw new Error('uploaded profile response looks invalid'); } dispatch( setMockProfileResponse({ ...profileResponse, // Make it always look like the freshest data so it'll overwrite any existing data responseMintedTimestamp: new Date().toISOString(), }), ); }; } ================================================ FILE: src/app/settings/WishListSettings.m.scss ================================================ .tooltipDiv { display: inline-block; } .text { width: 100%; margin: 4px 0; height: 26px; border: none; } ================================================ FILE: src/app/settings/WishListSettings.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'text': string; 'tooltipDiv': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/settings/WishListSettings.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { ConfirmButton } from 'app/dim-ui/ConfirmButton'; import ExternalLink from 'app/dim-ui/ExternalLink'; import { PressTip } from 'app/dim-ui/PressTip'; import { I18nKey, t } from 'app/i18next-t'; import { showNotification } from 'app/notifications/notifications'; import { AppIcon, banIcon, deleteIcon, plusIcon, refreshIcon } from 'app/shell/icons'; import { wishListGuideLink } from 'app/shell/links'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { errorMessage } from 'app/utils/errors'; import { builtInWishlists, validateWishListURLs, wishListAllowedHosts } from 'app/wishlists/utils'; import { fetchWishList, transformAndStoreWishList } from 'app/wishlists/wishlist-fetch'; import { toWishList } from 'app/wishlists/wishlist-file'; import { useEffect, useState } from 'react'; import { DropzoneOptions } from 'react-dropzone'; import { useSelector } from 'react-redux'; import FileUpload from '../dim-ui/FileUpload'; import HelpLink from '../dim-ui/HelpLink'; import { clearWishLists } from '../wishlists/actions'; import { wishListsLastFetchedSelector, wishListsSelector } from '../wishlists/selectors'; import Checkbox from './Checkbox'; import { fineprintClass, horizontalClass, settingClass } from './SettingsPage'; import * as styles from './WishListSettings.m.scss'; import { Settings } from './initial-settings'; export default function WishListSettings() { const dispatch = useThunkDispatch(); const settingsWishListSource = useSelector(settingSelector('wishListSource')); const wishListLastUpdated = useSelector(wishListsLastFetchedSelector); const wishList = useSelector(wishListsSelector).wishListAndInfo; const numWishListRolls = wishList.wishListRolls.length; useEffect(() => { dispatch(fetchWishList()); }, [dispatch]); // TODO: add a "local" source that can coexist with other sources? const activeWishlistUrls = settingsWishListSource ? settingsWishListSource.split('|').map((url) => url.trim()) : []; const reloadWishList = async ( reloadWishListSource: string | undefined, manualRefresh?: boolean | false, ) => { try { await dispatch(fetchWishList(reloadWishListSource, manualRefresh)); } catch (e) { showNotification({ type: 'error', title: t('WishListRoll.Header'), body: t('WishListRoll.ImportError', { url: reloadWishListSource ?? '', error: errorMessage(e), }), }); } }; const loadWishList: DropzoneOptions['onDrop'] = (acceptedFiles) => { const reader = new FileReader(); reader.onload = async () => { if (reader.result && typeof reader.result === 'string') { const wishListAndInfo = toWishList([[undefined, reader.result]]); if (wishListAndInfo.wishListRolls.length) { dispatch(clearWishLists()); } // Still attempt to store even with 0 rolls to show an error message dispatch(transformAndStoreWishList(wishListAndInfo)); } }; const file = acceptedFiles[0]; if (file) { reader.readAsText(file); } else { showNotification({ type: 'error', title: t('WishListRoll.ImportNoFile') }); } return false; }; const clearWishListEvent = () => { dispatch(clearWishLists()); }; const changeUrl = (url: string, enabled: boolean) => { const toAddOrRemove = validateWishListURLs(url); const newUrls = enabled ? [...activeWishlistUrls, ...toAddOrRemove.filter((url) => !activeWishlistUrls.includes(url))] : [...activeWishlistUrls.filter((url) => !toAddOrRemove.includes(url))]; reloadWishList(newUrls.join('|')); }; const handleReloadWishlists = () => { reloadWishList(activeWishlistUrls.join('|'), true); showNotification({ type: 'warning', title: t('Settings.WishlistRefreshNotificationTitle'), body: t('Settings.WishlistRefreshNotificationBody'), }); }; const addUrlDisabled = (url: string) => { const urls = validateWishListURLs(url); if (!urls.length) { return `${t('WishListRoll.InvalidExternalSource')}\n${wishListAllowedHosts .map((h) => `https://${h}`) .join('\n')}`; } if (!urls.some((url) => !activeWishlistUrls.includes(url))) { return t('WishListRoll.SourceAlreadyAdded'); } return false; }; const disabledBuiltinLists = builtInWishlists.filter( (list) => !activeWishlistUrls.includes(list.url), ); const hasRemoteWishList = activeWishlistUrls.length > 0; const hasLocalWishList = !hasRemoteWishList && wishList.infos.length > 0; return ( <section id="wishlist"> <h2> {t('WishListRoll.Header')} <HelpLink helpLink={wishListGuideLink} /> </h2> {numWishListRolls > 0 && ( <div className={settingClass}> <div className={horizontalClass}> <label> {t('WishListRoll.Num', { num: numWishListRolls, })} </label> <button type="button" className="dim-button" onClick={clearWishListEvent}> <AppIcon icon={banIcon} /> {t('WishListRoll.Clear')} </button> {hasRemoteWishList && ( <button type="button" className="dim-button" onClick={handleReloadWishlists}> <AppIcon icon={refreshIcon} /> {t('WishListRoll.Refresh')} </button> )} </div> {wishListLastUpdated && ( <div className={fineprintClass}> {t('WishListRoll.LastUpdated', { lastUpdatedDate: wishListLastUpdated.toLocaleDateString(), lastUpdatedTime: wishListLastUpdated.toLocaleTimeString(), })} </div> )} </div> )} {!hasLocalWishList && activeWishlistUrls.map((url) => { const loadedData = wishList.infos.find((info) => info.url === url); const builtinEntry = builtInWishlists.find((list) => list.url === url); if (builtinEntry) { return ( <BuiltinWishlist key={url} url={url} name={builtinEntry.name} title={loadedData?.title} description={loadedData?.description} rollsCount={loadedData?.numRolls} dupeRollsCount={loadedData?.dupeRolls} checked={true} onChange={(checked) => changeUrl(url, checked)} /> ); } else { return ( <UrlWishlist key={url} url={url} title={loadedData?.title} description={loadedData?.description} rollsCount={loadedData?.numRolls} dupeRollsCount={loadedData?.dupeRolls} onRemove={() => changeUrl(url, false)} /> ); } })} {!hasLocalWishList && disabledBuiltinLists.map((list) => ( <BuiltinWishlist key={list.url} url={list.url} name={list.name} title={undefined} description={undefined} checked={false} rollsCount={undefined} dupeRollsCount={undefined} onChange={(checked) => changeUrl(list.url, checked)} /> ))} {!hasLocalWishList && ( <NewUrlWishlist addWishlistDisabled={addUrlDisabled} onAddWishlist={(url) => changeUrl(url, true)} /> )} <div className={settingClass}> <FileUpload onDrop={loadWishList} title={t('WishListRoll.Import')} /> </div> </section> ); } function BuiltinWishlist({ name, url, title, description, rollsCount, dupeRollsCount, checked, onChange, }: { name: I18nKey; url: string; title: string | undefined; description: string | undefined; rollsCount: number | undefined; dupeRollsCount: number | undefined; checked: boolean; onChange: (checked: boolean) => void; }) { return ( <div className={settingClass}> <Checkbox label={t(name)} name={name as keyof Settings} value={checked} onChange={onChange} /> <ExternalLink href={url}> {rollsCount !== undefined && t('WishListRoll.NumRolls', { num: rollsCount })} {dupeRollsCount !== undefined && dupeRollsCount > 0 && t('WishListRoll.DupeRolls', { num: dupeRollsCount })} </ExternalLink> {(title || description) && ( <div className={fineprintClass}> <b>{title}</b> <br /> {description} </div> )} </div> ); } function UrlWishlist({ url, title, description, rollsCount, dupeRollsCount, onRemove, }: { url: string; title: string | undefined; description: string | undefined; rollsCount: number | undefined; dupeRollsCount: number | undefined; onRemove: () => void; }) { return ( <div className={settingClass}> <label>{title || url}</label> <ConfirmButton key="delete" danger onClick={onRemove}> <AppIcon icon={deleteIcon} title={t('Loadouts.Delete')} /> </ConfirmButton> {!title && <div className={fineprintClass}>{url}</div>} <ExternalLink href={url}> {rollsCount !== undefined && t('WishListRoll.NumRolls', { num: rollsCount })} {dupeRollsCount !== undefined && dupeRollsCount > 0 && t('WishListRoll.DupeRolls', { num: dupeRollsCount })} </ExternalLink> {description && <div className={fineprintClass}>{description}</div>} </div> ); } function NewUrlWishlist({ addWishlistDisabled, onAddWishlist, }: { addWishlistDisabled: (url: string) => string | false; onAddWishlist: (url: string) => void; }) { const [newWishlistSource, setNewWishlistSource] = useState(''); const canAddError = addWishlistDisabled(newWishlistSource); const disabled = canAddError !== false; return ( <div className={settingClass}> <div>{t('WishListRoll.ExternalSource')}</div> <div> <input type="text" className={styles.text} value={newWishlistSource} onChange={(e) => setNewWishlistSource(e.target.value)} placeholder={t('WishListRoll.ExternalSourcePlaceholder')} /> </div> <div className={styles.tooltipDiv}> <PressTip tooltip={canAddError !== undefined ? canAddError : undefined}> <button type="button" className="dim-button" disabled={disabled} onClick={() => { onAddWishlist(newWishlistSource); setNewWishlistSource(''); }} > <AppIcon icon={plusIcon} /> {t('WishListRoll.UpdateExternalSource')} </button> </PressTip> </div> </div> ); } ================================================ FILE: src/app/settings/actions.ts ================================================ import { createAction, PayloadAction } from 'typesafe-actions'; import type { Settings } from './initial-settings'; /** This one seems a bit like cheating, but it lets us set a specific property. */ export const setSettingAction = createAction( 'settings/SET', <V extends keyof Settings>(property: V, value: Settings[V]) => ({ property, value, }), )() as <V extends keyof Settings>( property: V, value: Settings[V], ) => PayloadAction<'settings/SET', { property: V; value: Settings[V] }>; /** Update a collapsible section */ export const toggleCollapsedSection = createAction('settings/COLLAPSIBLE')<string>(); /** Set the custom character order */ export const setCharacterOrder = createAction('settings/CHARACTER_ORDER')<string[]>(); ================================================ FILE: src/app/settings/character-sort.ts ================================================ import { CharacterOrder } from '@destinyitemmanager/dim-api-types'; import { settingsSelector } from 'app/dim-api/selectors'; import { RootState } from 'app/store/types'; import { compareBy, compareByIndex, reverseComparator } from 'app/utils/comparators'; import { createSelector } from 'reselect'; import { DimStore } from '../inventory/store-types'; export const characterOrderSelector = (state: RootState) => settingsSelector(state).characterOrder; const customCharacterSortSelector = (state: RootState) => settingsSelector(state).customCharacterSort; function sortCharacters( order: CharacterOrder, customCharacterSort: string[], ): (stores: readonly DimStore[]) => DimStore[] { switch (order) { case 'mostRecent': return (stores) => stores.toSorted(reverseComparator(compareBy((store) => store.lastPlayed.getTime()))); case 'mostRecentReverse': return (stores) => stores.toSorted( compareBy((store) => { if (store.isVault) { return Infinity; } else { return store.lastPlayed.getTime(); } }), ); case 'custom': { const customSortOrder = customCharacterSort; return (stores) => stores.toSorted(compareByIndex(customSortOrder, (s) => s.id)); } default: case 'fixed': // "Age" // https://github.com/Bungie-net/api/issues/614 return (stores) => stores.toSorted(compareBy((s) => s.id)); } } export const characterSortSelector = createSelector( characterOrderSelector, customCharacterSortSelector, sortCharacters, ); /** * This sorts stores by "importance" rather than how they display as columns. This is for * dropdowns and such where "mostRecentReverse" still implies that the most recent character * is most important. */ export const characterSortImportanceSelector = createSelector( characterOrderSelector, customCharacterSortSelector, (order, customCharacterSort) => sortCharacters(order === 'mostRecentReverse' ? 'mostRecent' : order, customCharacterSort), ); ================================================ FILE: src/app/settings/hooks.ts ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { setSettingAction } from './actions'; import { Settings } from './initial-settings'; /** A convenience for being able to dispatch an arbitrary setting action. */ export function useSetSetting() { const dispatch = useDispatch(); return useCallback( (...args: Parameters<typeof setSettingAction>) => dispatch(setSettingAction(...args)), [dispatch], ); } /** * Used like useState, but loads and saves a value from DIM's Settings. * * @example * * const [showNewItems, setShowNewItems] = useSetting('showNewItems'); */ export function useSetting<K extends keyof Settings>( settingName: K, ): [Setting: Settings[K], setSetting: (arg: Settings[K]) => void] { const dispatch = useDispatch(); const settingValue = useSelector(settingSelector(settingName)); const setter = useCallback( (value: Settings[K]) => dispatch(setSettingAction(settingName, value)), [dispatch, settingName], ); return [settingValue, setter]; } ================================================ FILE: src/app/settings/initial-settings.ts ================================================ import { defaultSettings, Settings as DimApiSettings } from '@destinyitemmanager/dim-api-types'; import { defaultLanguage, DimLanguage } from 'app/i18n'; /** * We extend the settings interface so we can try out new settings before * committing them to dim-api-types. * * Note: Nowadays, you *do* have to update dim-api-types and migrate the DIM * backend before using new settings in production - otherwise, the setting will * not be saved.. */ export interface Settings extends DimApiSettings { language: DimLanguage; } export const initialSettingsState: Settings = { ...defaultSettings, language: defaultLanguage(), organizerColumnsWeapons: [ 'icon', 'name', 'dmg', 'power', 'tag', 'wishList', 'archetype', 'perks', 'traits', 'originTrait', 'notes', ], organizerColumnsArmor: [ 'icon', 'name', 'power', 'energy', 'tag', 'modslot', 'intrinsics', 'perks', 'baseStats', 'customstat', 'notes', ], organizerColumnsGhost: ['icon', 'name', 'tag', 'perks', 'notes'], }; ================================================ FILE: src/app/settings/item-sort.ts ================================================ import { settingsSelector } from 'app/dim-api/selectors'; import { DimItem } from 'app/inventory/item-types'; import { getTagSelector } from 'app/inventory/selectors'; import { sortItems } from 'app/shell/item-comparators'; import { RootState } from 'app/store/types'; import { createSelector } from 'reselect'; import { Settings } from './initial-settings'; export interface ItemSortSettings { sortOrder: Settings['itemSortOrderCustom']; sortReversals: Settings['itemSortReversals']; } const itemSortOrderCustomSelector = (state: RootState) => settingsSelector(state).itemSortOrderCustom; const itemSortReversalsSelector = (state: RootState) => settingsSelector(state).itemSortReversals; export const itemSortSettingsSelector = createSelector( itemSortOrderCustomSelector, itemSortReversalsSelector, (itemSortOrderCustom, itemSortReversals) => ({ sortOrder: itemSortOrderCustom || ['primStat', 'name'], sortReversals: itemSortReversals || [], }), ); /** * Get a function that will sort items according to the user's preferences. */ export const itemSorterSelector = createSelector( itemSortSettingsSelector, getTagSelector, (sortSettings, getTag) => (items: readonly DimItem[]) => sortItems(items, sortSettings, getTag), ); ================================================ FILE: src/app/settings/settings.ts ================================================ export let readyResolve: (value?: unknown) => void; /** this promise is resolved when the initial big load of DIM API data is completed */ export const settingsReady = new Promise((resolve) => (readyResolve = resolve)); ================================================ FILE: src/app/settings/vault-grouping.test.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import store from 'app/store/store'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import { getTestDefinitions, getTestStores } from 'testing/test-utils'; import { vaultWeaponGroupingSelector, vaultWeaponGroupingSettingSelector } from './vault-grouping'; const rootState = store.getState(); describe('vaultWeaponGroupingSettingSelector', () => { it('returns the currently selected weapon grouping setting', () => { expect(vaultWeaponGroupingSettingSelector(rootState)).toBe(''); expect( vaultWeaponGroupingSettingSelector({ ...rootState, dimApi: { ...rootState.dimApi, settings: { ...rootState.dimApi.settings, vaultWeaponGrouping: 'typeName', }, }, }), ).toBe('typeName'); }); }); describe('vaultWeaponGroupingSelector', () => { let firstItem: DimItem; let items: readonly DimItem[]; beforeEach(async () => { const [, stores] = await Promise.all([getTestDefinitions(), getTestStores()]); [firstItem] = stores.flatMap((store) => store.items); items = [ { ...firstItem, typeName: 'Scout Rifle', itemCategoryHashes: [1], }, { ...firstItem, typeName: 'Auto Rifle', itemCategoryHashes: [2], }, { ...firstItem, typeName: undefined as unknown as string, itemCategoryHashes: [0 as ItemCategoryHashes], }, { ...firstItem, typeName: 'Scout Rifle', itemCategoryHashes: [1], }, { ...firstItem, typeName: 'Hand Cannon', itemCategoryHashes: [3], }, { ...firstItem, typeName: 'Auto Rifle', itemCategoryHashes: [2], }, ]; }); it('returns the items ungrouped when no grouping is selected', () => { const result = vaultWeaponGroupingSelector(rootState)(items); expect(result).toBe(items); }); it('groups items by the currently selected weapon grouping', async () => { const result = vaultWeaponGroupingSelector({ ...rootState, dimApi: { ...rootState.dimApi, settings: { ...rootState.dimApi.settings, vaultWeaponGrouping: 'typeName', }, }, })(items); expect(result).toEqual([ { groupingValue: 'Auto Rifle', icon: { type: 'typeName', itemCategoryHashes: [2], }, items: [ { ...firstItem, typeName: 'Auto Rifle', itemCategoryHashes: [2], }, { ...firstItem, typeName: 'Auto Rifle', itemCategoryHashes: [2], }, ], }, { groupingValue: 'Hand Cannon', icon: { type: 'typeName', itemCategoryHashes: [3], }, items: [ { ...firstItem, typeName: 'Hand Cannon', itemCategoryHashes: [3], }, ], }, { groupingValue: 'Scout Rifle', icon: { type: 'typeName', itemCategoryHashes: [1], }, items: [ { ...firstItem, typeName: 'Scout Rifle', itemCategoryHashes: [1], }, { ...firstItem, typeName: 'Scout Rifle', itemCategoryHashes: [1], }, ], }, { groupingValue: undefined, icon: { type: 'none', }, items: [ { ...firstItem, typeName: undefined, itemCategoryHashes: [0], }, ], }, ]); }); }); ================================================ FILE: src/app/settings/vault-grouping.ts ================================================ import { settingsSelector } from 'app/dim-api/selectors'; import { DimItem } from 'app/inventory/item-types'; import { getTagSelector } from 'app/inventory/selectors'; import { groupItems } from 'app/shell/item-comparators'; import { RootState } from 'app/store/types'; import { createSelector } from 'reselect'; export const vaultWeaponGroupingSettingSelector = (state: RootState) => settingsSelector(state).vaultWeaponGrouping; export const vaultWeaponGroupingEnabledSelector = createSelector( vaultWeaponGroupingSettingSelector, (state) => Boolean(state), ); export const vaultWeaponGroupingStyleSelector = (state: RootState) => settingsSelector(state).vaultWeaponGroupingStyle; export const vaultArmorGroupingStyleSelector = (state: RootState) => settingsSelector(state).vaultArmorGroupingStyle; /** * Get a function that will group items according to the user's preferences. */ export const vaultWeaponGroupingSelector = createSelector( vaultWeaponGroupingSettingSelector, getTagSelector, (vaultWeaponGrouping, getTag) => (items: readonly DimItem[]) => { if (!vaultWeaponGrouping) { return items; } return groupItems(items, vaultWeaponGrouping, getTag); }, ); ================================================ FILE: src/app/shell/About.m.scss ================================================ @use '../variables.scss' as *; .about { dt { font-style: normal; font-weight: 400; font-size: 1.1em; margin-top: 10px; color: var(--theme-accent-primary); } dd { margin: 4px 0 8px 0; } } .logo { float: left; margin-right: 10px; margin-top: 8px; } .header { margin-bottom: 16px; } .social { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 8px; margin: 8px 0; } ================================================ FILE: src/app/shell/About.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'about': string; 'header': string; 'logo': string; 'social': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/shell/About.tsx ================================================ import { getToken } from 'app/bungie-api/oauth-tokens'; import { clarityDiscordLink, clarityLink } from 'app/clarity/about'; import StaticPage from 'app/dim-ui/StaticPage'; import { t } from 'app/i18next-t'; import { isAppStoreVersion } from 'app/utils/browsers'; import { usePageTitle } from 'app/utils/hooks'; import { systemInfo } from 'app/utils/system-info'; import logo from 'images/dimlogo.svg'; import { useEffect } from 'react'; import { Link } from 'react-router'; import ExternalLink from '../dim-ui/ExternalLink'; import * as styles from './About.m.scss'; import { AppIcon, faDiscord, faGithub, faTshirt, heartIcon, helpIcon } from './icons'; import { discordLink, userGuideLink } from './links'; const githubLinkDirect = 'https://github.com/DestinyItemManager/DIM/'; const crowdinLinkDirect = 'https://crowdin.com/project/destiny-item-manager/invite?d=65a5l46565176393s2a3p403a3u22323e46383232393h4k4r443o4h3d4c333t2a3j4f453f4f3o4u643g393b343n4'; const bungieLinkDirect = 'https://www.bungie.net'; const openCollectiveLinkDirect = 'https://opencollective.com/dim'; const storeLinkDirect = 'https://www.designbyhumans.com/shop/DestinyItemManager/'; const githubLink = `<a href='${githubLinkDirect}' target='_blank' rel='noopener noreferrer'>GitHub</a>`; const crowdinLink = `<a href='${crowdinLinkDirect}' target='_blank' rel='noopener noreferrer'>Crowdin</a>`; const bungieLink = `<a href='${bungieLinkDirect}' target='_blank' rel='noopener noreferrer'>Bungie.net</a>`; const openCollectiveLink = `<a href='${openCollectiveLinkDirect}' target='_blank' rel='noopener noreferrer'>OpenCollective</a>`; const storeLink = `<a href='${storeLinkDirect}' target='_blank' rel='noopener noreferrer'>DesignByHumans</a>`; export default function About() { usePageTitle(t('Header.About')); // The App Store version can't show donation links I guess? const iOSApp = isAppStoreVersion(); useEffect(() => { if (iOSApp) { return; } const script = document.createElement('script'); script.src = 'https://opencollective.com/dim/banner.js?style={"a":{"display":"none"}, "h2":{"color":"white"}}'; script.async = true; document.getElementById('opencollective')!.appendChild(script); return () => { delete window.OC; }; }, [iOSApp]); const token = getToken(); return ( <StaticPage className={styles.about}> <div className={styles.header}> <img src={logo} className={styles.logo} alt="DIM Logo" height="48" width="48" /> <h1> <span>{t('Views.About.Header')}</span> </h1> <Link to="/whats-new"> <span> {t('Views.About.Version', { version: $DIM_VERSION, flavor: $DIM_FLAVOR, date: new Date($DIM_BUILD_DATE).toLocaleString(), })} </span> </Link> <br /> <span>{systemInfo}</span> <Link to="/debug">Debug</Link> </div> <p>{t('Views.About.HowItsMade')}</p> {$DIM_FLAVOR === 'release' && <p>{t(`Views.About.Schedule.release`)}</p>} {$DIM_FLAVOR === 'beta' && <p>{t(`Views.About.Schedule.beta`)}</p>} {$DIM_FLAVOR === 'pr' && ( <p> <a href={`https://github.com/DestinyItemManager/DIM/pull${$PUBLIC_PATH}`}> Pull Request #{$PUBLIC_PATH.replaceAll('/', '')} </a> </p> )} <ul> <li>{t('Views.About.BungieCopyright')}</li> <li> <Link to="/privacy">DIM Privacy Policy</Link> </li> {token && ( <li> <ExternalLink href={`https://www.bungie.net/en/Profile/ApplicationHistory/254/${token.bungieMembershipId}`} > {t('Views.About.APIHistory')} </ExternalLink> </li> )} <li dangerouslySetInnerHTML={{ __html: t('Views.About.CommunityInsight', { clarityLink, clarityDiscordLink, }), }} /> </ul> <div className={styles.social}> {!iOSApp && ( <div> <h2> <ExternalLink href={openCollectiveLinkDirect}> <AppIcon icon={heartIcon} /> {t('Views.Support.Support')} </ExternalLink> </h2> <div dangerouslySetInnerHTML={{ __html: t('Views.Support.OpenCollective', { link: openCollectiveLink }), }} /> </div> )} <div> <h2> <ExternalLink href={userGuideLink}> <AppIcon icon={helpIcon} /> {t('Views.About.Wiki')} </ExternalLink> </h2> {t('Views.About.WikiHelp')} <br /> </div> {!iOSApp && ( <div> <h2> <ExternalLink href={storeLinkDirect}> <AppIcon icon={faTshirt} /> {t('Header.Shop')} </ExternalLink> </h2> <div dangerouslySetInnerHTML={{ __html: t('Views.Support.Store', { link: storeLink }), }} /> </div> )} <div> <h2> <ExternalLink href={discordLink}> <AppIcon icon={faDiscord} /> {t('Views.About.Discord')} </ExternalLink> </h2> {t('Views.About.DiscordHelp')} </div> <div> <h2> <ExternalLink href={githubLinkDirect}> <AppIcon icon={faGithub} /> {t('Views.About.GitHub')} </ExternalLink> </h2> <div dangerouslySetInnerHTML={{ __html: t('Views.About.GitHubHelp', { link: githubLink }), }} /> </div> <div> <h2> <ExternalLink href={crowdinLinkDirect}>{t('Views.About.Translation')}</ExternalLink> </h2> <div dangerouslySetInnerHTML={{ __html: t('Views.About.TranslationText', { link: crowdinLink }), }} /> </div> </div> <h2>{t('Views.About.FAQ')}</h2> <dl> <dt>{t('Views.About.FAQMobile')}</dt> <dd>{t('Views.About.FAQMobileAnswer')}</dd> <dt>{t('Views.About.FAQLogout')}</dt> <dd>{t('Views.About.FAQLogoutAnswer')}</dd> <dt>{t('Views.About.FAQKeyboard')}</dt> <dd>{t('Views.About.FAQKeyboardAnswer')}</dd> <dt>{t('Views.About.FAQLostItem')}</dt> <dd> <div dangerouslySetInnerHTML={{ __html: t('Views.About.FAQLostItemAnswer', { link: bungieLink }), }} /> {token && ( <p> <ExternalLink href={`https://www.bungie.net/en/Profile/ApplicationHistory/254/${token.bungieMembershipId}`} > {t('Views.About.APIHistory')} </ExternalLink> </p> )} </dd> <dt>{t('Views.About.FAQAccess')}</dt> <dd>{t('Views.About.FAQAccessAnswer')}</dd> </dl> {!iOSApp && ( <> <h1>{t('Views.Support.Support')}</h1> <p>{t('Views.Support.FreeToDownload')}</p> <p> <span dangerouslySetInnerHTML={{ __html: t('Views.Support.OpenCollective', { link: openCollectiveLink }), }} />{' '} </p> {t('Views.Support.BackersDetail')} </> )} <div id="opencollective" /> </StaticPage> ); } ================================================ FILE: src/app/shell/AppInstallBanner.m.scss ================================================ .banner { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; width: 100%; padding: 8px 16px; box-sizing: border-box; text-decoration: none; background: rgb(30, 32, 43); border-bottom: 1px solid rgb(104, 111, 139); > *:first-child { flex: 1; } } .hideButton { composes: resetButton from '../dim-ui/common.m.scss'; margin-left: 16px; color: var(--theme-text); } ================================================ FILE: src/app/shell/AppInstallBanner.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'banner': string; 'hideButton': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/shell/AppInstallBanner.tsx ================================================ import { t } from 'app/i18next-t'; import { useLocalStorage } from 'app/utils/hooks'; import React from 'react'; import * as styles from './AppInstallBanner.m.scss'; import { AppIcon, closeIcon } from './icons'; const DAY = 1000 * 60 * 60 * 24; /** Shows a banner in the header on mobile asking the user to install */ export default function AppInstallBanner({ onClick }: { onClick: React.MouseEventHandler }) { const [lastDismissed, setLastDismissed] = useLocalStorage<number>( 'app-install-last-dismissed', 0, ); // Hide if they've dismissed this in the last 2 weeks if (Date.now() - lastDismissed < 14 * DAY) { return null; } const hide = (e: React.MouseEvent) => { e.stopPropagation(); setLastDismissed(Date.now()); }; return ( <a className={styles.banner} onClick={onClick}> <span>{t('Header.InstallDIMBanner')}</span> <button type="button" className={styles.hideButton} onClick={hide}> <AppIcon icon={closeIcon} /> </button> </a> ); } ================================================ FILE: src/app/shell/DefaultAccount.tsx ================================================ import SelectAccount from 'app/accounts/SelectAccount'; import { getPlatforms } from 'app/accounts/platforms'; import { accountsLoadedSelector, accountsSelector, currentAccountMembershipIdSelector, destinyVersionSelector, } from 'app/accounts/selectors'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { accountRoute } from 'app/routes'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { RootState } from 'app/store/types'; import { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { Navigate, useLocation } from 'react-router'; import ErrorPanel from './ErrorPanel'; /** * DefaultAccount handles when there is no URL path selecting a specific * account. It attempts to redirect to the last used account, or otherwise shows * either a menu of accounts or an error. */ export default function DefaultAccount() { const dispatch = useThunkDispatch(); const accounts = useSelector(accountsSelector); const accountsLoaded = useSelector(accountsLoadedSelector); const accountsError = useSelector((state: RootState) => state.accounts.accountsError); const currentAccountMembershipId = useSelector(currentAccountMembershipIdSelector); const destinyVersion = useSelector(destinyVersionSelector); const { search, pathname } = useLocation(); // Figure out where we'll go when we select a character const resultPath = useMemo(() => { // If we have a stored path from before we logged in (e.g. a loadout or armory link), send them back to that const returnPath = localStorage.getItem('returnPath'); if (returnPath) { localStorage.removeItem('returnPath'); return returnPath; } else { // Otherwise send them to wherever the current path says return `${pathname}${search}`; } }, [pathname, search]); useEffect(() => { // If currentAccountMembershipId is set we'll redirect immediately, we don't need to load accounts if (!accountsLoaded && !currentAccountMembershipId) { dispatch(getPlatforms); } }, [dispatch, accountsLoaded, currentAccountMembershipId]); // Show a loading screen while we're still loading accounts. // If currentAccountMembershipId is set we'll redirect immediately, we don't need to load accounts if (!accountsLoaded && !currentAccountMembershipId) { return <ShowPageLoading message={t('Loading.Accounts')} />; } // If we have a selected account ID, redirect to that regardless of whether it // actually exists. This help us show the correct error message on the Destiny // page and avoid bouncing to another account when Bungie.net isn't returning // all the accounts. if (currentAccountMembershipId) { return ( <Navigate to={accountRoute({ membershipId: currentAccountMembershipId, destinyVersion }) + resultPath} /> ); } // If we don't have a saved account ID, just present the list of accounts for them // to choose from, instead of selecting one based on play time or something. if (accounts.length > 0) { return ( <div className="dim-page"> <SelectAccount path={resultPath} /> </div> ); } // Finally, just show an error about there being no characters. We don't have anything else // to go on, return ( <div className="dim-page"> <ErrorPanel error={accountsError} fallbackMessage={t('Accounts.NoCharacters')} title={t('Accounts.ErrorLoading')} showSocials showReload /> </div> ); } ================================================ FILE: src/app/shell/Destiny.m.scss ================================================ .content { user-select: none; } ================================================ FILE: src/app/shell/Destiny.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'content': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/shell/Destiny.tsx ================================================ import { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import SelectAccount from 'app/accounts/SelectAccount'; import { getPlatforms, setActivePlatform } from 'app/accounts/platforms'; import { accountsLoadedSelector, accountsSelector, currentAccountSelector, } from 'app/accounts/selectors'; import ArmoryPage from 'app/armory/ArmoryPage'; import CompareContainer from 'app/compare/CompareContainer'; import { settingSelector } from 'app/dim-api/selectors'; import ErrorBoundary from 'app/dim-ui/ErrorBoundary'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import Farming from 'app/farming/Farming'; import { useHotkey, useHotkeys } from 'app/hotkeys/useHotkey'; import { t } from 'app/i18next-t'; import InfusionFinder from 'app/infuse/InfusionFinder'; import { ItemDragPreview } from 'app/inventory/ItemDragPreview'; import SyncTagLock from 'app/inventory/SyncTagLock'; import { blockingProfileErrorSelector, storesSelector } from 'app/inventory/selectors'; import { getCurrentStore } from 'app/inventory/stores-helpers'; import ItemFeedPage from 'app/item-feed/ItemFeedPage'; import LoadoutDrawerContainer from 'app/loadout-drawer/LoadoutDrawerContainer'; import { totalPostmasterItems } from 'app/loadout-drawer/postmaster'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { RootState } from 'app/store/types'; import StripSockets from 'app/strip-sockets/StripSockets'; import { setAppBadge } from 'app/utils/app-badge'; import { noop } from 'app/utils/functions'; import SingleVendorSheetContainer from 'app/vendors/single-vendor/SingleVendorSheetContainer'; import { fetchWishList } from 'app/wishlists/wishlist-fetch'; import { lazy, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { Navigate, Route, Routes, useLocation, useParams } from 'react-router'; import { Hotkey } from '../hotkeys/hotkeys'; import { itemTagList } from '../inventory/dim-item-info'; import ItemPickerContainer from '../item-picker/ItemPickerContainer'; import ItemPopupContainer from '../item-popup/ItemPopupContainer'; import * as styles from './Destiny.m.scss'; import ErrorPanel from './ErrorPanel'; // TODO: Could be slightly better to group these a bit, but for now we break them each into a separate chunk. const Inventory = lazy( () => import(/* webpackChunkName: "inventory" */ 'app/inventory-page/Inventory'), ); const Progress = lazy(() => import(/* webpackChunkName: "progress" */ 'app/progress/Progress')); const LoadoutBuilderContainer = lazy( () => import(/* webpackChunkName: "loadoutBuilder" */ 'app/loadout-builder/LoadoutBuilderContainer'), ); const D1LoadoutBuilder = lazy( () => import( /* webpackChunkName: "d1LoadoutBuilder" */ 'app/destiny1/loadout-builder/D1LoadoutBuilder' ), ); const Vendors = lazy(async () => import(/* webpackChunkName: "vendors" */ 'app/vendors/Vendors')); const SingleVendorPage = lazy( async () => import(/* webpackChunkName: "vendors" */ 'app/vendors/single-vendor/SingleVendorPage'), ); const D1Vendors = lazy( () => import(/* webpackChunkName: "d1vendors" */ 'app/destiny1/vendors/D1Vendors'), ); const RecordBooks = lazy( () => import(/* webpackChunkName: "recordbooks" */ 'app/destiny1/record-books/RecordBooks'), ); const Organizer = lazy(() => import(/* webpackChunkName: "organizer" */ 'app/organizer/Organizer')); const Activities = lazy( () => import(/* webpackChunkName: "activities" */ 'app/destiny1/activities/Activities'), ); const Records = lazy(() => import(/* webpackChunkName: "records" */ 'app/records/Records')); const Loadouts = lazy(() => import(/* webpackChunkName: "loadouts" */ 'app/loadout/Loadouts')); const SearchHistory = lazy( () => import(/* webpackChunkName: "searchHistory" */ '../search/SearchHistory'), ); /** * Base view for pages that show Destiny content. */ export default function Destiny() { const dispatch = useThunkDispatch(); const { destinyVersion: destinyVersionString, membershipId: platformMembershipId } = useParams(); const destinyVersion = parseInt( (destinyVersionString || 'd2').replace('d', ''), 10, ) as DestinyVersion; const accountsLoaded = useSelector(accountsLoadedSelector); const currentAccount = useSelector(currentAccountSelector); const account = useSelector((state: RootState) => accountsSelector(state).find( (account) => account.membershipId === platformMembershipId && account.destinyVersion === destinyVersion, ), ); const profileError = useSelector(blockingProfileErrorSelector); const autoLockTagged = useSelector(settingSelector('autoLockTagged')); useEffect(() => { if (!accountsLoaded) { dispatch(getPlatforms); } }, [dispatch, accountsLoaded]); useEffect(() => { if (account) { dispatch(setActivePlatform(account)); } }, [account, dispatch]); const isD2 = account?.destinyVersion === 2; useEffect(() => { if ($featureFlags.wishLists && isD2) { dispatch(fetchWishList()); } }, [dispatch, isD2]); const { pathname, search } = useLocation(); // Define some hotkeys without implementation, so they show up in the help useHotkey('c', t('Compare.ButtonHelp'), noop); useHotkey('l', t('Hotkey.LockUnlock'), noop); useHotkey('k', t('MovePopup.ToggleSidecar'), noop); useHotkey('v', t('Hotkey.Vault'), noop); useHotkey('p', t('Hotkey.Pull'), noop); useHotkey('i', t('MovePopup.InfuseTitle'), noop); useHotkey('a', t('Hotkey.Armory'), noop); useHotkey('shift+0', t('Tags.ClearTag'), noop); const hotkeys = useMemo(() => { const hotkeys: Hotkey[] = []; for (const tag of itemTagList) { if (tag.hotkey) { hotkeys.push({ combo: tag.hotkey, description: t('Hotkey.MarkItemAs', { tag: t(tag.label), }), callback() { // Empty - this gets redefined in item-tag.component.ts }, }); } } return hotkeys; }, []); useHotkeys(hotkeys); if ( !accountsLoaded || // This delays to wait for current account to be set in Redux so we don't get ahead of ourselves (account && !currentAccount) ) { return <ShowPageLoading message={t('Loading.Accounts')} />; } if (!account) { if (pathname.includes('/armory/')) { return <Navigate to={pathname.replace(/\/\d+\/d2/, '') + search} replace />; } else { return ( <div className="dim-page"> <ErrorPanel title={t('Accounts.MissingTitle')} fallbackMessage={t('Accounts.MissingDescription')} /> <SelectAccount path="/" /> </div> ); } } if (profileError) { const isManifestError = profileError.name === 'ManifestError'; return ( <div className="dim-page"> <ErrorPanel title={ isManifestError ? t('Accounts.ErrorLoadManifest') : t('Accounts.ErrorLoadInventory', { version: account.destinyVersion }) } error={profileError} showSocials showReload /> </div> ); } return ( <ItemPickerContainer> <SingleVendorSheetContainer> <div className={styles.content}> <Routes> <Route path="inventory" element={ <ErrorBoundary name="inventory" key="inventory"> <Inventory account={account} /> </ErrorBoundary> } /> {account.destinyVersion === 2 && ( <Route path="progress" element={ <ErrorBoundary name="progress" key="progress"> <Progress account={account} /> </ErrorBoundary> } /> )} {account.destinyVersion === 2 && ( <Route path="records" element={ <ErrorBoundary name="records" key="records"> <Records account={account} /> </ErrorBoundary> } /> )} <Route path="optimizer" element={ <ErrorBoundary name="optimizer" key="optimizer"> {account.destinyVersion === 2 ? ( <LoadoutBuilderContainer account={account} /> ) : ( <D1LoadoutBuilder account={account} /> )} </ErrorBoundary> } /> {account.destinyVersion === 2 && ( <Route path="loadouts" element={ <ErrorBoundary name="loadouts" key="loadouts"> <Loadouts account={account} /> </ErrorBoundary> } /> )} <Route path="organizer" element={ <ErrorBoundary name="organizer" key="organizer"> <Organizer account={account} /> </ErrorBoundary> } /> {account.destinyVersion === 2 && ( <Route path="vendors/:vendorHash" element={ <ErrorBoundary name="singleVendor" key="singleVendor"> <SingleVendorPage account={account} /> </ErrorBoundary> } /> )} <Route path="vendors" element={ <ErrorBoundary name="vendors" key="vendors"> {account.destinyVersion === 2 ? ( <Vendors account={account} /> ) : ( <D1Vendors account={account} /> )} </ErrorBoundary> } /> {account.destinyVersion === 2 && ( <Route path="armory/:itemHash" element={ <ErrorBoundary name="armory" key="armory"> <ArmoryPage account={account} /> </ErrorBoundary> } /> )} {account.destinyVersion === 2 && ( <Route path="item-feed" element={ <ErrorBoundary name="itemFeed" key="itemFeed"> <ItemFeedPage account={account} /> </ErrorBoundary> } /> )} {account.destinyVersion === 1 && ( <Route path="record-books" element={ <ErrorBoundary name="recordBooks" key="recordBooks"> <RecordBooks account={account} /> </ErrorBoundary> } /> )} {account.destinyVersion === 1 && ( <Route path="activities" element={ <ErrorBoundary name="activities" key="activities"> <Activities account={account} /> </ErrorBoundary> } /> )} <Route path="search-history" element={ <ErrorBoundary name="searchHistory" key="searchHistory"> <SearchHistory /> </ErrorBoundary> } /> <Route path="*" element={<Navigate to="../inventory" />} /> </Routes> </div> <LoadoutDrawerContainer account={account} /> <CompareContainer destinyVersion={account.destinyVersion} /> {account.destinyVersion === 2 && <StripSockets />} <Farming /> <InfusionFinder /> <ItemPopupContainer boundarySelector=".store-header" /> <GlobalEffects /> {Boolean(autoLockTagged) && <SyncTagLock />} <ItemDragPreview /> </SingleVendorSheetContainer> </ItemPickerContainer> ); } /** * Set some global CSS properties and such in reaction to the store. */ function GlobalEffects() { const stores = useSelector(storesSelector); // Set a CSS var for how many characters there are useEffect(() => { if (stores.length > 1) { document .querySelector('html')! .style.setProperty('--num-characters', String(stores.length - 1)); } }, [stores.length]); const badgePostmaster = useSelector(settingSelector('badgePostmaster')); // Badge the app icon with the number of postmaster items useEffect(() => { if (stores.length > 0 && badgePostmaster) { const activeStore = getCurrentStore(stores); activeStore && setAppBadge(totalPostmasterItems(activeStore)); } }, [badgePostmaster, stores]); return null; } ================================================ FILE: src/app/shell/ErrorPanel.m.scss ================================================ @use 'sass:color'; @use '../variables.scss' as *; .errorPanel { margin: 16px auto; max-width: 900px; padding: 0.85em; background: color.scale($red, $lightness: -90%); border-top: 4px solid $red; user-select: text; h2 { margin: 0 0 8px 0 !important; letter-spacing: normal !important; text-transform: none !important; } :global(.dim-button) { color: var(--theme-text); font-size: 12px; } p { font-size: 14px; white-space: pre-line; } } .links { composes: flexRow from '../dim-ui/common.m.scss'; flex-wrap: wrap; gap: 8px; } .errorCode { color: #888; margin: 0 0 0 8px; font-size: 12px; float: right; } .socials { display: flex; flex-direction: row; justify-content: space-evenly; gap: 16px; @include phone-portrait { flex-direction: column; } } .timeline { box-sizing: border-box; flex: 1; position: relative; border: 4px solid rgb(255, 255, 255, 0.3); iframe { width: 100%; overflow: auto; height: 400px; background-color: black; border: none; @include phone-portrait { height: 300px; } } } ================================================ FILE: src/app/shell/ErrorPanel.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'errorCode': string; 'errorPanel': string; 'links': string; 'socials': string; 'timeline': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/shell/ErrorPanel.tsx ================================================ import { BungieError, HttpStatusError } from 'app/bungie-api/http-client'; import ExternalLink from 'app/dim-ui/ExternalLink'; import { t } from 'app/i18next-t'; import { DimError } from 'app/utils/dim-error'; import BungieAlerts from 'app/whats-new/BungieAlerts'; import { PlatformErrorCodes } from 'bungie-api-ts/destiny2'; import { AppIcon, helpIcon, mastodonIcon, refreshIcon } from '../shell/icons'; import * as styles from './ErrorPanel.m.scss'; import { bungieHelpAccount, bungieHelpLink, troubleshootingLink } from './links'; function Socials() { return ( <div className={styles.socials}> {['https://mastodon.social/users/bungiehelp'].map((account) => ( <div key={account} className={styles.timeline}> <iframe allowFullScreen sandbox="allow-top-navigation allow-scripts allow-popups allow-popups-to-escape-sandbox" src={`https://www.mastofeed.com/apiv2/feed?userurl=${encodeURIComponent( account, )}&theme=dark&size=100&header=false&replies=false&boosts=true`} /> </div> ))} </div> ); } export default function ErrorPanel({ title, error, fallbackMessage, showSocials, showReload, frameless, }: { title?: string; error?: Error | DimError; fallbackMessage?: string; showSocials?: boolean; showReload?: boolean; /** Suitable for showing in a tooltip */ frameless?: boolean; }) { const underlyingError = error instanceof DimError ? error.cause : undefined; showSocials = showSocials ? !(error instanceof DimError) || error.showSocials : false; let code: string | number | undefined = error instanceof DimError ? error.code : undefined; if (underlyingError) { if (underlyingError instanceof BungieError) { code = underlyingError.code; } else if (underlyingError instanceof HttpStatusError) { code = underlyingError.status; } } let name = underlyingError?.name || error?.name; let message = error?.message || fallbackMessage; const ourFault = !( underlyingError instanceof BungieError || underlyingError instanceof HttpStatusError ); if (message?.includes('toSorted') || message?.includes('toReversed')) { title = t('ErrorPanel.BrowserTooOldTitle'); name = 'BrowserTooOld'; message = `${t('ErrorPanel.BrowserTooOld')}\n${navigator.userAgent}`; } const content = ( <> <h2> {title || t('ErrorBoundary.Title')} {error && ( <span className={styles.errorCode}> {name} {code !== undefined && ' '} {code} </span> )} </h2> <p> {message} {underlyingError instanceof BungieError && ( <span> {' '} {underlyingError.code === PlatformErrorCodes.SystemDisabled ? t('ErrorPanel.SystemDown') : t('ErrorPanel.Description')} </span> )} </p> {frameless ? ( <p>{t('ErrorPanel.ReadTheGuide')}</p> ) : ( <div className={styles.links}> {!ourFault && ( <ExternalLink href={bungieHelpLink} className="dim-button"> <AppIcon icon={mastodonIcon} /> {bungieHelpAccount} </ExternalLink> )} <ExternalLink href={troubleshootingLink} className="dim-button"> <AppIcon icon={helpIcon} /> {t('ErrorPanel.Troubleshooting')} </ExternalLink> {showReload && ( <div className="dim-button" onClick={() => window.location.reload()}> <AppIcon icon={refreshIcon} /> Reload </div> )} </div> )} </> ); if (frameless) { return content; } return ( <div> <div className={styles.errorPanel}>{content}</div> {showSocials && <BungieAlerts />} {showSocials && <Socials />} </div> ); } ================================================ FILE: src/app/shell/GATracker.tsx ================================================ import { gaPageView } from 'app/google'; import { useEffect } from 'react'; import { useLocation } from 'react-router'; /** * Record page views to Google Analytics. */ export default function GATracker() { const { pathname } = useLocation(); useEffect(() => { // Replace the profile membership ID so we can consolidate paths gaPageView(pathname.replace(/\/\d+\//, '/profileMembershipId/')); }, [pathname]); return null; } ================================================ FILE: src/app/shell/Header.m.scss ================================================ @use '../variables.scss' as *; body { padding-top: var(--header-height); box-sizing: border-box; min-height: var(--viewport-height); } // The height of the black header bar $header-height: 44px; .container { position: fixed; // Above the tempContainer. This is pretty much entirely so the search // autocomplete menu displays above the search results sheet on mobile. z-index: $tempContainerZindex + 2; top: 0; left: 0; right: 0; } // The actual header contents - the header element is just a container .header { width: 100%; box-sizing: border-box; padding-left: env(titlebar-area-x, env(safe-area-inset-left, 2px)); padding-right: calc( 100% - env(titlebar-area-width, 100%) - env(titlebar-area-x, 0) + env(safe-area-inset-right) ); padding-top: env(titlebar-area-y, env(safe-area-inset-top)); height: calc(env(titlebar-area-height, #{$header-height}) + env(safe-area-inset-top)); display: flex; flex-direction: row; align-items: center; background: var(--theme-header-nav-bg); background-position: center top; background-repeat: no-repeat; background-size: 100vw 100vh; -webkit-app-region: drag; // Use the size of this element for the container query in .headerLinks. This // is needed for the PWA mode where the header is narrower than the page. container-type: inline-size; @include phone-portrait { background: var(--theme-mobile-background); } button, a { -webkit-app-region: no-drag; > :global(.app-icon) { font-size: 1.33em; @include phone-portrait { font-size: 24px; } @include interactive($hover: true) { color: var(--theme-accent-primary); cursor: pointer; } } } } .logoLink { height: 24px; @include phone-portrait { height: 44px; margin: 0 4px !important; display: flex; align-items: center; } } .menuItem { margin: 0 8px; text-decoration: none; font-size: 13px; -webkit-touch-callout: none; user-select: none; white-space: nowrap; color: var(--theme-text); cursor: pointer; @include phone-portrait { margin: 0 12px; } @include interactive($hover: true) { color: var(--theme-accent-primary); } &.active { color: var(--theme-accent-primary); } } .headerRight { -webkit-app-region: no-drag; cursor: default; display: flex; flex: 1; margin: 0 6px 0 8px; align-items: center; justify-content: flex-end; flex-direction: row; height: 100%; } .menu { composes: resetButton from '../dim-ui/common.m.scss'; position: relative; margin-left: 16px !important; } .logo { height: 24px; width: 68px; color: var(--theme-accent-primary) !important; font-size: 16px !important; font-weight: bold; font-weight: 400; @include phone-portrait { height: 24px * 1.2; width: 68px * 1.2; } &.dev { filter: grayscale(100%) brightness(150%); } &.beta { filter: hue-rotate(160deg) brightness(107%); } &.pr { filter: hue-rotate(280deg) brightness(107%); } } .headerLinks { display: flex; flex-flow: row wrap; height: $header-height; align-items: center; overflow: hidden; justify-content: flex-end; @include phone-portrait { display: none; } // Hide the links if the page is too small @media (max-width: 1200px) { display: none; } // Only the newest browsers support container queries @container (max-width: 1200px) { display: none; } .menuItem { height: 26px; display: flex; align-items: center; border-bottom: 2px solid transparent; box-sizing: border-box; &.active { border-bottom: 2px solid var(--theme-accent-primary); } } } .dropdown { box-sizing: border-box; position: absolute; display: flex; overflow: auto; height: calc(var(--viewport-height) - var(--header-height)); left: 0; margin: 0; padding: 0; min-width: 150px; background-color: var(--theme-header-nav-slideout-menu-bg); padding-left: env(safe-area-inset-left); padding-bottom: Max(4px, env(safe-area-inset-bottom)); flex-direction: column; @include below-header; hr { margin: 8px 0 6px 0; border: none; border-top: 1px solid #333; } h3 { margin: 8px 2rem 0 1rem; text-transform: uppercase; letter-spacing: 1px; max-width: 13em; } .menuItem { display: block; padding: 4px 2rem 4px 1rem; font-size: 16px; margin: 0; @include phone-portrait { font-size: 18px; padding-top: 6px; padding-bottom: 6px; } @include interactive($hover: true, $focus: true) { background-color: var(--theme-accent-primary); color: var(--theme-text-invert); } &.active { color: var(--theme-text); border-left: 4px solid var(--theme-accent-primary); padding-left: calc(1rem - 4px); } .launchSeparateIcon { font-size: 1em; color: #888; } } } .pwaPrompt { margin: 1em 10px; font-size: 14px; } .searchLink { flex: 1; margin: 0 8px; @include phone-portrait { margin: 0 12px; } :global(.search-filter) { background: var(--theme-search-bg); } } .searchButton { composes: resetButton from '../dim-ui/common.m.scss'; display: none; @include phone-portrait { display: inline-block; } } ================================================ FILE: src/app/shell/Header.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'active': string; 'beta': string; 'container': string; 'dev': string; 'dropdown': string; 'header': string; 'headerLinks': string; 'headerRight': string; 'launchSeparateIcon': string; 'logo': string; 'logoLink': string; 'menu': string; 'menuItem': string; 'pr': string; 'pwaPrompt': string; 'searchButton': string; 'searchLink': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/shell/Header.tsx ================================================ import MenuAccounts from 'app/accounts/MenuAccounts'; import { currentAccountSelector } from 'app/accounts/selectors'; import { PressTipRoot } from 'app/dim-ui/PressTip'; import Sheet from 'app/dim-ui/Sheet'; import { showCheatSheet$ } from 'app/hotkeys/HotkeysCheatSheet'; import { Hotkey } from 'app/hotkeys/hotkeys'; import { useHotkeys } from 'app/hotkeys/useHotkey'; import { t } from 'app/i18next-t'; import { accountRoute } from 'app/routes'; import { SearchFilterRef } from 'app/search/SearchBar'; import DimApiWarningBanner from 'app/storage/DimApiWarningBanner'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import StreamDeckButton from 'app/stream-deck/StreamDeckButton/StreamDeckButton'; import { streamDeckEnabledSelector } from 'app/stream-deck/selectors'; import { isiOSBrowser } from 'app/utils/browsers'; import { compact } from 'app/utils/collections'; import { useSetCSSVarToHeight } from 'app/utils/hooks'; import { infoLog } from 'app/utils/log'; import clsx from 'clsx'; import logo from 'images/logo-type-right-light.svg'; import { AnimatePresence, Transition, Variants, motion } from 'motion/react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { Link, NavLink, useLocation } from 'react-router'; import { useSubscription } from 'use-subscription'; import ClickOutside from '../dim-ui/ClickOutside'; import ExternalLink from '../dim-ui/ExternalLink'; import SearchFilter from '../search/SearchFilter'; import WhatsNewLink from '../whats-new/WhatsNewLink'; import AppInstallBanner from './AppInstallBanner'; import * as styles from './Header.m.scss'; import MenuBadge from './MenuBadge'; import PostmasterWarningBanner from './PostmasterWarningBanner'; import RefreshButton from './RefreshButton'; import { setSearchQuery } from './actions'; import { installPrompt$ } from './app-install'; import { AppIcon, faExternalLinkAlt, menuIcon, searchIcon, settingsIcon } from './icons'; import { userGuideLink } from './links'; import { useIsPhonePortrait } from './selectors'; const bugReport = 'https://github.com/DestinyItemManager/DIM/issues'; const logoStyles = { beta: styles.beta, dev: styles.dev, pr: styles.pr, release: undefined, test: undefined, } as const; const menuAnimateVariants: Variants = { open: { x: 0 }, collapsed: { x: -250 }, }; const menuAnimateTransition: Transition<number> = { type: 'spring', duration: 0.3, bounce: 0 }; // TODO: finally time to hack apart the header styles! export default function Header() { const dispatch = useThunkDispatch(); const isPhonePortrait = useIsPhonePortrait(); const account = useSelector(currentAccountSelector); // Hamburger menu const [dropdownOpen, setDropdownOpen] = useState(false); const dropdownToggler = useRef<HTMLButtonElement>(null); const toggleDropdown = useCallback((e: React.MouseEvent | KeyboardEvent) => { e.preventDefault(); setDropdownOpen((dropdownOpen) => !dropdownOpen); }, []); const hideDropdown = useCallback(() => { setDropdownOpen(false); }, []); // Mobile search bar const [showSearch, setShowSearch] = useState(false); const toggleSearch = () => setShowSearch((showSearch) => !showSearch); const hideSearch = useCallback(() => { if (showSearch) { setShowSearch(false); } }, [showSearch]); // Install DIM as a PWA const [promptIosPwa, setPromptIosPwa] = useState(false); const installPromptEvent = useSubscription(installPrompt$); const showInstallPrompt = () => { setPromptIosPwa(true); setDropdownOpen(false); }; const installDim = () => { if (installPromptEvent) { installPromptEvent.prompt(); installPromptEvent.userChoice.then((choiceResult) => { if (choiceResult.outcome === 'accepted') { infoLog('install', 'User installed DIM to desktop/home screen'); } else { infoLog('install', 'User dismissed the install prompt'); } installPrompt$.next(undefined); }); } else { showInstallPrompt(); } }; // Is this running as an installed app? const isStandalone = window.navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches; const iosPwaAvailable = isiOSBrowser() && !isStandalone; const installable = installPromptEvent || iosPwaAvailable; const offerRelaunch = // as an alternative to installing, !isStandalone && !installable && // offer desktop users !isPhonePortrait; // the choice to relaunch in a no-tabs, less-UI window const reLaunchDim = () => { window.open(window.location.href, '_blank', 'resizable,scrollbars,status'); }; // Search filter const searchFilter = useRef<SearchFilterRef>(null); // Clear filter and close dropdown on path change const { pathname } = useLocation(); useEffect(() => { setDropdownOpen(false); dispatch(setSearchQuery('')); }, [dispatch, pathname]); // Focus search when shown useEffect(() => { if (searchFilter.current && showSearch) { searchFilter.current.focusFilterInput(); } }, [showSearch]); const dropdownRef = useRef<HTMLDivElement>(null); const bugReportLink = $DIM_FLAVOR !== 'release'; const navLinkClassName = ({ isActive }: { isActive: boolean }) => clsx(styles.menuItem, { [styles.active]: isActive }); // Generic links about DIM const dimLinks = ( <> <NavLink to="/about" className={navLinkClassName}> {t('Header.About')} </NavLink> <WhatsNewLink className={navLinkClassName} /> {bugReportLink && ( <ExternalLink className={styles.menuItem} href={bugReport}> {t('Header.ReportBug')} </ExternalLink> )} {isStandalone && ( <a className={styles.menuItem} onClick={() => window.location.reload()}> {t('Header.ReloadApp')} </a> )} </> ); let links: { to: string; text: string; badge?: React.ReactNode; }[] = []; if (account) { const path = accountRoute(account); links = compact([ { to: `${path}/inventory`, text: t('Header.Inventory'), }, account.destinyVersion === 2 && { to: `${path}/progress`, text: t('Progress.Progress'), }, { to: `${path}/vendors`, text: t('Vendors.Vendors'), }, account.destinyVersion === 2 && { to: `${path}/records`, text: t('Records.Title'), }, account.destinyVersion === 2 ? { to: `${path}/loadouts`, text: t('Loadouts.Loadouts') } : { to: `${path}/optimizer`, text: t('LB.LB'), }, { to: `${path}/organizer`, text: t('Organizer.Organizer'), }, account.destinyVersion === 2 && isPhonePortrait && { to: `${path}/item-feed`, text: t('ItemFeed.Description') }, account.destinyVersion === 1 && { to: `${path}/record-books`, text: t('RecordBooks.RecordBooks'), }, account.destinyVersion === 1 && { to: `${path}/activities`, text: t('Activities.Activities'), }, ]); } const linkNodes = links.map((link) => ( <NavLink className={navLinkClassName} key={link.to} to={link.to}> {link.badge} {link.text} </NavLink> )); // Links about the current Destiny version const destinyLinks = linkNodes; const hotkeys = useMemo(() => { const hotkeys: Hotkey[] = [ { combo: 'm', description: t('Hotkey.Menu'), callback: toggleDropdown, }, { combo: 'f', description: t('Hotkey.StartSearch'), callback: (event) => { if (searchFilter.current) { searchFilter.current.focusFilterInput(); if (isPhonePortrait) { setShowSearch(true); } } event.preventDefault(); event.stopPropagation(); }, }, { combo: 'shift+f', description: t('Hotkey.StartSearchClear'), callback: (event) => { if (searchFilter.current) { searchFilter.current.clearFilter(); searchFilter.current.focusFilterInput(); if (isPhonePortrait) { setShowSearch(true); } } event.preventDefault(); event.stopPropagation(); }, }, ]; return hotkeys; }, [isPhonePortrait, toggleDropdown]); useHotkeys(hotkeys); const showKeyboardHelp = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); showCheatSheet$.next(true); setDropdownOpen(false); }; // Calculate the true height of the header, for use in other things const headerRef = useRef<HTMLDivElement>(null); useSetCSSVarToHeight(headerRef, '--header-height'); const headerLinksRef = useRef<HTMLDivElement>(null); const streamDeckEnabled = $featureFlags.elgatoStreamDeck ? // eslint-disable-next-line react-hooks/rules-of-hooks useSelector(streamDeckEnabledSelector) : false; return ( <PressTipRoot value={headerRef}> <header className={styles.container} ref={headerRef}> <div className={styles.header}> <button type="button" className={clsx(styles.menuItem, styles.menu)} ref={dropdownToggler} onClick={toggleDropdown} aria-haspopup="menu" aria-label={t('Header.Menu')} aria-expanded={dropdownOpen} > <AppIcon icon={menuIcon} /> <MenuBadge /> </button> <AnimatePresence> {dropdownOpen && ( <motion.div key="dropdown" className={styles.dropdown} role="menu" initial="collapsed" animate="open" exit="collapsed" variants={menuAnimateVariants} transition={menuAnimateTransition} > <ClickOutside ref={dropdownRef} extraRef={dropdownToggler} onClickOutside={hideDropdown} > {destinyLinks} <hr /> <NavLink className={navLinkClassName} to="/settings"> {t('Settings.Settings')} </NavLink> {!isPhonePortrait && ( <a className={styles.menuItem} onClick={showKeyboardHelp}> {t('Header.KeyboardShortcuts')} </a> )} <ExternalLink className={styles.menuItem} href={userGuideLink}> {t('General.UserGuideLink')} </ExternalLink> {installable ? ( <a className={styles.menuItem} onClick={installDim}> {t('Header.InstallDIM')} </a> ) : offerRelaunch ? ( <a className={styles.menuItem} onClick={reLaunchDim}> {t('Header.LaunchDIMAlone')}{' '} <AppIcon icon={faExternalLinkAlt} className={styles.launchSeparateIcon} /> </a> ) : null} {dimLinks} <hr /> <MenuAccounts closeDropdown={hideDropdown} /> </ClickOutside> </motion.div> )} </AnimatePresence> <Link to="/" className={clsx(styles.menuItem, styles.logoLink)}> <img className={clsx(styles.logo, logoStyles[$DIM_FLAVOR])} title={`v${$DIM_VERSION} (${$DIM_FLAVOR})`} src={logo} alt="DIM" aria-label="dim" /> </Link> <div className={styles.headerLinks} ref={headerLinksRef}> {destinyLinks} </div> <div className={styles.headerRight}> {account && !isPhonePortrait && ( <span className={styles.searchLink}> <SearchFilter onClear={hideSearch} ref={searchFilter} /> </span> )} {streamDeckEnabled && <StreamDeckButton />} <RefreshButton className={styles.menuItem} /> {!isPhonePortrait && ( <Link className={styles.menuItem} to="/settings" title={t('Settings.Settings')}> <AppIcon icon={settingsIcon} /> </Link> )} <button type="button" className={clsx(styles.menuItem, styles.searchButton)} onClick={toggleSearch} > <AppIcon icon={searchIcon} /> </button> </div> </div> {account && isPhonePortrait && showSearch && ( <span className="mobile-search-link"> <SearchFilter onClear={hideSearch} ref={searchFilter} /> </span> )} {isPhonePortrait && installable && <AppInstallBanner onClick={installDim} />} <PostmasterWarningBanner /> {$featureFlags.warnNoSync && <DimApiWarningBanner />} {promptIosPwa && ( <Sheet header={<h1>{t('Header.InstallDIM')}</h1>} onClose={() => setPromptIosPwa(false)}> <p className={styles.pwaPrompt}>{t('Header.IosPwaPrompt')}</p> </Sheet> )} </header> </PressTipRoot> ); } ================================================ FILE: src/app/shell/HeaderWarningBanner.m.scss ================================================ @use '../variables.scss' as *; .banner { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; width: 100%; padding: 0 14px; box-sizing: border-box; background: rgb(151, 4, 1); font-size: 14px; @include phone-portrait { flex-direction: column-reverse; padding: 8px 14px; > *:first-child { margin-right: 0; margin-top: 8px; } } > *:first-child { margin-right: 16px; } } ================================================ FILE: src/app/shell/HeaderWarningBanner.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'banner': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/shell/HeaderWarningBanner.tsx ================================================ import React from 'react'; import * as styles from './HeaderWarningBanner.m.scss'; /** A red warning banner shown on the header of the app */ export default function HeaderWarningBanner({ children }: { children: React.ReactNode }) { return <div className={styles.banner}>{children}</div>; } ================================================ FILE: src/app/shell/LocationSwitcher.tsx ================================================ import { resetRouterLocation } from 'app/shell/actions'; import { routerLocationSelector } from 'app/shell/selectors'; import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router'; /** * Component used to auto-navigate to a selected route selected through * the Redux store using the setRouterLocation action */ export function LocationSwitcher() { const location = useSelector(routerLocationSelector); const navigate = useNavigate(); const dispatch = useDispatch(); useEffect(() => { if (location) { navigate(location); dispatch(resetRouterLocation()); } }, [dispatch, location, navigate]); return null; } ================================================ FILE: src/app/shell/MenuBadge.m.scss ================================================ @use '../variables.scss' as *; .dot { margin: 0 !important; position: absolute; border-radius: 50%; box-shadow: 0 0 0 2px var(--theme-pwa-background); // Use PWA fill var as it'll always be solid (not a gradient) @include phone-portrait { box-shadow: 0 0 0 2px var(--theme-mobile-background); } } .badgeNew { composes: dot; bottom: 0; right: -5px; display: block; background-color: $new-notification-dot; height: 8px; width: 8px; } .upgrade { composes: dot; bottom: -8px; right: -12px; color: $upgrade-notification-dot !important; font-size: 18px !important; } ================================================ FILE: src/app/shell/MenuBadge.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'badgeNew': string; 'dot': string; 'upgrade': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/shell/MenuBadge.tsx ================================================ import { dimNeedsUpdate$ } from 'app/register-service-worker'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { useEventBusListener } from 'app/utils/hooks'; import { GlobalAlertLevelsToToastLevels } from 'app/whats-new/BungieAlerts'; import { DimVersions } from 'app/whats-new/versions'; import clsx from 'clsx'; import { useCallback, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { useSubscription } from 'use-subscription'; import * as styles from './MenuBadge.m.scss'; import { pollForBungieAlerts } from './alerts'; import { AppIcon, updateIcon } from './icons'; import { refresh$ } from './refresh-events'; import { bungieAlertsSelector } from './selectors'; /** * A badge for the hamburger menu - must be kept in sync with WhatsNewLink, but may also incorporate other sources. * * Using inheritance to keep better in sync with WhatsNewLink. */ export default function MenuBadge() { // TODO: Incorporate settings/storage (e.g. DIM Sync disabled/busted) const showChangelog = useSubscription(DimVersions.showChangelog$); const alerts = useSelector(bungieAlertsSelector); const dimNeedsUpdate = useSubscription(dimNeedsUpdate$); const dispatch = useThunkDispatch(); const getAlerts = useCallback(() => dispatch(pollForBungieAlerts()), [dispatch]); useEventBusListener(refresh$, getAlerts); useEffect(() => { getAlerts(); }, [getAlerts]); if (dimNeedsUpdate) { return <AppIcon className={styles.upgrade} icon={updateIcon} />; } if (alerts.length) { return ( <span className={clsx( styles.badgeNew, `bungie-alert-${GlobalAlertLevelsToToastLevels[alerts[0].AlertLevel]}`, )} /> ); } if (showChangelog) { return <span className={styles.badgeNew} />; } return null; } ================================================ FILE: src/app/shell/PostmasterWarningBanner.tsx ================================================ import { t } from 'app/i18next-t'; import { PullFromPostmaster } from 'app/inventory/PullFromPostmaster'; import { currentStoreSelector } from 'app/inventory/selectors'; import { POSTMASTER_SIZE, postmasterAlmostFull, postmasterSpaceUsed, } from 'app/loadout-drawer/postmaster'; import { memo } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import HeaderWarningBanner from './HeaderWarningBanner'; import { useIsPhonePortrait } from './selectors'; /** Shows a warning anywhere in the app if your active character's postmaster is low. */ export default memo(function PostmasterWarningBanner() { // if postmaster low on most recent character // and we're not on the inventory screen || isPhonePortrait // show collect button // animate in const store = useSelector(currentStoreSelector); const isPhonePortrait = useIsPhonePortrait(); const { pathname } = useLocation(); const onInventory = pathname.endsWith('inventory'); // We don't show this on the desktop inventory page, you can already see it. if (!store || (!isPhonePortrait && onInventory)) { return null; } const storeIsDestiny2 = store?.destinyVersion === 2; const isPostmasterAlmostFull = store && postmasterAlmostFull(store); const postMasterSpaceUsed = store ? postmasterSpaceUsed(store) : 0; const showPostmasterFull = storeIsDestiny2 && isPostmasterAlmostFull; if (!showPostmasterFull) { return null; } const data = { number: postMasterSpaceUsed, postmasterSize: POSTMASTER_SIZE, }; const text = postMasterSpaceUsed < POSTMASTER_SIZE ? t('PostmasterWarningBanner.PostmasterAlmostFull', data) : t('PostmasterWarningBanner.PostmasterFull', data); return ( <HeaderWarningBanner> <PullFromPostmaster store={store} /> <span>{text}</span> </HeaderWarningBanner> ); }); ================================================ FILE: src/app/shell/Privacy.m.scss ================================================ .privacy { ol, p { color: #ccc; line-height: 1.2em; } li { margin-bottom: 4px; margin-top: 4px; } } ================================================ FILE: src/app/shell/Privacy.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'privacy': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/shell/Privacy.tsx ================================================ import ExternalLink from 'app/dim-ui/ExternalLink'; import StaticPage from 'app/dim-ui/StaticPage'; import { Link } from 'react-router'; import * as styles from './Privacy.m.scss'; export default function Privacy() { return ( <StaticPage className={styles.privacy}> <h1>Privacy Policy</h1> <p>Last updated August 23, 2020</p> <ol style={{ padding: 0, listStylePosition: 'inside' }}> <li> <strong>Introduction.</strong> <ol> <li> Destiny Item Manager ("DIM") is a free, open source, fan made service for the Destiny and Destiny 2 video games. This privacy policy explains how your data is used by this application. </li> <li> DIM is constantly improving, and we may modify this Privacy Policy from time to time to reflect changes in our privacy practices. You are encouraged to review this Privacy Policy periodically and to check the "Last Updated" date at the top of the Privacy Policy for the most recent version. </li> </ol> </li> <li> <strong>How we use your personal data.</strong> <ol> <li> <strong>Usage data.</strong> <ol> <li> We may process data about your use of our website and services ("usage data"). The usage data may include your IP address, geographical location, browser type and version, operating system, referral source, length of visit, page views and website navigation paths, as well as information about the timing, frequency and pattern of your service use. The source of the usage data is our analytics tracking system. This usage data may be processed for the purposes of analyzing the use of the website and services. The legal basis for this processing is your consent or our legitimate interests, namely monitoring and improving our website and services. </li> <li> We may use Google Analytics to analyze the use of our website. Google Analytics gathers information about website use by means of cookies. The information gathered relating to our website is used to create reports about the use of our website. Besides generic usage data, we also share your Bungie.net membership ID with Google Analytics to help provide a more accurate measure of how users use DIM. Google's privacy policy is available at:{' '} <ExternalLink href="https://www.google.com/policies/privacy/"> https://www.google.com/policies/privacy/ </ExternalLink> . We respect the Do Not Track setting. If you want to opt out of Google Analytics, you may use the{' '} <ExternalLink href="https://chrome.google.com/webstore/detail/google-analytics-opt-out/fllaojicojecljbmefodhfapmkghcbnh?hl=en"> Google Analytics Opt Out Extension for Chrome </ExternalLink>{' '} or similar tools for other browsers. </li> </ol> </li> <li> <strong>Destiny and Bungie account info.</strong> <ol> <li> In order to display and manipulate Destiny game information, DIM uses the Bungie.net API. You must grant permission for DIM to use this API through Bungie.net. The only information DIM receives, or has access to, is your game information (items, characters, etc.) and basic account information including your Bungie.net membership ID, and the identifiers of any linked services such as your public PSN, Xbox Live, Steam, Stadia, Blizzard or Bungie.net usernames. We do not have access to your email, name, address, payment information, or any other personal information held by Bungie or the game platforms. </li> <li> DIM only stores your Destiny and Bungie information locally on your own device and in memory, in order to provide DIM's functionality. We do not store any of this information anywhere that the DIM maintainers and contributors can access it. </li> <li> Use of the Bungie.net API is governed by the{' '} <ExternalLink href="https://www.bungie.net/7/en/Legal/Terms"> Terms of Use </ExternalLink>{' '} and{' '} <ExternalLink href="https://www.bungie.net/7/en/Legal/PrivacyPolicy"> Privacy Policy </ExternalLink>{' '} for Bungie.net. </li> </ol> </li> <li> <strong>DIM Sync: Settings (preferences), loadouts, tags and notes.</strong> <ol> <li> DIM allows you to connect to DIM Sync, a cloud service operated by the DIM team, in order to store your data and sync it between instances of DIM or other Destiny apps. This information is only accessible to you and the DIM team. Information stored in DIM Sync includes your DIM preferences and settings, loadouts, any per-item item tags and notes, saved and recently used search filters, and tracked triumphs. Your Bungie.net authentication information is sent to DIM Sync only in order to verify your account - it is not saved. </li> </ol> </li> {$featureFlags.sentry && ( <li> <strong>Sentry: Error reporting</strong> <ol> <li> Errors encountered while using DIM may be sent to Sentry, a service provided by Functional Software, Inc. These error reports contain information about your browser, recent actions in DIM as well as the details of any errors. No personal information is shared with Sentry. </li> <li> Use of Sentry for error reporting is governed by the Sentry{' '} <ExternalLink href="https://sentry.io/terms/">Terms of Service</ExternalLink>{' '} and{' '} <ExternalLink href="https://sentry.io/privacy/">Privacy Policy</ExternalLink>. </li> </ol> </li> )} </ol> </li> <li> <strong>Who can I ask if I have additional questions?</strong> <ol> <li> For additional inquiries about the privacy of your information, you can contact us via any of the means listed on our <Link to="/about">About page</Link> </li> </ol> </li> </ol> </StaticPage> ); } ================================================ FILE: src/app/shell/RefreshButton.m.scss ================================================ @use 'sass:color'; @use '../variables' as *; @use '../dim-ui/tooltip-mixins' as *; .refreshButton { composes: resetButton from '../dim-ui/common.m.scss'; position: relative; } .userIsPlaying { background-color: #3fe700; // Just for fun - on Safari, on wide color displays, this is a very bright green background-color: lch(81% 132 132); height: 6px; width: 6px; border-radius: 50%; position: absolute; bottom: -4px; right: -4px; } .outOfDate { color: $red; background-color: #1b1b2d; padding: 2px; border-radius: 50%; position: absolute; bottom: -8px; right: -8px; :global(.app-icon) { font-size: 10px !important; display: block; } } .errorTooltip { max-width: 800px; @include tooltip-background-color(color.scale($red, $lightness: -90%)); @include tooltip-border-color($red); @include tooltip-ribbon-color($red); @include phone-portrait { max-width: 95%; } } .errorDetails { h2 { margin: 0 0 8px 0; } p { margin-bottom: 1em; } } ================================================ FILE: src/app/shell/RefreshButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'errorDetails': string; 'errorTooltip': string; 'outOfDate': string; 'refreshButton': string; 'userIsPlaying': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/shell/RefreshButton.tsx ================================================ import { destinyVersionSelector } from 'app/accounts/selectors'; import { PressTip, useTooltipCustomization } from 'app/dim-ui/PressTip'; import { useHotkey } from 'app/hotkeys/useHotkey'; import { t } from 'app/i18next-t'; import { isDragging$ } from 'app/inventory/drag-events'; import { autoRefreshEnabledSelector, profileErrorSelector, profileMintedSelector, } from 'app/inventory/selectors'; import { useEventBusListener } from 'app/utils/hooks'; import { i15dDurationFromMsWithSeconds } from 'app/utils/time'; import clsx from 'clsx'; import { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { useSubscription } from 'use-subscription'; import ErrorPanel from './ErrorPanel'; import * as styles from './RefreshButton.m.scss'; import { AppIcon, faClock, faExclamationTriangle, refreshIcon } from './icons'; import { loadingTracker } from './loading-tracker'; import { refresh } from './refresh-events'; /** We consider the profile stale if it's out of date with respect to the game data by this much */ const STALE_PROFILE_THRESHOLD = 90_000; const MIN_SPIN = 1000; // 1 second export default function RefreshButton({ className }: { className?: string }) { const [disabled, setDisabled] = useState(false); const autoRefresh = useSelector(autoRefreshEnabledSelector); const handleChanges = useCallback( () => setDisabled(!navigator.onLine || document.hidden || isDragging$.getCurrentValue()), [], ); const active = useSubscription(loadingTracker.active$); // Always show the spinner for at least MIN_SPIN milliseconds const [spin, setSpin] = useState(active ? Date.now() : 0); useEffect(() => { if (active && spin === 0) { setSpin(Date.now()); } else if (!active && spin !== 0) { const elapsed = Date.now() - spin; const remainingTime = Math.max(0, MIN_SPIN - elapsed); if (remainingTime > 0) { const timer = window.setTimeout(() => { setSpin(0); }, remainingTime); return () => window.clearTimeout(timer); } else { setSpin(0); } } }, [active, spin]); useEventBusListener(isDragging$, handleChanges); useEffect(() => { document.addEventListener('visibilitychange', handleChanges); document.addEventListener('online', handleChanges); return () => { document.removeEventListener('visibilitychange', handleChanges); document.removeEventListener('online', handleChanges); }; }, [handleChanges]); useHotkey('r', t('Hotkey.RefreshInventory'), refresh); const outOfDate = useProfileOutOfDate(); const profileError = useSelector(profileErrorSelector); const showOutOfDateWarning = outOfDate && !active && !autoRefresh; return ( <PressTip tooltip={<RefreshButtonTooltip autoRefresh={autoRefresh} />}> <button type="button" className={clsx(styles.refreshButton, className, { disabled })} onClick={refresh} title={t('Header.Refresh') + (autoRefresh ? `\n${t('Header.AutoRefresh')}` : '')} aria-keyshortcuts="R" > <AppIcon icon={refreshIcon} spinning={spin !== 0} /> {autoRefresh && <div className={styles.userIsPlaying} />} {(profileError || showOutOfDateWarning) && ( <div className={styles.outOfDate}> <AppIcon icon={profileError ? faExclamationTriangle : faClock} /> </div> )} </button> </PressTip> ); } function useProfileAge() { const profileMintedDate = useSelector(profileMintedSelector); const [_tickState, setTickState] = useState(0); useEffect(() => { const interval = setInterval(() => setTickState((t) => t + 1), 1000); return () => clearInterval(interval); }, []); return profileAge(profileMintedDate); } function profileAge(profileMintedDate: Date) { return profileMintedDate.getTime() === 0 ? undefined : Date.now() - profileMintedDate.getTime(); } function profileOutOfDate(profileMintedDate: Date) { const profileAgeMs = profileAge(profileMintedDate); return profileAgeMs !== undefined && profileAgeMs > STALE_PROFILE_THRESHOLD; } /** Like useProfileAge but only sets a boolean to avoid lots of re-renders. */ function useProfileOutOfDate() { const profileMintedDate = useSelector(profileMintedSelector); const [outOfDate, setOutOfDate] = useState(() => profileOutOfDate(profileMintedDate)); useEffect(() => { setOutOfDate(profileOutOfDate(profileMintedDate)); const interval = setInterval(() => { setOutOfDate(profileOutOfDate(profileMintedDate)); }, 1000); return () => clearInterval(interval); }, [profileMintedDate]); return outOfDate; } function RefreshButtonTooltip({ autoRefresh }: { autoRefresh: boolean }) { const profileAge = useProfileAge(); const profileError = useSelector(profileErrorSelector); const isManifestError = profileError?.name === 'ManifestError'; const destinyVersion = useSelector(destinyVersionSelector); useTooltipCustomization({ className: profileError ? styles.errorTooltip : null, }); return ( <> {profileError ? ( <div className={styles.errorDetails}> <ErrorPanel title={ isManifestError ? t('Accounts.ErrorLoadManifest') : t('Accounts.ErrorLoadInventory', { version: destinyVersion }) } error={profileError} frameless /> </div> ) : ( <> <b>{t('Header.Refresh') + (autoRefresh ? `\n${t('Header.AutoRefresh')}` : '')}</b> {profileAge !== undefined && ( <div>{t('Header.ProfileAge', { age: i15dDurationFromMsWithSeconds(profileAge) })}</div> )} </> )} </> ); } ================================================ FILE: src/app/shell/ScrollToTop.tsx ================================================ import { useEffect } from 'react'; import { useLocation } from 'react-router'; /** * https://reacttraining.com/react-router/web/guides/scroll-restoration * * We need this until we drop support for pre-Chromium Edge and iOS < 13.1: * https://caniuse.com/#feat=mdn-api_history_scrollrestoration */ export default function ScrollToTop() { const { pathname } = useLocation(); useEffect(() => { window.scrollTo(0, 0); }, [pathname]); return null; } ================================================ FILE: src/app/shell/SneakyUpdates.tsx ================================================ import { dimNeedsUpdate$, reloadDIM } from 'app/register-service-worker'; import { useEffect, useRef } from 'react'; import { useLocation } from 'react-router'; /** * "Sneaky Updates" - reload on navigation if DIM needs an update. */ export default function SneakyUpdates() { const { pathname } = useLocation(); const initialLoad = useRef(true); useEffect(() => { if (!initialLoad.current && dimNeedsUpdate$.getCurrentValue()) { reloadDIM(); } initialLoad.current = false; }, [pathname]); return null; } ================================================ FILE: src/app/shell/actions.ts ================================================ import { GlobalAlert } from 'bungie-api-ts/core'; import { createAction } from 'typesafe-actions'; /** Set whether we're in phonePortrait view mode. */ export const setPhonePortrait = createAction('shell/PHONE_PORTRAIT')<boolean>(); /** * Set the current search query text. Only the search filter input component should set * updateVersion - all other uses should ignore that parameter. */ export const setSearchQuery = createAction( 'shell/SEARCH_QUERY', // Another lint auto-fixes this by removing :boolean, then thinks `updateVersion` is `any` // eslint-disable-next-line (query: string, updateVersion: boolean = true) => ({ query, updateVersion, }), )(); /** * Toggle in or out a specific search query component from the existing search. */ export const toggleSearchQueryComponent = createAction( 'shell/TOGGLE_SEARCH_QUERY_COMPONENT', )<string>(); export const toggleSearchResults = createAction( 'shell/TOGGLE_SEARCH_RESULTS', (open?: boolean) => open, )(); /** * Set the current location path */ export const setRouterLocation = createAction( 'shell/SET_ROUTER_LOCATION', (location?: string) => location, )(); /** * Reset the current location path */ export const resetRouterLocation = createAction('shell/RESET_ROUTER_LOCATION')(); /** * Update the known list of Bungie.net alerts. */ export const updateBungieAlerts = createAction('shell/BUNGIE_ALERTS')<GlobalAlert[]>(); /** * Signifies that there is a page-wide loading state, with a message. * These shouldn't be used directly - use loadingStart and loadingEnd. */ export const loadingStart = createAction('shell/LOADING')<string>(); export const loadingEnd = createAction('shell/LOADING_DONE')<string>(); ================================================ FILE: src/app/shell/alerts.ts ================================================ import { getGlobalAlerts } from 'app/bungie-api/bungie-core-api'; import { ThunkResult } from 'app/store/types'; import { errorLog } from 'app/utils/log'; import { updateBungieAlerts } from './actions'; let bungieAlertsLastUpdated = 0; /** * Update Bungie alerts. Throttled to not run more often than once per 10 minutes. */ export function pollForBungieAlerts(): ThunkResult { return async (dispatch) => { if (Date.now() - bungieAlertsLastUpdated > 10 * 60 * 1000) { try { dispatch(updateBungieAlerts(await getGlobalAlerts())); bungieAlertsLastUpdated = Date.now(); } catch (e) { errorLog('BungieAlerts', 'Unable to get Bungie.net alerts: ', e); } } }; } ================================================ FILE: src/app/shell/app-install.ts ================================================ import { Observable } from 'app/utils/observable'; export const installPrompt$ = new Observable<BeforeInstallPromptEvent | undefined>(undefined); // This event is fired whenever the browser thinks it's possible to install the app. We then have // to save the event itself to use it to show the install prompt from a menu item click. window.addEventListener('beforeinstallprompt', (e) => { // Prevent Chrome 67 and earlier from automatically showing the prompt e.preventDefault(); // Stash the event so it can be triggered later. installPrompt$.next(e); }); ================================================ FILE: src/app/shell/formatters.ts ================================================ /** * Given a value 0-1, returns a string describing it as a percentage from 0-100 */ export function percent(val: number): string { return `${Math.min(100, Math.floor((1000 * val) / 10))}%`; } /** * Given a value 0-1, returns a string describing it as a percentage from 0-100 */ export function percentWithSingleDecimal(val: number): string { return `${Math.min(100, Math.floor(1000 * val) / 10).toLocaleString()}%`; } /** * Given a value on (or outside) a 0-100 scale, returns a css color value. This * is used when comparing item stats. */ export function getCompareColor(value: number) { let color = ''; if (value < 0) { color = '#e0e0e0'; } else if (value < 50) { color = 'rgb(253 98 98)'; } else if (value < 80) { color = 'hsl(49, 58.60%, 56.50%)'; } else if (value < 100) { color = 'hsl(120, 54.70%, 60.20%)'; } else if (value >= 100) { color = 'oklch(82.08% 0.1561 215.44)'; } return color; } /** * Given a value on (or outside) a 0-100 scale, returns a css color key and * value for a react `style` attribute. This is the color scale DIM used for a * long time, and it's preserved here for D1 reasons. */ export function getD1QualityColor(value: number, property = 'background-color') { let color = 0; if (value < 0) { return { [property]: 'white' }; } else if (value <= 85) { color = 0; } else if (value <= 90) { color = 20; } else if (value <= 95) { color = 60; } else if (value < 100) { color = 120; } else if (value >= 100) { color = 190; } return { [property]: `hsl(${color},65%,50%, 1)`, }; } ================================================ FILE: src/app/shell/icons/AppIcon.scss ================================================ /* stylelint-disable font-family-no-missing-generic-family-keyword */ /* stylelint-disable scss/function-no-unknown */ /* stylelint-disable scss/at-function-pattern */ @use 'sass:math'; @use 'sass:string'; @use './font-awesome.scss'; // We manually copy specific files from the main "fontawesome" module to slim // things down. We used to import them directly from the node module, but Sass // decided to deprecate the import feature we were using. So we're doing it // manually now. // It was: // // We import specific files from the main "fontawesome" module to slim things down // @import '@fortawesome/fontawesome-free/scss/variables'; // @import '@fortawesome/fontawesome-free/scss/mixins'; // @import '@fortawesome/fontawesome-free/scss/core'; // // @import '@fortawesome/fontawesome-free/scss/larger'; // // @import '@fortawesome/fontawesome-free/scss/fixed-width'; // // @import '@fortawesome/fontawesome-free/scss/list'; // // @import '@fortawesome/fontawesome-free/scss/bordered-pulled'; // @import '@fortawesome/fontawesome-free/scss/animated'; // // @import '@fortawesome/fontawesome-free/scss/rotated-flipped'; // // @import '@fortawesome/fontawesome-free/scss/stacked'; // @import '@fortawesome/fontawesome-free/scss/icons'; // // @import '@fortawesome/fontawesome-free/scss/screen-reader'; // // And this imports the actual fonts // @import '@fortawesome/fontawesome-free/scss/solid'; // @import '@fortawesome/fontawesome-free/scss/regular'; // @import '@fortawesome/fontawesome-free/scss/brands'; /* Importing FontAwesome SCSS from node modules */ // Variables // -------------------------- $fa-font-path: 'data/webfonts' !default; // We customized this. $fa-font-size-base: 16px !default; $fa-font-display: block !default; $fa-css-prefix: fa !default; $fa-version: '5.15.4' !default; $fa-border-color: #eee !default; $fa-inverse: #fff !default; $fa-li-width: 2em !default; $fa-fw-width: math.div(20em, 16); $fa-primary-opacity: 1 !default; $fa-secondary-opacity: 0.4 !default; /*! * Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */ @font-face { font-family: 'Font Awesome 5 Free'; font-style: normal; font-weight: 900; font-display: $fa-font-display; src: url('#{$fa-font-path}/fa-solid-900.eot'); src: // url('#{$fa-font-path}/fa-solid-900.eot?#iefix') format('embedded-opentype'), url('#{$fa-font-path}/fa-solid-900.woff2') format('woff2'); // url('#{$fa-font-path}/fa-solid-900.woff') format('woff'), // url('#{$fa-font-path}/fa-solid-900.ttf') format('truetype'), // url('#{$fa-font-path}/fa-solid-900.svg#fontawesome') format('svg'); } .fa, .fas { font-family: 'Font Awesome 5 Free'; font-weight: 900; } /*! * Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */ @font-face { font-family: 'Font Awesome 5 Free'; font-style: normal; font-weight: 400; font-display: $fa-font-display; src: url('#{$fa-font-path}/fa-regular-400.eot'); src: // url('#{$fa-font-path}/fa-regular-400.eot?#iefix') format('embedded-opentype'), url('#{$fa-font-path}/fa-regular-400.woff2') format('woff2'); // url('#{$fa-font-path}/fa-regular-400.woff') format('woff'), // url('#{$fa-font-path}/fa-regular-400.ttf') format('truetype'), // url('#{$fa-font-path}/fa-regular-400.svg#fontawesome') format('svg'); } .far { font-family: 'Font Awesome 5 Free'; font-weight: 400; } /*! * Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */ @font-face { font-family: 'Font Awesome 5 Brands'; font-style: normal; font-weight: 400; font-display: $fa-font-display; src: url('#{$fa-font-path}/fa-brands-400.eot'); src: // url('#{$fa-font-path}/fa-brands-400.eot?#iefix') format('embedded-opentype'), url('#{$fa-font-path}/fa-brands-400.woff2') format('woff2'); // url('#{$fa-font-path}/fa-brands-400.woff') format('woff'), // url('#{$fa-font-path}/fa-brands-400.ttf') format('truetype'), // url('#{$fa-font-path}/fa-brands-400.svg#fontawesome') format('svg'); } .fab { font-family: 'Font Awesome 5 Brands'; font-weight: 400; } // Convenience function used to set content property @function fa-content($fa-var) { @return string.unquote('"#{ $fa-var }"'); } // Mixins // -------------------------- @mixin fa-icon { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: inline-block; font-style: normal; font-variant: normal; font-weight: normal; line-height: 1; } @mixin fa-icon-rotate($degrees, $rotation) { transform: rotate($degrees); } @mixin fa-icon-flip($horiz, $vert, $rotation) { transform: scale($horiz, $vert); } // Only display content to screen readers. A la Bootstrap 4. // // See: http://a11yproject.com/posts/how-to-hide-content/ @mixin sr-only { border: 0; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } // Use in conjunction with .sr-only to only display content when it's focused. // // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 // // Credit: HTML5 Boilerplate @mixin sr-only-focusable { &:active, &:focus { clip-path: none; height: auto; margin: 0; overflow: visible; position: static; width: auto; } } // Base Class Definition // ------------------------- .#{$fa-css-prefix}, .fas, .far, .fal, .fad, .fab { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; display: inline-block; font-style: normal; font-variant: normal; text-rendering: auto; line-height: 1; } %fa-icon { @include fa-icon; } // Animated Icons // -------------------------- .#{$fa-css-prefix}-spin { animation: fa-spin 2s infinite linear; } .#{$fa-css-prefix}-pulse { animation: fa-spin 1s infinite steps(8); } @keyframes fa-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ================================================ FILE: src/app/shell/icons/AppIcon.tsx ================================================ import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import clsx from 'clsx'; import { memo } from 'react'; import './AppIcon.scss'; function AppIcon({ icon, className, title, spinning, ariaHidden, }: { icon: string | IconDefinition; className?: string; title?: string; spinning?: boolean; ariaHidden?: boolean; }) { if (typeof icon === 'string') { return ( <span className={clsx(icon, 'app-icon', className, spinning ? 'fa-spin' : false)} title={title} aria-hidden={ariaHidden} /> ); } else { return ( <FontAwesomeIcon className={clsx('app-icon', className)} aria-hidden={ariaHidden} icon={icon} title={title} spin={spinning} /> ); } } export default memo(AppIcon); ================================================ FILE: src/app/shell/icons/Library.js ================================================ // https://fontawesome.com/v5/search // IMPORTANT: run `pnpm fa-subset` after changing this file or new icons won't show up export const archiveIcon = 'fas fa-archive'; export const updateIcon = 'fas fa-arrow-circle-up'; export const rightArrowIcon = 'fas fa-arrow-right'; export const moveIcon = rightArrowIcon; export const banIcon = 'fas fa-ban'; export const menuIcon = 'fas fa-bars'; export const boltIcon = 'fas fa-bolt'; export const faCalculator = 'fas fa-calculator'; export const faCheck = 'fas fa-check'; export const faCheckCircle = 'fas fa-check-circle'; export const faCheckSquare = 'fas fa-check-square'; export const faSquare = 'far fa-square'; export const enabledIcon = faCheckCircle; export const unselectedCheckIcon = 'far fa-check-circle'; export const settingsIcon = 'fas fa-cog'; export const copyIcon = 'fas fa-copy'; export const downloadIcon = 'fas fa-file-export'; export const sendIcon = 'fas fa-envelope'; export const clearIcon = 'fas fa-eraser'; export const levelDownIcon = 'fas fa-level-down-alt'; export const levellingIcon = 'fas fa-level-up-alt'; export const lockIcon = 'fas fa-lock'; export const unlockedIcon = 'fas fa-unlock'; export const faCaretDown = 'fas fa-caret-down'; export const collapseIcon = faCaretDown; export const editIcon = 'fas fa-pencil-alt'; export const plusIcon = 'fas fa-plus'; export const minusIcon = 'fas fa-minus'; export const expandIcon = 'fas fa-caret-right'; export const faAngleRight = 'fas fa-angle-right'; export const faAngleLeft = 'fas fa-angle-left'; export const maximizeIcon = 'fas fa-angle-double-left'; export const minimizeIcon = 'fas fa-angle-double-right'; export const addIcon = 'fas fa-plus-circle'; export const faPlusSquare = 'fas fa-plus-square'; export const helpIcon = 'far fa-question-circle'; export const infoIcon = 'fas fa-info-circle'; export const searchIcon = 'fas fa-search'; export const signOutIcon = 'fas fa-sign-out-alt'; export const expandDownIcon = 'fas fa-chevron-down'; export const expandUpIcon = 'fas fa-chevron-up'; export const moveDownIcon = 'fas fa-arrow-down'; export const moveUpIcon = 'fas fa-arrow-up'; export const shareIcon = 'fas fa-share'; export const shoppingCart = 'fas fa-shopping-cart'; export const starIcon = 'fas fa-star'; export const starOutlineIcon = 'far fa-star'; export const refreshIcon = 'fas fa-sync'; export const spreadsheetIcon = 'fas fa-table'; export const tagIcon = 'fas fa-tag'; export const faList = 'fas fa-list'; export const faGrid = 'fas fa-th'; export const thumbsUpIcon = 'fas fa-thumbs-up'; export const thumbsDownIcon = 'fas fa-thumbs-down'; export const closeIcon = 'fas fa-times'; export const faTimesCircle = 'fas fa-times-circle'; export const faWindowClose = 'fas fa-window-close'; export const disabledIcon = faTimesCircle; export const deleteIcon = 'far fa-trash-alt'; export const mastodonIcon = 'fab fa-mastodon'; export const undoIcon = 'fas fa-undo'; export const redoIcon = 'fas fa-redo'; export const uploadIcon = 'fas fa-file-import'; export const heartIcon = 'fas fa-heart'; export const globeIcon = 'fas fa-globe'; export const stickyNoteIcon = 'fas fa-sticky-note'; export const faMinusSquare = 'fas fa-minus-square'; export const faRandom = 'fas fa-random'; export const faEquals = 'fas fa-equals'; export const kebabIcon = 'fas fa-ellipsis-v'; export const faArrowCircleDown = 'fas fa-arrow-circle-down'; export const faArrowCircleUp = 'fas fa-arrow-circle-up'; export const faExclamationCircle = 'fas fa-exclamation-circle'; export const faCaretUp = 'fas fa-caret-up'; export const faClock = 'far fa-clock'; export const faExclamationTriangle = 'fas fa-exclamation-triangle'; export const faTshirt = 'fas fa-tshirt'; export const faExternalLinkAlt = 'fas fa-external-link-alt'; export const trackedIcon = 'fas fa-bookmark'; export const unTrackedIcon = 'far fa-bookmark'; export const compareIcon = 'fas fa-balance-scale-left'; export const dragHandleIcon = 'fas fa-grip-vertical'; export const pinIcon = 'fas fa-thumbtack'; export const faXbox = 'fab fa-xbox'; export const faPlayStation = 'fab fa-playstation'; export const faSteam = 'fab fa-steam'; export const faDiscord = 'fab fa-discord'; export const faGithub = 'fab fa-github'; export const saveIcon = 'fas fa-save'; export const greaterThanIcon = 'fas fa-greater-than-equal'; export const lessThanIcon = 'fas fa-less-than-equal'; export const equalsIcon = 'fas fa-equals'; export const stackIcon = 'fas fa-layer-group'; export const slashIcon = 'fas fa-slash'; ================================================ FILE: src/app/shell/icons/custom/Artifice.ts ================================================ import { makeCustomIcon } from './utils'; export const artificeIcon = makeCustomIcon( 'ArtificeIcon', 32, 32, 'M15.5, 23.2a7.2,7.2 0 1,0 0,-14.4zM15.5,30.2a14.2,14.2 1 1,1 0,-28.4m0,7a7.2,7.2 0 1,0 0,14.4zM15.5,32a16,16 1 1,0 0,-32m0,1.8a14.2,14.2 0 1,1 0,28.4z', ); ================================================ FILE: src/app/shell/icons/custom/Engram.ts ================================================ import { makeCustomIcon } from './utils'; export const dimEngramIcon = makeCustomIcon( 'Engram', 100, 100, 'M33.62 23.176h32.76l9.412-13.48L50 1.316l-25.79 8.38 9.41 13.48zm37.985 3.742l10.14 31.202 15.052 4.37V35.317L80.96 13.52l-9.354 13.398zM50 77.984l25.436-18.48-9.715-29.902H34.28l-9.715 29.903L50 77.985zm3.214 5.61v15.088l25.71-8.353 15.8-21.75L79.83 64.25 53.21 83.59zm-6.427 0L20.172 64.256 5.278 68.58l15.8 21.75 25.71 8.352V83.594zm-18.39-56.676l-9.357-13.4-15.836 21.8V62.49l15.053-4.37 10.14-31.202z', ); ================================================ FILE: src/app/shell/icons/custom/Enhanced.ts ================================================ import { makeCustomIcon } from './utils'; export const dimEnhancedIcon = makeCustomIcon( 'Enhanced', 32, 32, 'm0 17.25h7l0 2h4c1 0 3 1 3.75 2v10.75zm32 0h-7l0 2h-4c-1 0-3 1-3.75 2v10.75zm-32-2.5h7l0-2h4c1 0 3-1 3.75-2v-10.75zm32 0h-7l0-2h-4c-1 0-3-1-3.75-2v-10.75z', ); ================================================ FILE: src/app/shell/icons/custom/Epic.ts ================================================ import { makeCustomIcon } from './utils'; export const epicIcon = makeCustomIcon( 'Epic', 648, 751, 'm588.396 0h-529.625c-42.928 0-58.771 15.842-58.771 58.79v518.235c0 4.86.196 9.375.625 13.557.978 9.375 1.162 18.459 9.88 28.802.852 1.013 9.754 7.637 9.754 7.637 4.788 2.348 8.057 4.077 13.457 6.251l260.795 109.264c13.538 6.206 19.2 8.625 29.033 8.43v.002h.077v-.002c9.834.195 15.495-2.224 29.035-8.43l260.793-109.264c5.402-2.174 8.67-3.903 13.459-6.251 0 0 8.901-6.624 9.752-7.637 8.719-10.343 8.903-19.427 9.88-28.802.429-4.182.627-8.697.627-13.557v-518.235c0-42.948-15.844-58.79-58.771-58.79m-35.474 521.016-.12 1.196-.119 1.314-.24 1.196-.357 1.194-.232 1.076-.36 1.196-.477 1.076-.479.958-.471 1.074-.597.956-.597.958-.711.956-.599.956-.709.836-.837.837-.711.717-.955.838-.83.717-.956.717-.95.59-.949.717-1.076.597-1.069.479-1.068.597-1.196.479-1.188.479-1.188.357-1.188.36-1.076.357-1.188.24-1.07.239-1.068.239-1.196.24-1.189.118-1.068.12-1.196.12-1.307.12-1.189.119h-4.999l-1.189-.119h-1.307l-1.189-.12-1.195-.12-1.308-.12-1.188-.238-1.188-.12-1.196-.239-1.189-.239-1.188-.24-1.188-.238-1.195-.358-1.069-.24-1.188-.358-1.189-.359-1.075-.479-1.188-.358-1.069-.358-1.19-.478-1.074-.479-1.07-.479-1.069-.589-1.075-.479-1.069-.598-.949-.596-1.076-.599-.95-.597-.956-.598-1.898-1.434-.956-.718-.951-.717-.836-.717-.949-.836.717-.956.829-.838.718-.957.83-.956.718-.836.709-.956.837-.957.71-.836.837-.956.711-.838.836-.956.71-.957.718-.836.829-.956.717-.957.83-.836.717-.956.949.717 1.07.717.956.718 1.069.717.956.597 1.069.716.948.599 1.078.477.948.599 1.069.479 1.076.477 1.068.479 1.071.358 1.074.478 1.188.358 1.19.359 1.068.359 1.196.24 2.376.477 1.308.12 1.194.239h1.19l1.306.118h2.616l1.308-.118 1.196-.239 1.068-.24 1.07-.239.956-.358.83-.478.956-.717.71-.837.478-.958.36-1.074.118-1.196v-.239l-.118-1.435-.478-1.196-.599-.836-.829-.717-.958-.717-.949-.478-1.068-.478-1.196-.479-1.427-.596-.83-.24-.948-.239-1.076-.36-1.07-.238-1.188-.359-1.196-.239-1.306-.358-1.19-.239-1.308-.359-1.188-.238-1.196-.359-1.186-.24-1.19-.359-1.076-.357-1.186-.24-1.071-.359-1.068-.358-1.076-.359-1.308-.479-1.188-.478-1.189-.478-1.195-.478-1.068-.598-1.189-.597-.956-.597-1.07-.598-.949-.717-.956-.597-.829-.719-.836-.836-.831-.837-.717-.838-.71-.836-.598-.836-.591-.957-.597-1.075-.479-.829-.359-.957-.35-1.076-.359-.956-.24-1.196-.239-1.076-.12-1.195-.118-1.196-.12-1.313v-2.751l.12-1.195.118-1.076.12-1.196.239-1.074.24-1.076.359-1.078.238-1.074.471-1.076.479-1.076.477-1.075.592-1.077.596-1.075.711-.957.718-.956.83-.956.836-.836.83-.958.956-.837.83-.597.956-.718.95-.717.948-.597 1.076-.598 1.07-.478 1.068-.598 1.076-.477 1.188-.36 1.188-.477 1.076-.359.949-.24 1.071-.239 1.074-.238 1.188-.239 1.07-.24 1.188-.119 1.196-.12 1.188-.12 1.188-.118h5.118l1.308.118 1.308.12h1.188l1.315.12 1.188.239 1.308.12 1.189.239 1.076.238 1.188.239 1.188.24 1.07.239 1.194.358 1.07.359 1.069.24 1.195.477 1.069.359 1.068.477 1.069.479 1.195.479 1.069.597.95.479 1.076.597 1.069.597.949.599 1.075.717.951.596.956.719.949.717.948.716-.709.958-.598.956-.71.956-.718.957-.709 1.075-.599.956-.717.957-.711.956-.596.956-.712.957-.716.956-.598.956-.71 1.076-.718.956-.71.956-.597.956-.711.956-.956-.716-1.068-.598-.95-.598-.956-.717-1.068-.477-.958-.599-1.068-.479-.949-.477-1.076-.479-.949-.477-1.068-.359-.956-.478-1.31-.358-1.188-.36-1.188-.357-1.196-.24-1.188-.239-1.188-.24-1.188-.12-1.196-.118-1.068-.12h-2.616l-1.308.238-1.196.24-1.068.239-.949.479-.837.477-1.07.957-.717 1.076-.469 1.076-.122 1.195v.24l.122 1.553.589 1.315.479.717.829.838 1.076.597.949.597 1.188.479 1.315.479 1.428.477.949.239.95.358 1.076.239 1.068.359 1.188.24 1.316.358 1.308.358 1.308.36 1.306.238 1.188.359 1.308.359 1.196.238 1.19.359 1.188.359 1.068.358 1.196.359 1.069.359 1.068.358 1.315.471 1.188.599 1.188.477 1.189.597 1.076.599 1.069.597.948.598.956.717.951.597.949.838.956.836.83.957.836.836.71.957.718 1.075.59.956.599 1.077.477.956.359 1.075.352 1.076.239 1.076.477 2.392.12 1.195.12 1.314v2.87zm-82.269 23.902h-69.38v-87.011h68.782v19.725h-46.057v14.224h41.416v18.522h-41.416v14.821h46.655zm-86.954 0h-22.965v-51.452l-.596.956-.711 1.084-.598.956-.71.957-.597 1.075-.719.957-.589.956-.599 1.083-.709.956-.599.956-.716.957-.59 1.083-.718.956-.59.956-.599 1.076-.717.962-.591.958-.717 1.074-.59.958-.717.956-.591 1.082-.598.958-.716.956-.591 1.076-.717.962-.591.956-.717.958-.599 1.074-.591.963-.716.957-.59 1.076-.718.956-.597.956-.711 1.084-.597.956h-.472l-.716-1.076-.599-.964-.71-1.076-.598-.956-.71-1.074-.598-.964-.709-1.076-.599-.958-.717-1.074-.591-.956-.717-1.084-.589-.956-.718-1.076-.598-.956-.71-1.083-.598-.956-.71-1.076-.717-.956-.597-1.083-.711-.956-.597-1.076-.711-.956-.597-1.084-.711-.956-.597-1.076-.718-.956-.59-1.076-.718-.964-.59-1.074-.718-.958-.597-1.076-.709-.954-.6-1.084-.71-.956v51.212h-22.613v-87.011h24.4l.591.957.597 1.075.709.949.599 1.076.597.956.591 1.078.597.948.712 1.077.596.956.599.956.591 1.068.597.957.711 1.075.597.957.597 1.068.591.956.717.956.599 1.071.589.955.598 1.076.592.957.717 1.068.597.957.59 1.075.598.949.591.956.717 1.076.598.958.59 1.076.598.949.59 1.076.719.956.597 1.068.591.957.597-.957.598-1.068.71-.956.596-1.076.592-.949.598-1.076.71-.958.598-1.076.598-.956.59-.949.717-1.075.591-.957.598-1.068.596-.957.711-1.076.597-.955.599-1.071.589-.956.6-.956.71-1.068.597-.957.597-1.075.592-.957.716-1.068.592-.956.596-.956.599-1.077.709-.948.599-1.078.589-.956.599-1.076.717-.949.591-1.075.597-.957h24.392zm-139.266-35.855-.359-1.083-.471-1.196-.478-1.076-.359-1.082-.471-1.076-.477-1.196-.36-1.083-.479-1.076-.469-1.075-.36-1.082-.479-1.196-.35-1.076-.479-1.083-.479-1.076-.357-1.076-.472-1.201-.477-1.078-.359-1.074-.479-1.083-.47-1.196-.359-1.083-.479-1.076-.47 1.076-.358 1.083-.478 1.196-.479 1.083-.352 1.074-.477 1.078-.479 1.201-.471 1.076-.36 1.076-.477 1.083-.479 1.076-.352 1.196-.477 1.082-.479 1.075-.357 1.076-.472 1.083-.479 1.196-.477 1.076-.351 1.082-.478 1.076-.479 1.196-.359 1.083-.47 1.076h20.109zm38.56 35.855h-24.518l-.472-1.078-.358-1.076-.478-1.076-.478-1.194-.352-1.07-.478-1.074-.478-1.076-.352-1.076-.478-1.076-.478-1.076-.358-1.076-.472-1.195-.478-1.076-.359-1.076-.479-1.076h-34.152l-.47 1.076-.359 1.076-.479 1.076-.477 1.195-.352 1.076-.477 1.076-.479 1.076-.359 1.076-.47 1.076-.479 1.074-.359 1.07-.472 1.194-.477 1.076-.359 1.076-.478 1.078h-24.034l.471-1.078.479-1.076.477-1.076.472-1.194.359-1.07.478-1.074.478-1.076.47-1.076.479-1.076.479-1.196.47-1.076.479-1.075.357-1.076.479-1.076.471-1.076.478-1.195.478-1.076.472-1.077.478-1.075.478-1.076.358-1.076.471-1.196.479-1.074.478-1.076.471-1.076.957-2.152.471-1.196.359-1.075.478-1.076.478-1.076.472-1.076.477-1.076.477-1.195.472-1.076.478-1.076.358-1.076.479-1.074.471-1.076.478-1.196.479-1.076.471-1.076.477-1.076.479-1.075.358-1.077.472-1.068.478-1.196.477-1.075.472-1.077.477-1.076.479-1.075.471-1.076.358-1.196.479-1.075.477-1.076.472-1.076.479-1.076.477-1.076.472-1.196.478-1.074.358-1.076.478-1.076.472-1.076.477-1.076.479-1.195.47-1.076.958-2.152.35-1.075.479-1.077.479-1.195.478-1.076.471-1.076.479-1.076.477-1.076.471-1.074.36-1.196.477-1.076.479-1.076.47-1.076h22.016l.478 1.076.478 1.076.472 1.076.478 1.196.358 1.074.471 1.076.479 1.076.477 1.076.479 1.076.47 1.195.479 1.077.479 1.075.35 1.076.958 2.152.478 1.195.471 1.076.477 1.076.479 1.076.471 1.076.358 1.074.478 1.196.472 1.076.478 1.076.478 1.076.478 1.076.471 1.075.477 1.196.36 1.076.471 1.075.477 1.076.479 1.077.479 1.075.47 1.196.479 1.068.477 1.077.352 1.075.478 1.076.478 1.076.472 1.076.478 1.196.478 1.076.479 1.074.471 1.076.358 1.076.478 1.076.472 1.195.477 1.076.479 1.076.477 1.076.472 1.076.479 1.075.357 1.196.472 1.076.477 1.076.479 1.076.471 1.076.478 1.074.479 1.196.479 1.076.35 1.076.479 1.075.479 1.077.47 1.076.478 1.195.478 1.076.478 1.076.47 1.076.36 1.075.479 1.076.471 1.196.954 2.152.472 1.076.478 1.074.478 1.07.36 1.194.47 1.076.478 1.076zm-105.444-11.112-.956.717-.831.599-.956.717-.949.597-.95.718-.956.597-.949.599-1.076.596-1.069.598-1.07.598-1.074.47-1.07.599-1.188.477-1.068.479-1.196.477-1.068.479-1.07.359-1.075.358-1.069.36-1.188.358-1.068.36-1.196.238-1.07.239-1.188.239-1.194.238-1.19.12-1.308.12-1.188.12-1.308.119-1.196.12-1.306.119h-5.12l-1.188-.119-1.308-.12-1.188-.119-1.188-.12-1.196-.12-1.188-.238-1.19-.24-1.068-.239-1.194-.239-1.07-.358-1.188-.239-1.076-.36-1.069-.477-1.069-.359-1.187-.479-1.076-.477-1.069-.477-1.068-.479-1.078-.597-.948-.592-1.069-.598-.956-.598-.949-.596-.956-.719-.95-.716-.95-.717-.836-.718-.83-.717-.837-.837-.829-.716-.838-.838-.829-.837-.717-.836-.711-.956-.717-.837-.598-.956-.71-.958-.596-.954-.592-.958-.598-1.076-.597-.956-.471-1.076-.478-1.076-.478-1.076-.472-1.074-.358-.956-.479-1.078-.239-1.076-.352-1.074-.238-1.195-.359-1.077-.239-1.195-.12-1.076-.24-1.196-.117-1.075-.121-1.196-.112-1.196-.119-1.194v-3.946l.119-1.314v-1.195l.112-1.196.121-1.315.239-1.188.118-1.196.239-1.076.359-1.194.24-1.196.35-1.076.359-1.195.359-1.076.478-1.196.472-1.075.478-1.076.478-1.076.471-1.076.597-1.076.598-.956.592-.957.596-.956.598-.956.71-.956.717-.956.711-.956.717-.839.829-.836.718-.837.83-.835.836-.838.83-.716.956-.838.83-.717.95-.717.956-.598.949-.717.957-.597.948-.598 1.07-.598 1.076-.598 1.068-.597 1.069-.479 1.075-.477 1.189-.599.949-.357 1.188-.36 1.078-.477 1.068-.24 1.068-.359 1.194-.239 1.07-.358 1.188-.239 1.189-.12 1.195-.239 1.189-.12 1.187-.118 1.189-.12 1.196-.12h5.111l1.316.12 1.308.12h1.188l1.188.118 1.308.24 1.076.119 1.188.24 1.189.239 1.075.238 1.068.239 1.07.24 1.076.359 1.068.238.95.359 1.076.359 1.069.477 1.068.479 1.076.478 1.069.598 1.068.478.958.598 1.068.597.949.597 1.076.599.949.716.95.599.956.717.949.717.956.836.949.718-.717.957-.829.836-.718.956-.829.956-.717.957-.711.836-.837.956-.71.956-.717.839-.831.956-.716.956-.83.956-.718.837-.709.956-.836.956-.712.956-.836.838-.711.956-.956-.718-.949-.836-.956-.598-.949-.718-.95-.597-.956-.599-.95-.596-1.068-.598-.956-.478-.95-.478-.956-.359-1.069-.359-1.068-.358-1.076-.239-1.188-.24-1.188-.239-1.189-.118-1.308-.12-1.315-.119h-2.496l-1.188.119-1.076.12-1.188.238-1.069.24-1.076.358-1.068.358-1.07.479-1.069.477-.956.598-.948.598-.957.599-.83.716-.837.717-.829.718-.718.836-.829.837-.717.956-.592.837-.598.956-.589 1.076-.599.956-.477 1.076-.359 1.076-.472 1.076-.358 1.195-.239 1.189-.239 1.075-.24 1.316-.118 1.195-.112 1.194v2.751l.112 1.195.118 1.076.12 1.196.239 1.074.24 1.078.239 1.074.358 1.076.352.956.479 1.195.477 1.077.591 1.076 1.195 1.912.71.958.718.836.829.837.719.836.828.717.957.718.949.717.948.597.956.598 1.071.478 1.068.479 1.075.477 1.189.359 1.188.359 1.196.24 1.188.238 1.188.119 1.308.12h2.735l1.316-.12 1.308-.119 1.188-.12 1.188-.24 1.196-.238 1.188-.358 1.068-.24 1.07-.477.956-.479 1.068-.478.836-.478.951-.598v-10.878h-17.376v-17.445h39.272v38.843zm-38.275-272.769v86.798h54.447v39.897h-98.245v-287.935h97.429v39.896h-53.631v81.448h51.578v39.896zm369.434 8.643h42.983v67.045c0 35.791-17.599 53.475-53.215 53.475h-21.699c-35.608 0-53.216-17.684-53.216-53.475v-185.922c0-35.79 17.608-53.475 53.216-53.475h21.292c35.607 0 52.8 17.277 52.8 53.06v58.827h-42.977v-56.359c0-11.517-5.324-16.861-16.377-16.861h-7.37c-11.46 0-16.784 5.344-16.784 16.861v181.816c0 11.519 5.324 16.863 16.784 16.863h8.192c11.047 0 16.371-5.344 16.371-16.863zm-152.1 118.052v-287.936h43.806v287.936zm-63.559-160.01v-72.398c0-11.516-5.316-16.861-16.369-16.861h-18.015v106.128h18.015c11.053 0 16.369-5.351 16.369-16.869zm-9.414-127.926c35.614 0 53.214 17.686 53.214 53.475v76.512c0 35.783-17.6 53.467-53.214 53.467h-24.97v104.482h-43.8v-287.936zm-84.83 548.287h250.739l-127.982 42.206z', ); ================================================ FILE: src/app/shell/icons/custom/FeaturedBanner.ts ================================================ import { makeCustomIcon } from './utils'; export const featuredBannerIcon = makeCustomIcon( 'FeaturedBanner', 24, 28, 'm0 0h24v27.889l-12-3.874-12 3.442z', ); ================================================ FILE: src/app/shell/icons/custom/Hunter.ts ================================================ import { makeCustomIcon } from './utils'; export const dimHunterIcon = makeCustomIcon( 'Hunter', 32, 32, 'm9.055 10.446 6.945-.023-6.948 10.451 6.948-.024-7.412 11.15h-7.045l7.036-10.428h-7.036l7.032-10.422h-7.032l7.507-11.126 6.95-.024zm13.89 0-6.945-10.446 6.95.024 7.507 11.126h-7.032l7.032 10.422h-7.036l7.036 10.428h-7.045l-7.412-11.15 6.948.024-6.948-10.451z', ); ================================================ FILE: src/app/shell/icons/custom/HunterProportional.ts ================================================ import { makeCustomIcon } from './utils'; export const dimHunterProportionalIcon = makeCustomIcon( 'HunterProportional', 32, 22, 'm11.297 7 4.703-.016-4.705 7.078 4.705-.016-5.02 7.551h-4.77l4.764-7.062h-4.764l4.762-7.059h-4.762l5.083-7.534 4.707-.017zm9.406 0-4.703-7.075 4.707.017 5.083 7.534h-4.762l4.762 7.059h-4.764l4.764 7.062h-4.77l-5.02-7.551 4.705.016-4.705-7.078z', ); export default dimHunterProportionalIcon; ================================================ FILE: src/app/shell/icons/custom/MasterworkHammer.ts ================================================ import { makeCustomIcon } from './utils'; export const masterworkHammer = makeCustomIcon( 'MasterworkHammer', 32, 32, 'M0,0L0,16L8,16L8,10L6.5,9.5L6.5,6.5L8,6L8,0zM32,0L32,16L24,16L24,10L25.5,9.5L25.5,6.5L24,6L24,0zM14,0L18,0L18,1Q 16,1.8 14,1zM13.5,18L18.5,18L18.5,32L13.5,32zM9.5,1.2L12,1.2Q 16,3.5 20,1.2L22.5,1.2L22.5,14.8L20,14.8Q 16,12.6 12,14.8L9.5,14.8zM13,15.5Q 16,14 19,15.5L19,17L13,17z', ); ================================================ FILE: src/app/shell/icons/custom/Power.ts ================================================ import { makeCustomIcon } from './utils'; export const dimPowerIcon = makeCustomIcon( 'Power', 32, 32, 'M16 32l-7.245-8.755-8.755-7.245 8.755-7.321 7.245-8.679 7.245 8.679 8.755 7.321-8.755 7.245zM9.811 16l3.396 2.792 2.792 3.396 2.792-3.396 3.396-2.792-3.396-2.868-2.792-3.321-2.792 3.321z', ); ================================================ FILE: src/app/shell/icons/custom/PowerAlt.ts ================================================ import { makeCustomIcon } from './utils'; export const dimPowerAltIcon = makeCustomIcon( 'Power', 32, 32, 'M22.962 8.863c-2.628-2.576-4.988-5.407-7.045-8.458l-0.123-0.193c-2.234 3.193-4.556 5.993-7.083 8.592l0.015-0.016c-2.645 2.742-5.496 5.245-8.542 7.499l-0.184 0.13c3.341 2.271 6.262 4.682 8.943 7.335l-0.005-0.005c2.459 2.429 4.71 5.055 6.731 7.858l0.125 0.182c4.324-6.341 9.724-11.606 15.986-15.649l0.219-0.133c-3.401-2.168-6.359-4.524-9.048-7.153l0.010 0.010zM18.761 18.998c-1.036 1.024-1.971 2.145-2.792 3.35l-0.050 0.078c-0.884-1.215-1.8-2.285-2.793-3.279l0 0c-1.090-1.075-2.28-2.055-3.552-2.923l-0.088-0.057c1.326-0.969 2.495-1.988 3.571-3.097l0.007-0.007c1.010-1.051 1.947-2.191 2.794-3.399l0.061-0.092c0.882 1.32 1.842 2.471 2.912 3.51l0.005 0.005c1.089 1.072 2.293 2.034 3.589 2.864l0.088 0.053c-1.412 0.905-2.641 1.891-3.754 2.994l0.002-0.002z', ); ================================================ FILE: src/app/shell/icons/custom/Shaped.ts ================================================ import { makeCustomIcon } from './utils'; export const dimShapedIcon = makeCustomIcon( 'Shaped', 32, 32, 'm0 17.616h6.517c5.314 0 5.486 5.073 5.486 7.192l-.003 3.288h2.53v3.904h-14.53zm31.997 0h-6.517c-5.314 0-5.486 5.073-5.486 7.192l.003 3.288h-2.53v3.904h14.53zm-31.994-3.232h6.517c5.314 0 5.486-5.073 5.486-7.192l-.003-3.288h2.53v-3.904h-14.53zm31.997 0h-6.517c-5.314 0-5.486-5.073-5.486-7.192l.003-3.288h-2.53v-3.904h14.53z', ); ================================================ FILE: src/app/shell/icons/custom/StatBarsIcon.ts ================================================ import { makeCustomIcon } from './utils'; export const statBarsIcon = makeCustomIcon( 'StatBars', 20, 32, 'M10 6.5h10v4h-10zM5 14.5h15v4h-15zM0 22.5h20v4h-20z', ); ================================================ FILE: src/app/shell/icons/custom/Titan.ts ================================================ import { makeCustomIcon } from './utils'; export const dimTitanIcon = makeCustomIcon( 'Titan', 32, 32, 'm14.839 15.979-13.178-7.609v15.218zm2.322 0 13.178 7.609v-15.218zm5.485-12.175-6.589-3.804-13.178 7.609 13.178 7.609 13.179-7.609zm0 16.784-6.589-3.805-13.178 7.609 13.178 7.608 13.179-7.608-6.59-3.805z', ); ================================================ FILE: src/app/shell/icons/custom/TitanProportional.ts ================================================ import { makeCustomIcon } from './utils'; export const dimTitanProportionalIcon = makeCustomIcon( 'TitanProportional', 32, 22, 'm15.214 10.986-8.925-5.153v10.306zm1.572 0 8.925 5.153v-10.306zm8.109-5.629-8.856-5.193-8.896 5.17 8.896 5.136zm-.023 11.274-8.833-5.101-8.873 5.123 8.873 5.183z', ); export default dimTitanProportionalIcon; ================================================ FILE: src/app/shell/icons/custom/TunedStatIcon.ts ================================================ import { makeCustomIcon } from './utils'; export const tunedStatIcon = makeCustomIcon( 'TunedStat', 32, 32, 'M2,14.25 h28 v3.5 h-28zM2,10.5 l7,-7 l7,7 h-4.5 l-2.5,-2.5 l-2.5,2.5 zM30,21.5 l-7,7 l-7,-7 h4.5 l2.5,2.5 l2.5,-2.5 z', ); ================================================ FILE: src/app/shell/icons/custom/Warlock.ts ================================================ import { makeCustomIcon } from './utils'; export const dimWarlockIcon = makeCustomIcon( 'Warlock', 32, 32, 'm5.442 23.986 7.255-11.65-2.71-4.322-9.987 15.972zm5.986 0 4.28-6.849-2.717-4.333-6.992 11.182zm7.83-11.611 7.316 11.611h5.426l-10.015-15.972zm-7.26 11.611h8.004l-4.008-6.392zm6.991-11.182-2.703 4.324 4.302 6.858h5.413zm-5.707-.459 2.71-4.331 2.71 4.331-2.703 4.326z', ); ================================================ FILE: src/app/shell/icons/custom/WarlockProportional.ts ================================================ import { makeCustomIcon } from './utils'; export const dimWarlockProportionalIcon = makeCustomIcon( 'WarlockProportional', 32, 22, 'm5.442 18.786 7.255-11.65-2.71-4.322-9.987 15.972zm5.986 0 4.28-6.849-2.717-4.333-6.992 11.182zm7.83-11.611 7.316 11.611h5.426l-10.015-15.972zm-7.26 11.611h8.004l-4.008-6.392zm6.991-11.182-2.703 4.324 4.302 6.858h5.413zm-5.707-.459 2.71-4.331 2.71 4.331-2.703 4.326z', ); export default dimWarlockProportionalIcon; ================================================ FILE: src/app/shell/icons/custom/utils.ts ================================================ import { IconDefinition, IconName, IconPrefix } from '@fortawesome/fontawesome-svg-core'; export const makeCustomIcon = ( name: string, width: number, height: number, pathData: string, ): IconDefinition => ({ iconName: `dim${name}` as unknown as IconName, prefix: 'dim' as IconPrefix, icon: [width, height, [], '', pathData], }); ================================================ FILE: src/app/shell/icons/font-awesome-icon-variables.scss ================================================ $fa-var-500px: \f26e; $fa-var-accessible-icon: \f368; $fa-var-accusoft: \f369; $fa-var-acquisitions-incorporated: \f6af; $fa-var-ad: \f641; $fa-var-address-book: \f2b9; $fa-var-address-card: \f2bb; $fa-var-adjust: \f042; $fa-var-adn: \f170; $fa-var-adversal: \f36a; $fa-var-affiliatetheme: \f36b; $fa-var-air-freshener: \f5d0; $fa-var-airbnb: \f834; $fa-var-algolia: \f36c; $fa-var-align-center: \f037; $fa-var-align-justify: \f039; $fa-var-align-left: \f036; $fa-var-align-right: \f038; $fa-var-alipay: \f642; $fa-var-allergies: \f461; $fa-var-amazon: \f270; $fa-var-amazon-pay: \f42c; $fa-var-ambulance: \f0f9; $fa-var-american-sign-language-interpreting: \f2a3; $fa-var-amilia: \f36d; $fa-var-anchor: \f13d; $fa-var-android: \f17b; $fa-var-angellist: \f209; $fa-var-angle-double-down: \f103; $fa-var-angle-double-left: \f100; $fa-var-angle-double-right: \f101; $fa-var-angle-double-up: \f102; $fa-var-angle-down: \f107; $fa-var-angle-left: \f104; $fa-var-angle-right: \f105; $fa-var-angle-up: \f106; $fa-var-angry: \f556; $fa-var-angrycreative: \f36e; $fa-var-angular: \f420; $fa-var-ankh: \f644; $fa-var-app-store: \f36f; $fa-var-app-store-ios: \f370; $fa-var-apper: \f371; $fa-var-apple: \f179; $fa-var-apple-alt: \f5d1; $fa-var-apple-pay: \f415; $fa-var-archive: \f187; $fa-var-archway: \f557; $fa-var-arrow-alt-circle-down: \f358; $fa-var-arrow-alt-circle-left: \f359; $fa-var-arrow-alt-circle-right: \f35a; $fa-var-arrow-alt-circle-up: \f35b; $fa-var-arrow-circle-down: \f0ab; $fa-var-arrow-circle-left: \f0a8; $fa-var-arrow-circle-right: \f0a9; $fa-var-arrow-circle-up: \f0aa; $fa-var-arrow-down: \f063; $fa-var-arrow-left: \f060; $fa-var-arrow-right: \f061; $fa-var-arrow-up: \f062; $fa-var-arrows-alt: \f0b2; $fa-var-arrows-alt-h: \f337; $fa-var-arrows-alt-v: \f338; $fa-var-artstation: \f77a; $fa-var-assistive-listening-systems: \f2a2; $fa-var-asterisk: \f069; $fa-var-asymmetrik: \f372; $fa-var-at: \f1fa; $fa-var-atlas: \f558; $fa-var-atlassian: \f77b; $fa-var-atom: \f5d2; $fa-var-audible: \f373; $fa-var-audio-description: \f29e; $fa-var-autoprefixer: \f41c; $fa-var-avianex: \f374; $fa-var-aviato: \f421; $fa-var-award: \f559; $fa-var-aws: \f375; $fa-var-baby: \f77c; $fa-var-baby-carriage: \f77d; $fa-var-backspace: \f55a; $fa-var-backward: \f04a; $fa-var-bacon: \f7e5; $fa-var-bacteria: \e059; $fa-var-bacterium: \e05a; $fa-var-bahai: \f666; $fa-var-balance-scale: \f24e; $fa-var-balance-scale-left: \f515; $fa-var-balance-scale-right: \f516; $fa-var-ban: \f05e; $fa-var-band-aid: \f462; $fa-var-bandcamp: \f2d5; $fa-var-barcode: \f02a; $fa-var-bars: \f0c9; $fa-var-baseball-ball: \f433; $fa-var-basketball-ball: \f434; $fa-var-bath: \f2cd; $fa-var-battery-empty: \f244; $fa-var-battery-full: \f240; $fa-var-battery-half: \f242; $fa-var-battery-quarter: \f243; $fa-var-battery-three-quarters: \f241; $fa-var-battle-net: \f835; $fa-var-bed: \f236; $fa-var-beer: \f0fc; $fa-var-behance: \f1b4; $fa-var-behance-square: \f1b5; $fa-var-bell: \f0f3; $fa-var-bell-slash: \f1f6; $fa-var-bezier-curve: \f55b; $fa-var-bible: \f647; $fa-var-bicycle: \f206; $fa-var-biking: \f84a; $fa-var-bimobject: \f378; $fa-var-binoculars: \f1e5; $fa-var-biohazard: \f780; $fa-var-birthday-cake: \f1fd; $fa-var-bitbucket: \f171; $fa-var-bitcoin: \f379; $fa-var-bity: \f37a; $fa-var-black-tie: \f27e; $fa-var-blackberry: \f37b; $fa-var-blender: \f517; $fa-var-blender-phone: \f6b6; $fa-var-blind: \f29d; $fa-var-blog: \f781; $fa-var-blogger: \f37c; $fa-var-blogger-b: \f37d; $fa-var-bluetooth: \f293; $fa-var-bluetooth-b: \f294; $fa-var-bold: \f032; $fa-var-bolt: \f0e7; $fa-var-bomb: \f1e2; $fa-var-bone: \f5d7; $fa-var-bong: \f55c; $fa-var-book: \f02d; $fa-var-book-dead: \f6b7; $fa-var-book-medical: \f7e6; $fa-var-book-open: \f518; $fa-var-book-reader: \f5da; $fa-var-bookmark: \f02e; $fa-var-bootstrap: \f836; $fa-var-border-all: \f84c; $fa-var-border-none: \f850; $fa-var-border-style: \f853; $fa-var-bowling-ball: \f436; $fa-var-box: \f466; $fa-var-box-open: \f49e; $fa-var-box-tissue: \e05b; $fa-var-boxes: \f468; $fa-var-braille: \f2a1; $fa-var-brain: \f5dc; $fa-var-bread-slice: \f7ec; $fa-var-briefcase: \f0b1; $fa-var-briefcase-medical: \f469; $fa-var-broadcast-tower: \f519; $fa-var-broom: \f51a; $fa-var-brush: \f55d; $fa-var-btc: \f15a; $fa-var-buffer: \f837; $fa-var-bug: \f188; $fa-var-building: \f1ad; $fa-var-bullhorn: \f0a1; $fa-var-bullseye: \f140; $fa-var-burn: \f46a; $fa-var-buromobelexperte: \f37f; $fa-var-bus: \f207; $fa-var-bus-alt: \f55e; $fa-var-business-time: \f64a; $fa-var-buy-n-large: \f8a6; $fa-var-buysellads: \f20d; $fa-var-calculator: \f1ec; $fa-var-calendar: \f133; $fa-var-calendar-alt: \f073; $fa-var-calendar-check: \f274; $fa-var-calendar-day: \f783; $fa-var-calendar-minus: \f272; $fa-var-calendar-plus: \f271; $fa-var-calendar-times: \f273; $fa-var-calendar-week: \f784; $fa-var-camera: \f030; $fa-var-camera-retro: \f083; $fa-var-campground: \f6bb; $fa-var-canadian-maple-leaf: \f785; $fa-var-candy-cane: \f786; $fa-var-cannabis: \f55f; $fa-var-capsules: \f46b; $fa-var-car: \f1b9; $fa-var-car-alt: \f5de; $fa-var-car-battery: \f5df; $fa-var-car-crash: \f5e1; $fa-var-car-side: \f5e4; $fa-var-caravan: \f8ff; $fa-var-caret-down: \f0d7; $fa-var-caret-left: \f0d9; $fa-var-caret-right: \f0da; $fa-var-caret-square-down: \f150; $fa-var-caret-square-left: \f191; $fa-var-caret-square-right: \f152; $fa-var-caret-square-up: \f151; $fa-var-caret-up: \f0d8; $fa-var-carrot: \f787; $fa-var-cart-arrow-down: \f218; $fa-var-cart-plus: \f217; $fa-var-cash-register: \f788; $fa-var-cat: \f6be; $fa-var-cc-amazon-pay: \f42d; $fa-var-cc-amex: \f1f3; $fa-var-cc-apple-pay: \f416; $fa-var-cc-diners-club: \f24c; $fa-var-cc-discover: \f1f2; $fa-var-cc-jcb: \f24b; $fa-var-cc-mastercard: \f1f1; $fa-var-cc-paypal: \f1f4; $fa-var-cc-stripe: \f1f5; $fa-var-cc-visa: \f1f0; $fa-var-centercode: \f380; $fa-var-centos: \f789; $fa-var-certificate: \f0a3; $fa-var-chair: \f6c0; $fa-var-chalkboard: \f51b; $fa-var-chalkboard-teacher: \f51c; $fa-var-charging-station: \f5e7; $fa-var-chart-area: \f1fe; $fa-var-chart-bar: \f080; $fa-var-chart-line: \f201; $fa-var-chart-pie: \f200; $fa-var-check: \f00c; $fa-var-check-circle: \f058; $fa-var-check-double: \f560; $fa-var-check-square: \f14a; $fa-var-cheese: \f7ef; $fa-var-chess: \f439; $fa-var-chess-bishop: \f43a; $fa-var-chess-board: \f43c; $fa-var-chess-king: \f43f; $fa-var-chess-knight: \f441; $fa-var-chess-pawn: \f443; $fa-var-chess-queen: \f445; $fa-var-chess-rook: \f447; $fa-var-chevron-circle-down: \f13a; $fa-var-chevron-circle-left: \f137; $fa-var-chevron-circle-right: \f138; $fa-var-chevron-circle-up: \f139; $fa-var-chevron-down: \f078; $fa-var-chevron-left: \f053; $fa-var-chevron-right: \f054; $fa-var-chevron-up: \f077; $fa-var-child: \f1ae; $fa-var-chrome: \f268; $fa-var-chromecast: \f838; $fa-var-church: \f51d; $fa-var-circle: \f111; $fa-var-circle-notch: \f1ce; $fa-var-city: \f64f; $fa-var-clinic-medical: \f7f2; $fa-var-clipboard: \f328; $fa-var-clipboard-check: \f46c; $fa-var-clipboard-list: \f46d; $fa-var-clock: \f017; $fa-var-clone: \f24d; $fa-var-closed-captioning: \f20a; $fa-var-cloud: \f0c2; $fa-var-cloud-download-alt: \f381; $fa-var-cloud-meatball: \f73b; $fa-var-cloud-moon: \f6c3; $fa-var-cloud-moon-rain: \f73c; $fa-var-cloud-rain: \f73d; $fa-var-cloud-showers-heavy: \f740; $fa-var-cloud-sun: \f6c4; $fa-var-cloud-sun-rain: \f743; $fa-var-cloud-upload-alt: \f382; $fa-var-cloudflare: \e07d; $fa-var-cloudscale: \f383; $fa-var-cloudsmith: \f384; $fa-var-cloudversify: \f385; $fa-var-cocktail: \f561; $fa-var-code: \f121; $fa-var-code-branch: \f126; $fa-var-codepen: \f1cb; $fa-var-codiepie: \f284; $fa-var-coffee: \f0f4; $fa-var-cog: \f013; $fa-var-cogs: \f085; $fa-var-coins: \f51e; $fa-var-columns: \f0db; $fa-var-comment: \f075; $fa-var-comment-alt: \f27a; $fa-var-comment-dollar: \f651; $fa-var-comment-dots: \f4ad; $fa-var-comment-medical: \f7f5; $fa-var-comment-slash: \f4b3; $fa-var-comments: \f086; $fa-var-comments-dollar: \f653; $fa-var-compact-disc: \f51f; $fa-var-compass: \f14e; $fa-var-compress: \f066; $fa-var-compress-alt: \f422; $fa-var-compress-arrows-alt: \f78c; $fa-var-concierge-bell: \f562; $fa-var-confluence: \f78d; $fa-var-connectdevelop: \f20e; $fa-var-contao: \f26d; $fa-var-cookie: \f563; $fa-var-cookie-bite: \f564; $fa-var-copy: \f0c5; $fa-var-copyright: \f1f9; $fa-var-cotton-bureau: \f89e; $fa-var-couch: \f4b8; $fa-var-cpanel: \f388; $fa-var-creative-commons: \f25e; $fa-var-creative-commons-by: \f4e7; $fa-var-creative-commons-nc: \f4e8; $fa-var-creative-commons-nc-eu: \f4e9; $fa-var-creative-commons-nc-jp: \f4ea; $fa-var-creative-commons-nd: \f4eb; $fa-var-creative-commons-pd: \f4ec; $fa-var-creative-commons-pd-alt: \f4ed; $fa-var-creative-commons-remix: \f4ee; $fa-var-creative-commons-sa: \f4ef; $fa-var-creative-commons-sampling: \f4f0; $fa-var-creative-commons-sampling-plus: \f4f1; $fa-var-creative-commons-share: \f4f2; $fa-var-creative-commons-zero: \f4f3; $fa-var-credit-card: \f09d; $fa-var-critical-role: \f6c9; $fa-var-crop: \f125; $fa-var-crop-alt: \f565; $fa-var-cross: \f654; $fa-var-crosshairs: \f05b; $fa-var-crow: \f520; $fa-var-crown: \f521; $fa-var-crutch: \f7f7; $fa-var-css3: \f13c; $fa-var-css3-alt: \f38b; $fa-var-cube: \f1b2; $fa-var-cubes: \f1b3; $fa-var-cut: \f0c4; $fa-var-cuttlefish: \f38c; $fa-var-d-and-d: \f38d; $fa-var-d-and-d-beyond: \f6ca; $fa-var-dailymotion: \e052; $fa-var-dashcube: \f210; $fa-var-database: \f1c0; $fa-var-deaf: \f2a4; $fa-var-deezer: \e077; $fa-var-delicious: \f1a5; $fa-var-democrat: \f747; $fa-var-deploydog: \f38e; $fa-var-deskpro: \f38f; $fa-var-desktop: \f108; $fa-var-dev: \f6cc; $fa-var-deviantart: \f1bd; $fa-var-dharmachakra: \f655; $fa-var-dhl: \f790; $fa-var-diagnoses: \f470; $fa-var-diaspora: \f791; $fa-var-dice: \f522; $fa-var-dice-d20: \f6cf; $fa-var-dice-d6: \f6d1; $fa-var-dice-five: \f523; $fa-var-dice-four: \f524; $fa-var-dice-one: \f525; $fa-var-dice-six: \f526; $fa-var-dice-three: \f527; $fa-var-dice-two: \f528; $fa-var-digg: \f1a6; $fa-var-digital-ocean: \f391; $fa-var-digital-tachograph: \f566; $fa-var-directions: \f5eb; $fa-var-discord: \f392; $fa-var-discourse: \f393; $fa-var-disease: \f7fa; $fa-var-divide: \f529; $fa-var-dizzy: \f567; $fa-var-dna: \f471; $fa-var-dochub: \f394; $fa-var-docker: \f395; $fa-var-dog: \f6d3; $fa-var-dollar-sign: \f155; $fa-var-dolly: \f472; $fa-var-dolly-flatbed: \f474; $fa-var-donate: \f4b9; $fa-var-door-closed: \f52a; $fa-var-door-open: \f52b; $fa-var-dot-circle: \f192; $fa-var-dove: \f4ba; $fa-var-download: \f019; $fa-var-draft2digital: \f396; $fa-var-drafting-compass: \f568; $fa-var-dragon: \f6d5; $fa-var-draw-polygon: \f5ee; $fa-var-dribbble: \f17d; $fa-var-dribbble-square: \f397; $fa-var-dropbox: \f16b; $fa-var-drum: \f569; $fa-var-drum-steelpan: \f56a; $fa-var-drumstick-bite: \f6d7; $fa-var-drupal: \f1a9; $fa-var-dumbbell: \f44b; $fa-var-dumpster: \f793; $fa-var-dumpster-fire: \f794; $fa-var-dungeon: \f6d9; $fa-var-dyalog: \f399; $fa-var-earlybirds: \f39a; $fa-var-ebay: \f4f4; $fa-var-edge: \f282; $fa-var-edge-legacy: \e078; $fa-var-edit: \f044; $fa-var-egg: \f7fb; $fa-var-eject: \f052; $fa-var-elementor: \f430; $fa-var-ellipsis-h: \f141; $fa-var-ellipsis-v: \f142; $fa-var-ello: \f5f1; $fa-var-ember: \f423; $fa-var-empire: \f1d1; $fa-var-envelope: \f0e0; $fa-var-envelope-open: \f2b6; $fa-var-envelope-open-text: \f658; $fa-var-envelope-square: \f199; $fa-var-envira: \f299; $fa-var-equals: \f52c; $fa-var-eraser: \f12d; $fa-var-erlang: \f39d; $fa-var-ethereum: \f42e; $fa-var-ethernet: \f796; $fa-var-etsy: \f2d7; $fa-var-euro-sign: \f153; $fa-var-evernote: \f839; $fa-var-exchange-alt: \f362; $fa-var-exclamation: \f12a; $fa-var-exclamation-circle: \f06a; $fa-var-exclamation-triangle: \f071; $fa-var-expand: \f065; $fa-var-expand-alt: \f424; $fa-var-expand-arrows-alt: \f31e; $fa-var-expeditedssl: \f23e; $fa-var-external-link-alt: \f35d; $fa-var-external-link-square-alt: \f360; $fa-var-eye: \f06e; $fa-var-eye-dropper: \f1fb; $fa-var-eye-slash: \f070; $fa-var-facebook: \f09a; $fa-var-facebook-f: \f39e; $fa-var-facebook-messenger: \f39f; $fa-var-facebook-square: \f082; $fa-var-fan: \f863; $fa-var-fantasy-flight-games: \f6dc; $fa-var-fast-backward: \f049; $fa-var-fast-forward: \f050; $fa-var-faucet: \e005; $fa-var-fax: \f1ac; $fa-var-feather: \f52d; $fa-var-feather-alt: \f56b; $fa-var-fedex: \f797; $fa-var-fedora: \f798; $fa-var-female: \f182; $fa-var-fighter-jet: \f0fb; $fa-var-figma: \f799; $fa-var-file: \f15b; $fa-var-file-alt: \f15c; $fa-var-file-archive: \f1c6; $fa-var-file-audio: \f1c7; $fa-var-file-code: \f1c9; $fa-var-file-contract: \f56c; $fa-var-file-csv: \f6dd; $fa-var-file-download: \f56d; $fa-var-file-excel: \f1c3; $fa-var-file-export: \f56e; $fa-var-file-image: \f1c5; $fa-var-file-import: \f56f; $fa-var-file-invoice: \f570; $fa-var-file-invoice-dollar: \f571; $fa-var-file-medical: \f477; $fa-var-file-medical-alt: \f478; $fa-var-file-pdf: \f1c1; $fa-var-file-powerpoint: \f1c4; $fa-var-file-prescription: \f572; $fa-var-file-signature: \f573; $fa-var-file-upload: \f574; $fa-var-file-video: \f1c8; $fa-var-file-word: \f1c2; $fa-var-fill: \f575; $fa-var-fill-drip: \f576; $fa-var-film: \f008; $fa-var-filter: \f0b0; $fa-var-fingerprint: \f577; $fa-var-fire: \f06d; $fa-var-fire-alt: \f7e4; $fa-var-fire-extinguisher: \f134; $fa-var-firefox: \f269; $fa-var-firefox-browser: \e007; $fa-var-first-aid: \f479; $fa-var-first-order: \f2b0; $fa-var-first-order-alt: \f50a; $fa-var-firstdraft: \f3a1; $fa-var-fish: \f578; $fa-var-fist-raised: \f6de; $fa-var-flag: \f024; $fa-var-flag-checkered: \f11e; $fa-var-flag-usa: \f74d; $fa-var-flask: \f0c3; $fa-var-flickr: \f16e; $fa-var-flipboard: \f44d; $fa-var-flushed: \f579; $fa-var-fly: \f417; $fa-var-folder: \f07b; $fa-var-folder-minus: \f65d; $fa-var-folder-open: \f07c; $fa-var-folder-plus: \f65e; $fa-var-font: \f031; $fa-var-font-awesome: \f2b4; $fa-var-font-awesome-alt: \f35c; $fa-var-font-awesome-flag: \f425; $fa-var-font-awesome-logo-full: \f4e6; $fa-var-fonticons: \f280; $fa-var-fonticons-fi: \f3a2; $fa-var-football-ball: \f44e; $fa-var-fort-awesome: \f286; $fa-var-fort-awesome-alt: \f3a3; $fa-var-forumbee: \f211; $fa-var-forward: \f04e; $fa-var-foursquare: \f180; $fa-var-free-code-camp: \f2c5; $fa-var-freebsd: \f3a4; $fa-var-frog: \f52e; $fa-var-frown: \f119; $fa-var-frown-open: \f57a; $fa-var-fulcrum: \f50b; $fa-var-funnel-dollar: \f662; $fa-var-futbol: \f1e3; $fa-var-galactic-republic: \f50c; $fa-var-galactic-senate: \f50d; $fa-var-gamepad: \f11b; $fa-var-gas-pump: \f52f; $fa-var-gavel: \f0e3; $fa-var-gem: \f3a5; $fa-var-genderless: \f22d; $fa-var-get-pocket: \f265; $fa-var-gg: \f260; $fa-var-gg-circle: \f261; $fa-var-ghost: \f6e2; $fa-var-gift: \f06b; $fa-var-gifts: \f79c; $fa-var-git: \f1d3; $fa-var-git-alt: \f841; $fa-var-git-square: \f1d2; $fa-var-github: \f09b; $fa-var-github-alt: \f113; $fa-var-github-square: \f092; $fa-var-gitkraken: \f3a6; $fa-var-gitlab: \f296; $fa-var-gitter: \f426; $fa-var-glass-cheers: \f79f; $fa-var-glass-martini: \f000; $fa-var-glass-martini-alt: \f57b; $fa-var-glass-whiskey: \f7a0; $fa-var-glasses: \f530; $fa-var-glide: \f2a5; $fa-var-glide-g: \f2a6; $fa-var-globe: \f0ac; $fa-var-globe-africa: \f57c; $fa-var-globe-americas: \f57d; $fa-var-globe-asia: \f57e; $fa-var-globe-europe: \f7a2; $fa-var-gofore: \f3a7; $fa-var-golf-ball: \f450; $fa-var-goodreads: \f3a8; $fa-var-goodreads-g: \f3a9; $fa-var-google: \f1a0; $fa-var-google-drive: \f3aa; $fa-var-google-pay: \e079; $fa-var-google-play: \f3ab; $fa-var-google-plus: \f2b3; $fa-var-google-plus-g: \f0d5; $fa-var-google-plus-square: \f0d4; $fa-var-google-wallet: \f1ee; $fa-var-gopuram: \f664; $fa-var-graduation-cap: \f19d; $fa-var-gratipay: \f184; $fa-var-grav: \f2d6; $fa-var-greater-than: \f531; $fa-var-greater-than-equal: \f532; $fa-var-grimace: \f57f; $fa-var-grin: \f580; $fa-var-grin-alt: \f581; $fa-var-grin-beam: \f582; $fa-var-grin-beam-sweat: \f583; $fa-var-grin-hearts: \f584; $fa-var-grin-squint: \f585; $fa-var-grin-squint-tears: \f586; $fa-var-grin-stars: \f587; $fa-var-grin-tears: \f588; $fa-var-grin-tongue: \f589; $fa-var-grin-tongue-squint: \f58a; $fa-var-grin-tongue-wink: \f58b; $fa-var-grin-wink: \f58c; $fa-var-grip-horizontal: \f58d; $fa-var-grip-lines: \f7a4; $fa-var-grip-lines-vertical: \f7a5; $fa-var-grip-vertical: \f58e; $fa-var-gripfire: \f3ac; $fa-var-grunt: \f3ad; $fa-var-guilded: \e07e; $fa-var-guitar: \f7a6; $fa-var-gulp: \f3ae; $fa-var-h-square: \f0fd; $fa-var-hacker-news: \f1d4; $fa-var-hacker-news-square: \f3af; $fa-var-hackerrank: \f5f7; $fa-var-hamburger: \f805; $fa-var-hammer: \f6e3; $fa-var-hamsa: \f665; $fa-var-hand-holding: \f4bd; $fa-var-hand-holding-heart: \f4be; $fa-var-hand-holding-medical: \e05c; $fa-var-hand-holding-usd: \f4c0; $fa-var-hand-holding-water: \f4c1; $fa-var-hand-lizard: \f258; $fa-var-hand-middle-finger: \f806; $fa-var-hand-paper: \f256; $fa-var-hand-peace: \f25b; $fa-var-hand-point-down: \f0a7; $fa-var-hand-point-left: \f0a5; $fa-var-hand-point-right: \f0a4; $fa-var-hand-point-up: \f0a6; $fa-var-hand-pointer: \f25a; $fa-var-hand-rock: \f255; $fa-var-hand-scissors: \f257; $fa-var-hand-sparkles: \e05d; $fa-var-hand-spock: \f259; $fa-var-hands: \f4c2; $fa-var-hands-helping: \f4c4; $fa-var-hands-wash: \e05e; $fa-var-handshake: \f2b5; $fa-var-handshake-alt-slash: \e05f; $fa-var-handshake-slash: \e060; $fa-var-hanukiah: \f6e6; $fa-var-hard-hat: \f807; $fa-var-hashtag: \f292; $fa-var-hat-cowboy: \f8c0; $fa-var-hat-cowboy-side: \f8c1; $fa-var-hat-wizard: \f6e8; $fa-var-hdd: \f0a0; $fa-var-head-side-cough: \e061; $fa-var-head-side-cough-slash: \e062; $fa-var-head-side-mask: \e063; $fa-var-head-side-virus: \e064; $fa-var-heading: \f1dc; $fa-var-headphones: \f025; $fa-var-headphones-alt: \f58f; $fa-var-headset: \f590; $fa-var-heart: \f004; $fa-var-heart-broken: \f7a9; $fa-var-heartbeat: \f21e; $fa-var-helicopter: \f533; $fa-var-highlighter: \f591; $fa-var-hiking: \f6ec; $fa-var-hippo: \f6ed; $fa-var-hips: \f452; $fa-var-hire-a-helper: \f3b0; $fa-var-history: \f1da; $fa-var-hive: \e07f; $fa-var-hockey-puck: \f453; $fa-var-holly-berry: \f7aa; $fa-var-home: \f015; $fa-var-hooli: \f427; $fa-var-hornbill: \f592; $fa-var-horse: \f6f0; $fa-var-horse-head: \f7ab; $fa-var-hospital: \f0f8; $fa-var-hospital-alt: \f47d; $fa-var-hospital-symbol: \f47e; $fa-var-hospital-user: \f80d; $fa-var-hot-tub: \f593; $fa-var-hotdog: \f80f; $fa-var-hotel: \f594; $fa-var-hotjar: \f3b1; $fa-var-hourglass: \f254; $fa-var-hourglass-end: \f253; $fa-var-hourglass-half: \f252; $fa-var-hourglass-start: \f251; $fa-var-house-damage: \f6f1; $fa-var-house-user: \e065; $fa-var-houzz: \f27c; $fa-var-hryvnia: \f6f2; $fa-var-html5: \f13b; $fa-var-hubspot: \f3b2; $fa-var-i-cursor: \f246; $fa-var-ice-cream: \f810; $fa-var-icicles: \f7ad; $fa-var-icons: \f86d; $fa-var-id-badge: \f2c1; $fa-var-id-card: \f2c2; $fa-var-id-card-alt: \f47f; $fa-var-ideal: \e013; $fa-var-igloo: \f7ae; $fa-var-image: \f03e; $fa-var-images: \f302; $fa-var-imdb: \f2d8; $fa-var-inbox: \f01c; $fa-var-indent: \f03c; $fa-var-industry: \f275; $fa-var-infinity: \f534; $fa-var-info: \f129; $fa-var-info-circle: \f05a; $fa-var-innosoft: \e080; $fa-var-instagram: \f16d; $fa-var-instagram-square: \e055; $fa-var-instalod: \e081; $fa-var-intercom: \f7af; $fa-var-internet-explorer: \f26b; $fa-var-invision: \f7b0; $fa-var-ioxhost: \f208; $fa-var-italic: \f033; $fa-var-itch-io: \f83a; $fa-var-itunes: \f3b4; $fa-var-itunes-note: \f3b5; $fa-var-java: \f4e4; $fa-var-jedi: \f669; $fa-var-jedi-order: \f50e; $fa-var-jenkins: \f3b6; $fa-var-jira: \f7b1; $fa-var-joget: \f3b7; $fa-var-joint: \f595; $fa-var-joomla: \f1aa; $fa-var-journal-whills: \f66a; $fa-var-js: \f3b8; $fa-var-js-square: \f3b9; $fa-var-jsfiddle: \f1cc; $fa-var-kaaba: \f66b; $fa-var-kaggle: \f5fa; $fa-var-key: \f084; $fa-var-keybase: \f4f5; $fa-var-keyboard: \f11c; $fa-var-keycdn: \f3ba; $fa-var-khanda: \f66d; $fa-var-kickstarter: \f3bb; $fa-var-kickstarter-k: \f3bc; $fa-var-kiss: \f596; $fa-var-kiss-beam: \f597; $fa-var-kiss-wink-heart: \f598; $fa-var-kiwi-bird: \f535; $fa-var-korvue: \f42f; $fa-var-landmark: \f66f; $fa-var-language: \f1ab; $fa-var-laptop: \f109; $fa-var-laptop-code: \f5fc; $fa-var-laptop-house: \e066; $fa-var-laptop-medical: \f812; $fa-var-laravel: \f3bd; $fa-var-lastfm: \f202; $fa-var-lastfm-square: \f203; $fa-var-laugh: \f599; $fa-var-laugh-beam: \f59a; $fa-var-laugh-squint: \f59b; $fa-var-laugh-wink: \f59c; $fa-var-layer-group: \f5fd; $fa-var-leaf: \f06c; $fa-var-leanpub: \f212; $fa-var-lemon: \f094; $fa-var-less: \f41d; $fa-var-less-than: \f536; $fa-var-less-than-equal: \f537; $fa-var-level-down-alt: \f3be; $fa-var-level-up-alt: \f3bf; $fa-var-life-ring: \f1cd; $fa-var-lightbulb: \f0eb; $fa-var-line: \f3c0; $fa-var-link: \f0c1; $fa-var-linkedin: \f08c; $fa-var-linkedin-in: \f0e1; $fa-var-linode: \f2b8; $fa-var-linux: \f17c; $fa-var-lira-sign: \f195; $fa-var-list: \f03a; $fa-var-list-alt: \f022; $fa-var-list-ol: \f0cb; $fa-var-list-ul: \f0ca; $fa-var-location-arrow: \f124; $fa-var-lock: \f023; $fa-var-lock-open: \f3c1; $fa-var-long-arrow-alt-down: \f309; $fa-var-long-arrow-alt-left: \f30a; $fa-var-long-arrow-alt-right: \f30b; $fa-var-long-arrow-alt-up: \f30c; $fa-var-low-vision: \f2a8; $fa-var-luggage-cart: \f59d; $fa-var-lungs: \f604; $fa-var-lungs-virus: \e067; $fa-var-lyft: \f3c3; $fa-var-magento: \f3c4; $fa-var-magic: \f0d0; $fa-var-magnet: \f076; $fa-var-mail-bulk: \f674; $fa-var-mailchimp: \f59e; $fa-var-male: \f183; $fa-var-mandalorian: \f50f; $fa-var-map: \f279; $fa-var-map-marked: \f59f; $fa-var-map-marked-alt: \f5a0; $fa-var-map-marker: \f041; $fa-var-map-marker-alt: \f3c5; $fa-var-map-pin: \f276; $fa-var-map-signs: \f277; $fa-var-markdown: \f60f; $fa-var-marker: \f5a1; $fa-var-mars: \f222; $fa-var-mars-double: \f227; $fa-var-mars-stroke: \f229; $fa-var-mars-stroke-h: \f22b; $fa-var-mars-stroke-v: \f22a; $fa-var-mask: \f6fa; $fa-var-mastodon: \f4f6; $fa-var-maxcdn: \f136; $fa-var-mdb: \f8ca; $fa-var-medal: \f5a2; $fa-var-medapps: \f3c6; $fa-var-medium: \f23a; $fa-var-medium-m: \f3c7; $fa-var-medkit: \f0fa; $fa-var-medrt: \f3c8; $fa-var-meetup: \f2e0; $fa-var-megaport: \f5a3; $fa-var-meh: \f11a; $fa-var-meh-blank: \f5a4; $fa-var-meh-rolling-eyes: \f5a5; $fa-var-memory: \f538; $fa-var-mendeley: \f7b3; $fa-var-menorah: \f676; $fa-var-mercury: \f223; $fa-var-meteor: \f753; $fa-var-microblog: \e01a; $fa-var-microchip: \f2db; $fa-var-microphone: \f130; $fa-var-microphone-alt: \f3c9; $fa-var-microphone-alt-slash: \f539; $fa-var-microphone-slash: \f131; $fa-var-microscope: \f610; $fa-var-microsoft: \f3ca; $fa-var-minus: \f068; $fa-var-minus-circle: \f056; $fa-var-minus-square: \f146; $fa-var-mitten: \f7b5; $fa-var-mix: \f3cb; $fa-var-mixcloud: \f289; $fa-var-mixer: \e056; $fa-var-mizuni: \f3cc; $fa-var-mobile: \f10b; $fa-var-mobile-alt: \f3cd; $fa-var-modx: \f285; $fa-var-monero: \f3d0; $fa-var-money-bill: \f0d6; $fa-var-money-bill-alt: \f3d1; $fa-var-money-bill-wave: \f53a; $fa-var-money-bill-wave-alt: \f53b; $fa-var-money-check: \f53c; $fa-var-money-check-alt: \f53d; $fa-var-monument: \f5a6; $fa-var-moon: \f186; $fa-var-mortar-pestle: \f5a7; $fa-var-mosque: \f678; $fa-var-motorcycle: \f21c; $fa-var-mountain: \f6fc; $fa-var-mouse: \f8cc; $fa-var-mouse-pointer: \f245; $fa-var-mug-hot: \f7b6; $fa-var-music: \f001; $fa-var-napster: \f3d2; $fa-var-neos: \f612; $fa-var-network-wired: \f6ff; $fa-var-neuter: \f22c; $fa-var-newspaper: \f1ea; $fa-var-nimblr: \f5a8; $fa-var-node: \f419; $fa-var-node-js: \f3d3; $fa-var-not-equal: \f53e; $fa-var-notes-medical: \f481; $fa-var-npm: \f3d4; $fa-var-ns8: \f3d5; $fa-var-nutritionix: \f3d6; $fa-var-object-group: \f247; $fa-var-object-ungroup: \f248; $fa-var-octopus-deploy: \e082; $fa-var-odnoklassniki: \f263; $fa-var-odnoklassniki-square: \f264; $fa-var-oil-can: \f613; $fa-var-old-republic: \f510; $fa-var-om: \f679; $fa-var-opencart: \f23d; $fa-var-openid: \f19b; $fa-var-opera: \f26a; $fa-var-optin-monster: \f23c; $fa-var-orcid: \f8d2; $fa-var-osi: \f41a; $fa-var-otter: \f700; $fa-var-outdent: \f03b; $fa-var-page4: \f3d7; $fa-var-pagelines: \f18c; $fa-var-pager: \f815; $fa-var-paint-brush: \f1fc; $fa-var-paint-roller: \f5aa; $fa-var-palette: \f53f; $fa-var-palfed: \f3d8; $fa-var-pallet: \f482; $fa-var-paper-plane: \f1d8; $fa-var-paperclip: \f0c6; $fa-var-parachute-box: \f4cd; $fa-var-paragraph: \f1dd; $fa-var-parking: \f540; $fa-var-passport: \f5ab; $fa-var-pastafarianism: \f67b; $fa-var-paste: \f0ea; $fa-var-patreon: \f3d9; $fa-var-pause: \f04c; $fa-var-pause-circle: \f28b; $fa-var-paw: \f1b0; $fa-var-paypal: \f1ed; $fa-var-peace: \f67c; $fa-var-pen: \f304; $fa-var-pen-alt: \f305; $fa-var-pen-fancy: \f5ac; $fa-var-pen-nib: \f5ad; $fa-var-pen-square: \f14b; $fa-var-pencil-alt: \f303; $fa-var-pencil-ruler: \f5ae; $fa-var-penny-arcade: \f704; $fa-var-people-arrows: \e068; $fa-var-people-carry: \f4ce; $fa-var-pepper-hot: \f816; $fa-var-perbyte: \e083; $fa-var-percent: \f295; $fa-var-percentage: \f541; $fa-var-periscope: \f3da; $fa-var-person-booth: \f756; $fa-var-phabricator: \f3db; $fa-var-phoenix-framework: \f3dc; $fa-var-phoenix-squadron: \f511; $fa-var-phone: \f095; $fa-var-phone-alt: \f879; $fa-var-phone-slash: \f3dd; $fa-var-phone-square: \f098; $fa-var-phone-square-alt: \f87b; $fa-var-phone-volume: \f2a0; $fa-var-photo-video: \f87c; $fa-var-php: \f457; $fa-var-pied-piper: \f2ae; $fa-var-pied-piper-alt: \f1a8; $fa-var-pied-piper-hat: \f4e5; $fa-var-pied-piper-pp: \f1a7; $fa-var-pied-piper-square: \e01e; $fa-var-piggy-bank: \f4d3; $fa-var-pills: \f484; $fa-var-pinterest: \f0d2; $fa-var-pinterest-p: \f231; $fa-var-pinterest-square: \f0d3; $fa-var-pizza-slice: \f818; $fa-var-place-of-worship: \f67f; $fa-var-plane: \f072; $fa-var-plane-arrival: \f5af; $fa-var-plane-departure: \f5b0; $fa-var-plane-slash: \e069; $fa-var-play: \f04b; $fa-var-play-circle: \f144; $fa-var-playstation: \f3df; $fa-var-plug: \f1e6; $fa-var-plus: \f067; $fa-var-plus-circle: \f055; $fa-var-plus-square: \f0fe; $fa-var-podcast: \f2ce; $fa-var-poll: \f681; $fa-var-poll-h: \f682; $fa-var-poo: \f2fe; $fa-var-poo-storm: \f75a; $fa-var-poop: \f619; $fa-var-portrait: \f3e0; $fa-var-pound-sign: \f154; $fa-var-power-off: \f011; $fa-var-pray: \f683; $fa-var-praying-hands: \f684; $fa-var-prescription: \f5b1; $fa-var-prescription-bottle: \f485; $fa-var-prescription-bottle-alt: \f486; $fa-var-print: \f02f; $fa-var-procedures: \f487; $fa-var-product-hunt: \f288; $fa-var-project-diagram: \f542; $fa-var-pump-medical: \e06a; $fa-var-pump-soap: \e06b; $fa-var-pushed: \f3e1; $fa-var-puzzle-piece: \f12e; $fa-var-python: \f3e2; $fa-var-qq: \f1d6; $fa-var-qrcode: \f029; $fa-var-question: \f128; $fa-var-question-circle: \f059; $fa-var-quidditch: \f458; $fa-var-quinscape: \f459; $fa-var-quora: \f2c4; $fa-var-quote-left: \f10d; $fa-var-quote-right: \f10e; $fa-var-quran: \f687; $fa-var-r-project: \f4f7; $fa-var-radiation: \f7b9; $fa-var-radiation-alt: \f7ba; $fa-var-rainbow: \f75b; $fa-var-random: \f074; $fa-var-raspberry-pi: \f7bb; $fa-var-ravelry: \f2d9; $fa-var-react: \f41b; $fa-var-reacteurope: \f75d; $fa-var-readme: \f4d5; $fa-var-rebel: \f1d0; $fa-var-receipt: \f543; $fa-var-record-vinyl: \f8d9; $fa-var-recycle: \f1b8; $fa-var-red-river: \f3e3; $fa-var-reddit: \f1a1; $fa-var-reddit-alien: \f281; $fa-var-reddit-square: \f1a2; $fa-var-redhat: \f7bc; $fa-var-redo: \f01e; $fa-var-redo-alt: \f2f9; $fa-var-registered: \f25d; $fa-var-remove-format: \f87d; $fa-var-renren: \f18b; $fa-var-reply: \f3e5; $fa-var-reply-all: \f122; $fa-var-replyd: \f3e6; $fa-var-republican: \f75e; $fa-var-researchgate: \f4f8; $fa-var-resolving: \f3e7; $fa-var-restroom: \f7bd; $fa-var-retweet: \f079; $fa-var-rev: \f5b2; $fa-var-ribbon: \f4d6; $fa-var-ring: \f70b; $fa-var-road: \f018; $fa-var-robot: \f544; $fa-var-rocket: \f135; $fa-var-rocketchat: \f3e8; $fa-var-rockrms: \f3e9; $fa-var-route: \f4d7; $fa-var-rss: \f09e; $fa-var-rss-square: \f143; $fa-var-ruble-sign: \f158; $fa-var-ruler: \f545; $fa-var-ruler-combined: \f546; $fa-var-ruler-horizontal: \f547; $fa-var-ruler-vertical: \f548; $fa-var-running: \f70c; $fa-var-rupee-sign: \f156; $fa-var-rust: \e07a; $fa-var-sad-cry: \f5b3; $fa-var-sad-tear: \f5b4; $fa-var-safari: \f267; $fa-var-salesforce: \f83b; $fa-var-sass: \f41e; $fa-var-satellite: \f7bf; $fa-var-satellite-dish: \f7c0; $fa-var-save: \f0c7; $fa-var-schlix: \f3ea; $fa-var-school: \f549; $fa-var-screwdriver: \f54a; $fa-var-scribd: \f28a; $fa-var-scroll: \f70e; $fa-var-sd-card: \f7c2; $fa-var-search: \f002; $fa-var-search-dollar: \f688; $fa-var-search-location: \f689; $fa-var-search-minus: \f010; $fa-var-search-plus: \f00e; $fa-var-searchengin: \f3eb; $fa-var-seedling: \f4d8; $fa-var-sellcast: \f2da; $fa-var-sellsy: \f213; $fa-var-server: \f233; $fa-var-servicestack: \f3ec; $fa-var-shapes: \f61f; $fa-var-share: \f064; $fa-var-share-alt: \f1e0; $fa-var-share-alt-square: \f1e1; $fa-var-share-square: \f14d; $fa-var-shekel-sign: \f20b; $fa-var-shield-alt: \f3ed; $fa-var-shield-virus: \e06c; $fa-var-ship: \f21a; $fa-var-shipping-fast: \f48b; $fa-var-shirtsinbulk: \f214; $fa-var-shoe-prints: \f54b; $fa-var-shopify: \e057; $fa-var-shopping-bag: \f290; $fa-var-shopping-basket: \f291; $fa-var-shopping-cart: \f07a; $fa-var-shopware: \f5b5; $fa-var-shower: \f2cc; $fa-var-shuttle-van: \f5b6; $fa-var-sign: \f4d9; $fa-var-sign-in-alt: \f2f6; $fa-var-sign-language: \f2a7; $fa-var-sign-out-alt: \f2f5; $fa-var-signal: \f012; $fa-var-signature: \f5b7; $fa-var-sim-card: \f7c4; $fa-var-simplybuilt: \f215; $fa-var-sink: \e06d; $fa-var-sistrix: \f3ee; $fa-var-sitemap: \f0e8; $fa-var-sith: \f512; $fa-var-skating: \f7c5; $fa-var-sketch: \f7c6; $fa-var-skiing: \f7c9; $fa-var-skiing-nordic: \f7ca; $fa-var-skull: \f54c; $fa-var-skull-crossbones: \f714; $fa-var-skyatlas: \f216; $fa-var-skype: \f17e; $fa-var-slack: \f198; $fa-var-slack-hash: \f3ef; $fa-var-slash: \f715; $fa-var-sleigh: \f7cc; $fa-var-sliders-h: \f1de; $fa-var-slideshare: \f1e7; $fa-var-smile: \f118; $fa-var-smile-beam: \f5b8; $fa-var-smile-wink: \f4da; $fa-var-smog: \f75f; $fa-var-smoking: \f48d; $fa-var-smoking-ban: \f54d; $fa-var-sms: \f7cd; $fa-var-snapchat: \f2ab; $fa-var-snapchat-ghost: \f2ac; $fa-var-snapchat-square: \f2ad; $fa-var-snowboarding: \f7ce; $fa-var-snowflake: \f2dc; $fa-var-snowman: \f7d0; $fa-var-snowplow: \f7d2; $fa-var-soap: \e06e; $fa-var-socks: \f696; $fa-var-solar-panel: \f5ba; $fa-var-sort: \f0dc; $fa-var-sort-alpha-down: \f15d; $fa-var-sort-alpha-down-alt: \f881; $fa-var-sort-alpha-up: \f15e; $fa-var-sort-alpha-up-alt: \f882; $fa-var-sort-amount-down: \f160; $fa-var-sort-amount-down-alt: \f884; $fa-var-sort-amount-up: \f161; $fa-var-sort-amount-up-alt: \f885; $fa-var-sort-down: \f0dd; $fa-var-sort-numeric-down: \f162; $fa-var-sort-numeric-down-alt: \f886; $fa-var-sort-numeric-up: \f163; $fa-var-sort-numeric-up-alt: \f887; $fa-var-sort-up: \f0de; $fa-var-soundcloud: \f1be; $fa-var-sourcetree: \f7d3; $fa-var-spa: \f5bb; $fa-var-space-shuttle: \f197; $fa-var-speakap: \f3f3; $fa-var-speaker-deck: \f83c; $fa-var-spell-check: \f891; $fa-var-spider: \f717; $fa-var-spinner: \f110; $fa-var-splotch: \f5bc; $fa-var-spotify: \f1bc; $fa-var-spray-can: \f5bd; $fa-var-square: \f0c8; $fa-var-square-full: \f45c; $fa-var-square-root-alt: \f698; $fa-var-squarespace: \f5be; $fa-var-stack-exchange: \f18d; $fa-var-stack-overflow: \f16c; $fa-var-stackpath: \f842; $fa-var-stamp: \f5bf; $fa-var-star: \f005; $fa-var-star-and-crescent: \f699; $fa-var-star-half: \f089; $fa-var-star-half-alt: \f5c0; $fa-var-star-of-david: \f69a; $fa-var-star-of-life: \f621; $fa-var-staylinked: \f3f5; $fa-var-steam: \f1b6; $fa-var-steam-square: \f1b7; $fa-var-steam-symbol: \f3f6; $fa-var-step-backward: \f048; $fa-var-step-forward: \f051; $fa-var-stethoscope: \f0f1; $fa-var-sticker-mule: \f3f7; $fa-var-sticky-note: \f249; $fa-var-stop: \f04d; $fa-var-stop-circle: \f28d; $fa-var-stopwatch: \f2f2; $fa-var-stopwatch-20: \e06f; $fa-var-store: \f54e; $fa-var-store-alt: \f54f; $fa-var-store-alt-slash: \e070; $fa-var-store-slash: \e071; $fa-var-strava: \f428; $fa-var-stream: \f550; $fa-var-street-view: \f21d; $fa-var-strikethrough: \f0cc; $fa-var-stripe: \f429; $fa-var-stripe-s: \f42a; $fa-var-stroopwafel: \f551; $fa-var-studiovinari: \f3f8; $fa-var-stumbleupon: \f1a4; $fa-var-stumbleupon-circle: \f1a3; $fa-var-subscript: \f12c; $fa-var-subway: \f239; $fa-var-suitcase: \f0f2; $fa-var-suitcase-rolling: \f5c1; $fa-var-sun: \f185; $fa-var-superpowers: \f2dd; $fa-var-superscript: \f12b; $fa-var-supple: \f3f9; $fa-var-surprise: \f5c2; $fa-var-suse: \f7d6; $fa-var-swatchbook: \f5c3; $fa-var-swift: \f8e1; $fa-var-swimmer: \f5c4; $fa-var-swimming-pool: \f5c5; $fa-var-symfony: \f83d; $fa-var-synagogue: \f69b; $fa-var-sync: \f021; $fa-var-sync-alt: \f2f1; $fa-var-syringe: \f48e; $fa-var-table: \f0ce; $fa-var-table-tennis: \f45d; $fa-var-tablet: \f10a; $fa-var-tablet-alt: \f3fa; $fa-var-tablets: \f490; $fa-var-tachometer-alt: \f3fd; $fa-var-tag: \f02b; $fa-var-tags: \f02c; $fa-var-tape: \f4db; $fa-var-tasks: \f0ae; $fa-var-taxi: \f1ba; $fa-var-teamspeak: \f4f9; $fa-var-teeth: \f62e; $fa-var-teeth-open: \f62f; $fa-var-telegram: \f2c6; $fa-var-telegram-plane: \f3fe; $fa-var-temperature-high: \f769; $fa-var-temperature-low: \f76b; $fa-var-tencent-weibo: \f1d5; $fa-var-tenge: \f7d7; $fa-var-terminal: \f120; $fa-var-text-height: \f034; $fa-var-text-width: \f035; $fa-var-th: \f00a; $fa-var-th-large: \f009; $fa-var-th-list: \f00b; $fa-var-the-red-yeti: \f69d; $fa-var-theater-masks: \f630; $fa-var-themeco: \f5c6; $fa-var-themeisle: \f2b2; $fa-var-thermometer: \f491; $fa-var-thermometer-empty: \f2cb; $fa-var-thermometer-full: \f2c7; $fa-var-thermometer-half: \f2c9; $fa-var-thermometer-quarter: \f2ca; $fa-var-thermometer-three-quarters: \f2c8; $fa-var-think-peaks: \f731; $fa-var-thumbs-down: \f165; $fa-var-thumbs-up: \f164; $fa-var-thumbtack: \f08d; $fa-var-ticket-alt: \f3ff; $fa-var-tiktok: \e07b; $fa-var-times: \f00d; $fa-var-times-circle: \f057; $fa-var-tint: \f043; $fa-var-tint-slash: \f5c7; $fa-var-tired: \f5c8; $fa-var-toggle-off: \f204; $fa-var-toggle-on: \f205; $fa-var-toilet: \f7d8; $fa-var-toilet-paper: \f71e; $fa-var-toilet-paper-slash: \e072; $fa-var-toolbox: \f552; $fa-var-tools: \f7d9; $fa-var-tooth: \f5c9; $fa-var-torah: \f6a0; $fa-var-torii-gate: \f6a1; $fa-var-tractor: \f722; $fa-var-trade-federation: \f513; $fa-var-trademark: \f25c; $fa-var-traffic-light: \f637; $fa-var-trailer: \e041; $fa-var-train: \f238; $fa-var-tram: \f7da; $fa-var-transgender: \f224; $fa-var-transgender-alt: \f225; $fa-var-trash: \f1f8; $fa-var-trash-alt: \f2ed; $fa-var-trash-restore: \f829; $fa-var-trash-restore-alt: \f82a; $fa-var-tree: \f1bb; $fa-var-trello: \f181; $fa-var-trophy: \f091; $fa-var-truck: \f0d1; $fa-var-truck-loading: \f4de; $fa-var-truck-monster: \f63b; $fa-var-truck-moving: \f4df; $fa-var-truck-pickup: \f63c; $fa-var-tshirt: \f553; $fa-var-tty: \f1e4; $fa-var-tumblr: \f173; $fa-var-tumblr-square: \f174; $fa-var-tv: \f26c; $fa-var-twitch: \f1e8; $fa-var-twitter: \f099; $fa-var-twitter-square: \f081; $fa-var-typo3: \f42b; $fa-var-uber: \f402; $fa-var-ubuntu: \f7df; $fa-var-uikit: \f403; $fa-var-umbraco: \f8e8; $fa-var-umbrella: \f0e9; $fa-var-umbrella-beach: \f5ca; $fa-var-uncharted: \e084; $fa-var-underline: \f0cd; $fa-var-undo: \f0e2; $fa-var-undo-alt: \f2ea; $fa-var-uniregistry: \f404; $fa-var-unity: \e049; $fa-var-universal-access: \f29a; $fa-var-university: \f19c; $fa-var-unlink: \f127; $fa-var-unlock: \f09c; $fa-var-unlock-alt: \f13e; $fa-var-unsplash: \e07c; $fa-var-untappd: \f405; $fa-var-upload: \f093; $fa-var-ups: \f7e0; $fa-var-usb: \f287; $fa-var-user: \f007; $fa-var-user-alt: \f406; $fa-var-user-alt-slash: \f4fa; $fa-var-user-astronaut: \f4fb; $fa-var-user-check: \f4fc; $fa-var-user-circle: \f2bd; $fa-var-user-clock: \f4fd; $fa-var-user-cog: \f4fe; $fa-var-user-edit: \f4ff; $fa-var-user-friends: \f500; $fa-var-user-graduate: \f501; $fa-var-user-injured: \f728; $fa-var-user-lock: \f502; $fa-var-user-md: \f0f0; $fa-var-user-minus: \f503; $fa-var-user-ninja: \f504; $fa-var-user-nurse: \f82f; $fa-var-user-plus: \f234; $fa-var-user-secret: \f21b; $fa-var-user-shield: \f505; $fa-var-user-slash: \f506; $fa-var-user-tag: \f507; $fa-var-user-tie: \f508; $fa-var-user-times: \f235; $fa-var-users: \f0c0; $fa-var-users-cog: \f509; $fa-var-users-slash: \e073; $fa-var-usps: \f7e1; $fa-var-ussunnah: \f407; $fa-var-utensil-spoon: \f2e5; $fa-var-utensils: \f2e7; $fa-var-vaadin: \f408; $fa-var-vector-square: \f5cb; $fa-var-venus: \f221; $fa-var-venus-double: \f226; $fa-var-venus-mars: \f228; $fa-var-vest: \e085; $fa-var-vest-patches: \e086; $fa-var-viacoin: \f237; $fa-var-viadeo: \f2a9; $fa-var-viadeo-square: \f2aa; $fa-var-vial: \f492; $fa-var-vials: \f493; $fa-var-viber: \f409; $fa-var-video: \f03d; $fa-var-video-slash: \f4e2; $fa-var-vihara: \f6a7; $fa-var-vimeo: \f40a; $fa-var-vimeo-square: \f194; $fa-var-vimeo-v: \f27d; $fa-var-vine: \f1ca; $fa-var-virus: \e074; $fa-var-virus-slash: \e075; $fa-var-viruses: \e076; $fa-var-vk: \f189; $fa-var-vnv: \f40b; $fa-var-voicemail: \f897; $fa-var-volleyball-ball: \f45f; $fa-var-volume-down: \f027; $fa-var-volume-mute: \f6a9; $fa-var-volume-off: \f026; $fa-var-volume-up: \f028; $fa-var-vote-yea: \f772; $fa-var-vr-cardboard: \f729; $fa-var-vuejs: \f41f; $fa-var-walking: \f554; $fa-var-wallet: \f555; $fa-var-warehouse: \f494; $fa-var-watchman-monitoring: \e087; $fa-var-water: \f773; $fa-var-wave-square: \f83e; $fa-var-waze: \f83f; $fa-var-weebly: \f5cc; $fa-var-weibo: \f18a; $fa-var-weight: \f496; $fa-var-weight-hanging: \f5cd; $fa-var-weixin: \f1d7; $fa-var-whatsapp: \f232; $fa-var-whatsapp-square: \f40c; $fa-var-wheelchair: \f193; $fa-var-whmcs: \f40d; $fa-var-wifi: \f1eb; $fa-var-wikipedia-w: \f266; $fa-var-wind: \f72e; $fa-var-window-close: \f410; $fa-var-window-maximize: \f2d0; $fa-var-window-minimize: \f2d1; $fa-var-window-restore: \f2d2; $fa-var-windows: \f17a; $fa-var-wine-bottle: \f72f; $fa-var-wine-glass: \f4e3; $fa-var-wine-glass-alt: \f5ce; $fa-var-wix: \f5cf; $fa-var-wizards-of-the-coast: \f730; $fa-var-wodu: \e088; $fa-var-wolf-pack-battalion: \f514; $fa-var-won-sign: \f159; $fa-var-wordpress: \f19a; $fa-var-wordpress-simple: \f411; $fa-var-wpbeginner: \f297; $fa-var-wpexplorer: \f2de; $fa-var-wpforms: \f298; $fa-var-wpressr: \f3e4; $fa-var-wrench: \f0ad; $fa-var-x-ray: \f497; $fa-var-xbox: \f412; $fa-var-xing: \f168; $fa-var-xing-square: \f169; $fa-var-y-combinator: \f23b; $fa-var-yahoo: \f19e; $fa-var-yammer: \f840; $fa-var-yandex: \f413; $fa-var-yandex-international: \f414; $fa-var-yarn: \f7e3; $fa-var-yelp: \f1e9; $fa-var-yen-sign: \f157; $fa-var-yin-yang: \f6ad; $fa-var-yoast: \f2b1; $fa-var-youtube: \f167; $fa-var-youtube-square: \f431; $fa-var-zhihu: \f63f; ================================================ FILE: src/app/shell/icons/font-awesome.scss ================================================ /* stylelint-disable */ @use 'sass:string'; @use './font-awesome-icon-variables.scss' as *; // Convenience function used to set content property @function fa-content($fa-var) { @return string.unquote('"#{ $fa-var }"'); } .fa-plus-circle:before { content: fa-content($fa-var-plus-circle); } .fa-archive:before { content: fa-content($fa-var-archive); } .fa-ban:before { content: fa-content($fa-var-ban); } .fa-bolt:before { content: fa-content($fa-var-bolt); } .fa-eraser:before { content: fa-content($fa-var-eraser); } .fa-times:before { content: fa-content($fa-var-times); } .fa-caret-down:before { content: fa-content($fa-var-caret-down); } .fa-balance-scale-left:before { content: fa-content($fa-var-balance-scale-left); } .fa-copy:before { content: fa-content($fa-var-copy); } .fa-trash-alt:before { content: fa-content($fa-var-trash-alt); } .fa-times-circle:before { content: fa-content($fa-var-times-circle); } .fa-file-export:before { content: fa-content($fa-var-file-export); } .fa-grip-vertical:before { content: fa-content($fa-var-grip-vertical); } .fa-pencil-alt:before { content: fa-content($fa-var-pencil-alt); } .fa-check-circle:before { content: fa-content($fa-var-check-circle); } .fa-equals:before { content: fa-content($fa-var-equals); } .fa-chevron-down:before { content: fa-content($fa-var-chevron-down); } .fa-caret-right:before { content: fa-content($fa-var-caret-right); } .fa-chevron-up:before { content: fa-content($fa-var-chevron-up); } .fa-angle-left:before { content: fa-content($fa-var-angle-left); } .fa-angle-right:before { content: fa-content($fa-var-angle-right); } .fa-arrow-circle-down:before { content: fa-content($fa-var-arrow-circle-down); } .fa-arrow-circle-up:before { content: fa-content($fa-var-arrow-circle-up); } .fa-calculator:before { content: fa-content($fa-var-calculator); } .fa-caret-down:before { content: fa-content($fa-var-caret-down); } .fa-caret-up:before { content: fa-content($fa-var-caret-up); } .fa-check:before { content: fa-content($fa-var-check); } .fa-check-circle:before { content: fa-content($fa-var-check-circle); } .fa-check-square:before { content: fa-content($fa-var-check-square); } .fa-clock:before { content: fa-content($fa-var-clock); } .fa-discord:before { content: fa-content($fa-var-discord); } .fa-equals:before { content: fa-content($fa-var-equals); } .fa-exclamation-circle:before { content: fa-content($fa-var-exclamation-circle); } .fa-exclamation-triangle:before { content: fa-content($fa-var-exclamation-triangle); } .fa-external-link-alt:before { content: fa-content($fa-var-external-link-alt); } .fa-github:before { content: fa-content($fa-var-github); } .fa-th:before { content: fa-content($fa-var-th); } .fa-list:before { content: fa-content($fa-var-list); } .fa-minus-square:before { content: fa-content($fa-var-minus-square); } .fa-playstation:before { content: fa-content($fa-var-playstation); } .fa-plus-square:before { content: fa-content($fa-var-plus-square); } .fa-random:before { content: fa-content($fa-var-random); } .fa-square:before { content: fa-content($fa-var-square); } .fa-steam:before { content: fa-content($fa-var-steam); } .fa-times-circle:before { content: fa-content($fa-var-times-circle); } .fa-tshirt:before { content: fa-content($fa-var-tshirt); } .fa-window-close:before { content: fa-content($fa-var-window-close); } .fa-xbox:before { content: fa-content($fa-var-xbox); } .fa-globe:before { content: fa-content($fa-var-globe); } .fa-greater-than-equal:before { content: fa-content($fa-var-greater-than-equal); } .fa-heart:before { content: fa-content($fa-var-heart); } .fa-question-circle:before { content: fa-content($fa-var-question-circle); } .fa-info-circle:before { content: fa-content($fa-var-info-circle); } .fa-ellipsis-v:before { content: fa-content($fa-var-ellipsis-v); } .fa-less-than-equal:before { content: fa-content($fa-var-less-than-equal); } .fa-level-down-alt:before { content: fa-content($fa-var-level-down-alt); } .fa-level-up-alt:before { content: fa-content($fa-var-level-up-alt); } .fa-lock:before { content: fa-content($fa-var-lock); } .fa-mastodon:before { content: fa-content($fa-var-mastodon); } .fa-angle-double-left:before { content: fa-content($fa-var-angle-double-left); } .fa-bars:before { content: fa-content($fa-var-bars); } .fa-angle-double-right:before { content: fa-content($fa-var-angle-double-right); } .fa-minus:before { content: fa-content($fa-var-minus); } .fa-arrow-down:before { content: fa-content($fa-var-arrow-down); } .fa-arrow-right:before { content: fa-content($fa-var-arrow-right); } .fa-arrow-up:before { content: fa-content($fa-var-arrow-up); } .fa-thumbtack:before { content: fa-content($fa-var-thumbtack); } .fa-plus:before { content: fa-content($fa-var-plus); } .fa-redo:before { content: fa-content($fa-var-redo); } .fa-sync:before { content: fa-content($fa-var-sync); } .fa-arrow-right:before { content: fa-content($fa-var-arrow-right); } .fa-save:before { content: fa-content($fa-var-save); } .fa-search:before { content: fa-content($fa-var-search); } .fa-envelope:before { content: fa-content($fa-var-envelope); } .fa-cog:before { content: fa-content($fa-var-cog); } .fa-share:before { content: fa-content($fa-var-share); } .fa-shopping-cart:before { content: fa-content($fa-var-shopping-cart); } .fa-sign-out-alt:before { content: fa-content($fa-var-sign-out-alt); } .fa-slash:before { content: fa-content($fa-var-slash); } .fa-table:before { content: fa-content($fa-var-table); } .fa-layer-group:before { content: fa-content($fa-var-layer-group); } .fa-star:before { content: fa-content($fa-var-star); } .fa-star:before { content: fa-content($fa-var-star); } .fa-sticky-note:before { content: fa-content($fa-var-sticky-note); } .fa-tag:before { content: fa-content($fa-var-tag); } .fa-thumbs-down:before { content: fa-content($fa-var-thumbs-down); } .fa-thumbs-up:before { content: fa-content($fa-var-thumbs-up); } .fa-bookmark:before { content: fa-content($fa-var-bookmark); } .fa-bookmark:before { content: fa-content($fa-var-bookmark); } .fa-undo:before { content: fa-content($fa-var-undo); } .fa-unlock:before { content: fa-content($fa-var-unlock); } .fa-check-circle:before { content: fa-content($fa-var-check-circle); } .fa-arrow-circle-up:before { content: fa-content($fa-var-arrow-circle-up); } .fa-file-import:before { content: fa-content($fa-var-file-import); } ================================================ FILE: src/app/shell/icons/index.ts ================================================ import AppIconComponent from './AppIcon'; export const AppIcon = AppIconComponent; export { dimEngramIcon as engramIcon } from './custom/Engram'; export { dimEnhancedIcon as enhancedIcon } from './custom/Enhanced'; export { epicIcon } from './custom/Epic'; export { featuredBannerIcon } from './custom/FeaturedBanner'; export { dimHunterIcon as hunterIcon } from './custom/Hunter'; export { dimPowerIcon as powerIndicatorIcon } from './custom/Power'; export { dimPowerAltIcon as powerActionIcon } from './custom/PowerAlt'; export { dimShapedIcon as shapedIcon } from './custom/Shaped'; export { statBarsIcon } from './custom/StatBarsIcon'; export { dimTitanIcon as titanIcon } from './custom/Titan'; export { tunedStatIcon } from './custom/TunedStatIcon'; export { dimWarlockIcon as warlockIcon } from './custom/Warlock'; export * from './Library.js'; ================================================ FILE: src/app/shell/item-comparators.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import { getSeason } from 'app/inventory/store/season'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { ItemRarityMap } from 'app/search/d2-known-values'; import { ItemSortSettings } from 'app/settings/item-sort'; import { getArmor3StatFocus, getSpecialtySocketMetadata, isArmor3, isArtifice, isD1Item, } from 'app/utils/item-utils'; import { getWeaponArchetype } from 'app/utils/socket-utils'; import { DestinyAmmunitionType, DestinyDamageTypeDefinition } from 'bungie-api-ts/destiny2'; import { BucketHashes, ItemCategoryHashes } from 'data/d2/generated-enums'; import { TagValue, tagConfig, vaultGroupTagOrder } from '../inventory/dim-item-info'; import { Comparator, chainComparator, compareBy, compareByIndex, reverseComparator, } from '../utils/comparators'; const INSTANCEID_PADDING = 20; export const getItemRecencyKey = (item: DimItem) => item.instanced ? item.id.padStart(INSTANCEID_PADDING, '0') : 0; /** * Sorts items by how recently they were acquired, newest items first. */ export const acquisitionRecencyComparator = reverseComparator(compareBy(getItemRecencyKey)); export const isNewerThan = (item: DimItem, watermarkInstanceId: string) => getItemRecencyKey(item) > watermarkInstanceId.padStart(INSTANCEID_PADDING, '0'); const D1_CONSUMABLE_SORT_ORDER = [ 1043138475, // black-wax-idol 1772853454, // blue-polyphage 3783295803, // ether-seeds 3446457162, // resupply-codes 269776572, // house-banners 3632619276, // silken-codex 2904517731, // axiomatic-beads 1932910919, // network-keys // 417308266, // three of coins // 2180254632, // ammo-synth 928169143, // special-ammo-synth 211861343, // heavy-ammo-synth // 705234570, // primary telemetry 3371478409, // special telemetry 2929837733, // heavy telemetry 4159731660, // auto rifle telemetry 846470091, // hand cannon telemetry 2610276738, // pulse telemetry 323927027, // scout telemetry 729893597, // fusion rifle telemetry 4141501356, // shotgun telemetry 927802664, // sniper rifle telemetry 1485751393, // machine gun telemetry 3036931873, // rocket launcher telemetry // 2220921114, // vanguard rep boost 1500229041, // crucible rep boost 1603376703, // HoJ rep boost // 2575095887, // Splicer Intel Relay 3815757277, // Splicer Cache Key 4244618453, // Splicer Key ]; const D1_MATERIAL_SORT_ORDER = [ 1797491610, // Helium 3242866270, // Relic Iron 2882093969, // Spin Metal 2254123540, // Spirit Bloom 3164836592, // Wormspore 3164836593, // Hadium Flakes // 452597397, // Exotic Shard 1542293174, // Armor Materials 1898539128, // Weapon Materials // 937555249, // Motes of Light // 1738186005, // Strange Coins // 258181985, // Ascendant Shards 1893498008, // Ascendant Energy 769865458, // Radiant Shards 616706469, // Radiant Energy // 342707701, // Reciprocal Rune 342707700, // Stolen Rune 2906158273, // Antiquated Rune 2620224196, // Stolen Rune (Charging) 2906158273, // Antiquated Rune (Charging) ]; // Bucket IDs that'll never be sorted. const ITEM_SORT_DENYLIST = new Set([ D1BucketHashes.Bounties, D1BucketHashes.Missions, D1BucketHashes.Quests, ]); // These comparators require knowledge of the tag state/database const TAG_ITEM_COMPARATORS: { [key: string]: (getTag: (item: DimItem) => TagValue | undefined) => Comparator<DimItem>; } = { // see tagConfig tag: (getTag) => compareBy((item) => { const tag = getTag(item); return (tag && tagConfig[tag]?.sortOrder) ?? 1000; }), // not archive -> archive archive: (getTag) => compareBy((item) => getTag(item) === 'archive'), }; export type VaultGroupValue = string | number | boolean | undefined; interface VaultGroupIconNone { type: 'none'; } interface VaultGroupIconTag { type: 'tag'; tag: TagValue | undefined; } interface VaultGroupIconTypeName { type: 'typeName'; itemCategoryHashes: ItemCategoryHashes[]; } interface VaultGroupIconAmmoType { type: 'ammoType'; ammoType: DestinyAmmunitionType; } interface VaultGroupIconElementWeapon { type: 'elementWeapon'; element: DestinyDamageTypeDefinition | null; } export type VaultGroupIcon = | VaultGroupIconNone | VaultGroupIconTag | VaultGroupIconTypeName | VaultGroupIconAmmoType | VaultGroupIconElementWeapon; interface VaultGroup { groupingValue: VaultGroupValue; icon: VaultGroupIcon; items: DimItem[]; } const groupingValueIndexInTagOrder = (input: VaultGroup) => { if (typeof input.groupingValue !== 'string') { return Infinity; } const index = vaultGroupTagOrder.indexOf(input.groupingValue as TagValue); if (index < 0) { return Infinity; } return index; }; const groupingValueProperty = (input: VaultGroup) => input.groupingValue; const undefinedVaultGroupLast = (comparator: Comparator<VaultGroup>) => (a: VaultGroup, b: VaultGroup) => { if (a.groupingValue === undefined) { if (b.groupingValue === undefined) { return 0; } return 1; } if (b.groupingValue === undefined) { return -1; } return comparator(a, b); }; const GROUP_BY_GETTERS_AND_COMPARATORS: { [key: string]: { comparator: Comparator<VaultGroup>; getValue: (item: DimItem, getTag: (item: DimItem) => TagValue | undefined) => VaultGroupValue; getIcon: (item: DimItem, getTag: (item: DimItem) => TagValue | undefined) => VaultGroupIcon; }; } = { tag: { comparator: undefinedVaultGroupLast(compareBy(groupingValueIndexInTagOrder)), getValue: (item, getTag) => getTag(item), getIcon: (item, getTag) => ({ type: 'tag', tag: getTag(item) }), }, // A -> Z typeName: { comparator: undefinedVaultGroupLast(compareBy(groupingValueProperty)), getValue: (item) => item.typeName, getIcon: (item) => ({ type: 'typeName', itemCategoryHashes: item.itemCategoryHashes }), }, // exotic -> common rarity: { comparator: undefinedVaultGroupLast(reverseComparator(compareBy(groupingValueProperty))), getValue: (item) => ItemRarityMap[item.rarity], getIcon: () => ({ type: 'none' }), }, // None -> Primary -> Special -> Heavy -> Unknown ammoType: { comparator: undefinedVaultGroupLast(compareBy(groupingValueProperty)), getValue: (item) => item.ammoType, getIcon: (item) => ({ type: 'ammoType', ammoType: item.ammoType }), }, // None -> Kinetic -> Arc -> Thermal -> Void -> Raid -> Stasis elementWeapon: { comparator: undefinedVaultGroupLast(compareBy(groupingValueProperty)), getValue: (item) => { if (item.bucket.inWeapons) { return item.element?.enumValue ?? Number.MAX_SAFE_INTEGER; } }, getIcon: (item) => { if (item.bucket.inWeapons) { return { type: 'elementWeapon', element: item.element, }; } return { type: 'none', }; }, }, }; const ITEM_COMPARATORS: { [key: string]: Comparator<DimItem>; } = { // A -> Z typeName: compareBy((item) => item.typeName), // exotic -> common rarity: reverseComparator(compareBy((item) => ItemRarityMap[item.rarity])), // high -> low primStat: reverseComparator(compareBy((item) => item.primaryStat?.value ?? 0)), // high -> low basePower: reverseComparator(compareBy((item) => item.power)), // This only sorts by D1 item quality rating: reverseComparator(compareBy((item) => isD1Item(item) && item.quality?.min)), // Titan -> Hunter -> Warlock -> Unknown classType: compareBy((item) => item.classType), // None -> Primary -> Special -> Heavy -> Unknown ammoType: compareBy((item) => item.ammoType), // A -> Z name: compareBy((item) => item.name), // lots -> few amount: reverseComparator(compareBy((item) => item.amount)), // recent season -> old season season: reverseComparator( chainComparator( compareBy((item) => (item.destinyVersion === 2 ? getSeason(item) : 0)), compareBy((item) => item.iconOverlay ?? ''), ), ), // new -> old acquisitionRecency: acquisitionRecencyComparator, // None -> Kinetic -> Arc -> Thermal -> Void -> Raid -> Stasis elementWeapon: compareBy((item) => { if (item.bucket.inWeapons) { return item.element?.enumValue ?? Number.MAX_SAFE_INTEGER; } }), // masterwork -> not masterwork masterworked: compareBy((item) => (item.masterwork ? 0 : 1)), // crafted -> not crafted crafted: compareBy((item) => (item.crafted ? 0 : 1)), // deepsight -> no deepsight deepsight: compareBy((item) => (item.deepsightInfo ? 1 : 2)), // featured -> not featured featured: compareBy((item) => (item.featured ? 0 : 1)), // high -> low tier: reverseComparator(compareBy((item) => item.tier)), // armor 3 archetypes -> artifice -> old specialty modslots -> nada armorArchetype: compareBy((item) => { if (!item.bucket.inArmor) { return '0'; } if (isArmor3(item)) { return `1 ${getArmor3StatFocus(item)?.[0]}`; } if (isArtifice(item)) { return '2'; } const specialtySocket = getSpecialtySocketMetadata(item)?.slotTag; if (specialtySocket) { return `3 ${specialtySocket}`; } return '4'; }), // non-weapons -> ascending frame rarity -> frame index weaponFrame: compareBy((item) => { if (!item.bucket.inWeapons) { return '0'; } const frame = getWeaponArchetype(item); if (frame) { // Tier types to separate e.g. Ergo Sum Tiebreak matching names just in case return `1 ${frame.inventory!.tierType} ${frame.displayProperties.name} ${frame.displayProperties.icon}`; } return '2'; }), default: () => 0, }; /** * Sort items according to the user's preferences (via the sort parameter). * Returned array is readonly since it could either be a new array or the * original. */ export function sortItems( items: readonly DimItem[], itemSortSettings: ItemSortSettings, getTag: (item: DimItem) => TagValue | undefined, ): readonly DimItem[] { if (!items.length) { return items; } const itemLocationId = items[0].location.hash; if (!items.length || ITEM_SORT_DENYLIST.has(itemLocationId)) { return items; } let specificSortOrder: number[] = []; // Group like items in the General Section if (itemLocationId === BucketHashes.Consumables) { specificSortOrder = D1_CONSUMABLE_SORT_ORDER; } // Group like items in the General Section if (itemLocationId === BucketHashes.Materials) { specificSortOrder = D1_MATERIAL_SORT_ORDER; } if (specificSortOrder.length > 0 && !itemSortSettings.sortOrder.includes('rarity')) { return items.toSorted(compareByIndex(specificSortOrder, (item) => item.hash)); } // Re-sort consumables if (itemLocationId === BucketHashes.Consumables) { return items.toSorted( chainComparator( ITEM_COMPARATORS.typeName, ITEM_COMPARATORS.rarity, ITEM_COMPARATORS.name, ITEM_COMPARATORS.amount, ), ); } // Engrams and Postmaster always sort by recency, oldest to newest, like in game if (itemLocationId === BucketHashes.Engrams || itemLocationId === BucketHashes.LostItems) { return items.toSorted(reverseComparator(acquisitionRecencyComparator)); } // always sort by archive first const comparator = chainComparator( ...['archive', ...itemSortSettings.sortOrder].map((comparatorName) => { let comparator = ITEM_COMPARATORS[comparatorName]; if (!comparator) { const tagComparator = TAG_ITEM_COMPARATORS[comparatorName]?.(getTag); if (!tagComparator) { return ITEM_COMPARATORS.default; } comparator = tagComparator; } return itemSortSettings.sortReversals.includes(comparatorName) ? reverseComparator(comparator) : comparator; }), ); return items.toSorted(comparator); } export function groupItems( items: readonly DimItem[], vaultGrouping: string, getTag: (item: DimItem) => TagValue | undefined, ): readonly VaultGroup[] { const comparatorsAndGetters = GROUP_BY_GETTERS_AND_COMPARATORS[vaultGrouping]; // If there are no items, or the grouping is not supported, return all items in a single group if (!items.length || !comparatorsAndGetters) { return [{ groupingValue: undefined, icon: { type: 'none' }, items: [...items] }]; } const { getValue, getIcon, comparator } = comparatorsAndGetters; const groupedItems = Map.groupBy(items, (item) => getValue(item, getTag)); return Array.from( groupedItems.entries(), ([groupingValue, items]): VaultGroup => ({ groupingValue, items, icon: groupingValue === undefined ? // Don't display an icon if they are ungrouped { type: 'none', } : getIcon(items[0], getTag), }), ).sort(comparator); } // Used to create string keys for vault grouping values export const vaultGroupingValueWithType = (value: VaultGroupValue) => `${typeof value}-${value}`; ================================================ FILE: src/app/shell/links.ts ================================================ /** Shared constants for external links that may be used on multiple pages. */ export const bungieHelpLink = 'https://mastodon.social/@bungiehelp'; export const discordLink = 'https://discord.gg/UK2GWC7'; export const userGuideLink = 'https://guide.dim.gg'; export const wishListGuideLink = 'https://github.com/DestinyItemManager/DIM/wiki/Creating-Wish-Lists'; export const bungieHelpAccount = '@BungieHelp'; export function userGuideUrl(topic: string) { return `${userGuideLink}/${topic}`; } export const troubleshootingLink = userGuideUrl('Troubleshooting'); ================================================ FILE: src/app/shell/loading-tracker.ts ================================================ import { Observable } from 'app/utils/observable'; /** * An object that can keep track of multiple running promises in order to drive a loading spinner. */ class PromiseTracker { numTracked = 0; active$ = new Observable(false); addPromise<T>(promise: Promise<T>): Promise<T> { this.numTracked++; this.active$.next(true); promise.then(this.countDown, this.countDown); return promise; } active() { return this.numTracked > 0; } /** Convert a function that returns a promise into a function that tracks that promise then returns it. */ trackPromise = <T extends unknown[], K>(promiseFn: (...args: T) => Promise<K>): ((...args: T) => Promise<K>) => (...args: T) => { const promise = promiseFn(...args); this.addPromise(promise); return promise; }; private countDown = () => { this.numTracked--; this.active$.next(this.active()); }; } export const loadingTracker = new PromiseTracker(); ================================================ FILE: src/app/shell/reducer.ts ================================================ import { GlobalAlert } from 'bungie-api-ts/core'; import { deepEqual } from 'fast-equals'; import { Reducer } from 'redux'; import { ActionType, getType } from 'typesafe-actions'; import { isPhonePortraitFromMediaQuery } from '../utils/media-queries'; import * as actions from './actions'; export interface ShellState { readonly isPhonePortrait: boolean; readonly searchQuery: string; /** * This is a workaround for the fact that our search query input is debounced. When setting the * query text from outside of the search input, this version will be updated, which tells the * search input component to reset its internal state. Otherwise if we listened to every * change of the search query text, your typing would be undone when the redux store updates. */ readonly searchQueryVersion: number; /** * Whether the detailed search results drawer is open. The logic for this is * a bit tricky and needs to be shared between a few components. */ readonly searchResultsOpen: boolean; /** Global, page-covering loading state. */ readonly loadingMessages: string[]; /** BrowserRouter custom location */ readonly routerLocation?: string; readonly bungieAlerts: GlobalAlert[]; } export type ShellAction = ActionType<typeof actions>; const initialState: ShellState = { isPhonePortrait: isPhonePortraitFromMediaQuery(), searchQuery: '', searchQueryVersion: 0, searchResultsOpen: false, loadingMessages: [], routerLocation: '', bungieAlerts: [], }; export const shell: Reducer<ShellState, ShellAction> = ( state: ShellState = initialState, action: ShellAction, ): ShellState => { switch (action.type) { case getType(actions.setPhonePortrait): return { ...state, isPhonePortrait: action.payload, }; case getType(actions.setSearchQuery): { const { query, updateVersion } = action.payload; if (query === undefined) { throw new Error('undefined query'); } return query !== state.searchQuery ? { ...state, searchQuery: query, searchQueryVersion: updateVersion ? state.searchQueryVersion + 1 : state.searchQueryVersion, searchResultsOpen: (query && state.searchResultsOpen) || Boolean(state.isPhonePortrait && !state.searchQuery && query), } : state; } case getType(actions.toggleSearchQueryComponent): { const existingQuery = state.searchQuery; const queryComponent = action.payload.trim(); const newQuery = existingQuery.includes(queryComponent) ? existingQuery.replace(queryComponent, '') : `${existingQuery} ${queryComponent}`; return { ...state, searchQuery: newQuery.replace(/\s+/, ' ').trim(), searchQueryVersion: state.searchQueryVersion + 1, }; } case getType(actions.toggleSearchResults): { return { ...state, searchResultsOpen: action.payload ?? !state.searchResultsOpen, }; } case getType(actions.loadingStart): { return { ...state, loadingMessages: [...new Set([...state.loadingMessages, action.payload])], }; } case getType(actions.loadingEnd): { return { ...state, loadingMessages: state.loadingMessages.filter((m) => m !== action.payload), }; } case getType(actions.updateBungieAlerts): { return deepEqual(state.bungieAlerts, action.payload) ? state : { ...state, bungieAlerts: action.payload }; } case getType(actions.setRouterLocation): { return { ...state, routerLocation: action.payload, }; } case getType(actions.resetRouterLocation): { return { ...state, routerLocation: undefined, }; } default: return state; } }; ================================================ FILE: src/app/shell/refresh-events.ts ================================================ import { EventBus } from 'app/utils/observable'; export const refresh$ = new EventBus<undefined>(); export function refresh(e?: React.MouseEvent | KeyboardEvent) { // Individual pages should listen to this event and decide what to refresh, // and their services should decide how to cache/dedupe refreshes. // This event should *NOT* be listened to by services! if (e) { e.preventDefault(); } refresh$.next(undefined); } ================================================ FILE: src/app/shell/selectors.ts ================================================ import { RootState } from 'app/store/types'; import { useSelector } from 'react-redux'; export const isPhonePortraitSelector = (state: RootState) => state.shell.isPhonePortrait; export const querySelector = (state: RootState) => state.shell.searchQuery; export const hasSearchQuerySelector = (state: RootState) => Boolean(state.shell.searchQuery); export const searchQueryVersionSelector = (state: RootState) => state.shell.searchQueryVersion; export const bungieAlertsSelector = (state: RootState) => state.shell.bungieAlerts; export const searchResultsOpenSelector = (state: RootState) => state.shell.searchResultsOpen; export const routerLocationSelector = (state: RootState) => state.shell.routerLocation; export function useIsPhonePortrait() { return useSelector(isPhonePortraitSelector); } ================================================ FILE: src/app/storage/DimApiSettings.m.scss ================================================ .storage { p { margin: 8px 0; } ul { margin: 8px 0; padding-left: 2em; } } ================================================ FILE: src/app/storage/DimApiSettings.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'storage': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/storage/DimApiSettings.tsx ================================================ import { ExportResponse } from '@destinyitemmanager/dim-api-types'; import { deleteAllApiData, loadDimApiData } from 'app/dim-api/actions'; import { setApiPermissionGranted } from 'app/dim-api/basic-actions'; import { exportDimApiData } from 'app/dim-api/dim-api'; import { importDataBackup } from 'app/dim-api/import'; import { apiPermissionGrantedSelector, dimSyncErrorSelector } from 'app/dim-api/selectors'; import HelpLink from 'app/dim-ui/HelpLink'; import useConfirm from 'app/dim-ui/useConfirm'; import { t } from 'app/i18next-t'; import { dimApiHelpLink } from 'app/login/Login'; import { showNotification } from 'app/notifications/notifications'; import Checkbox from 'app/settings/Checkbox'; import { fineprintClass, horizontalClass, settingClass } from 'app/settings/SettingsPage'; import { Settings } from 'app/settings/initial-settings'; import ErrorPanel from 'app/shell/ErrorPanel'; import { AppIcon, deleteIcon, refreshIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { errorMessage } from 'app/utils/errors'; import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import { Link } from 'react-router'; import * as styles from './DimApiSettings.m.scss'; import ImportExport from './ImportExport'; import LocalStorageInfo from './LocalStorageInfo'; import { exportBackupData, exportLocalData } from './export-data'; export default function DimApiSettings() { const dispatch = useThunkDispatch(); const apiPermissionGranted = useSelector(apiPermissionGrantedSelector); const profileLoadedError = useSelector(dimSyncErrorSelector); const [hasBackedUp, setHasBackedUp] = useState(false); const onApiPermissionChange = async (checked: boolean) => { const granted = checked; dispatch(setApiPermissionGranted(granted)); if (granted) { const data = await dispatch(exportLocalData()); // Force a backup of their data just in case exportBackupData(data); showBackupDownloadedNotification(); dispatch(loadDimApiData()); } else { // Reset the warning about data not being saved localStorage.removeItem('warned-no-sync'); } }; const onExportData = async () => { setHasBackedUp(true); let data: ExportResponse; if (apiPermissionGranted) { // Export from the server try { data = await exportDimApiData(); } catch (e) { showNotification({ type: 'error', title: t('Storage.ExportError'), body: t('Storage.ExportErrorBody', { error: errorMessage(e) }), duration: 15000, }); data = await dispatch(exportLocalData()); } } else { // Export from local data data = await dispatch(exportLocalData()); } exportBackupData(data); }; const [confirmDialog, confirm] = useConfirm(); const onImportData = async (data: ExportResponse) => { if (await confirm(t('Storage.ImportConfirmDimApi'))) { await dispatch(importDataBackup(data)); } }; const deleteAllData = async (e: React.MouseEvent) => { e.preventDefault(); if (apiPermissionGranted && !hasBackedUp) { showNotification({ type: 'warning', title: t('Storage.BackUpFirst') }); } else if (await confirm(t('Storage.DeleteAllDataConfirm'))) { dispatch(deleteAllApiData()); } }; const refreshDimSync = async () => { await dispatch(loadDimApiData({ forceLoad: true })); }; return ( <section className={styles.storage} id="storage"> {confirmDialog} <h2>{t('Storage.MenuTitle')}</h2> <div className={settingClass}> <Checkbox name={'apiPermissionGranted' as keyof Settings} label={ <> {t('Storage.EnableDimApi')} <HelpLink helpLink={dimApiHelpLink} /> </> } value={apiPermissionGranted} onChange={onApiPermissionChange} /> <div className={fineprintClass}>{t('Storage.DimApiFinePrint')}</div> {apiPermissionGranted && ( <> <button type="button" className="dim-button" onClick={refreshDimSync}> <AppIcon icon={refreshIcon} /> {t('Storage.RefreshDimSync')} </button> <button type="button" className="dim-button" onClick={deleteAllData}> <AppIcon icon={deleteIcon} /> {t('Storage.DeleteAllData')} </button> </> )} </div> {profileLoadedError && ( <ErrorPanel title={t('Storage.ProfileErrorTitle')} error={profileLoadedError} /> )} {apiPermissionGranted && ( <div className={settingClass}> <div className={horizontalClass}> <label>{t('SearchHistory.Link')}</label> <Link to="/search-history" className="dim-button"> {t('SearchHistory.Title')} </Link> </div> </div> )} <LocalStorageInfo showDetails={!apiPermissionGranted} className={settingClass} /> <div className={settingClass}> <ImportExport onExportData={onExportData} onImportData={onImportData} /> </div> </section> ); } // TODO: gotta change all these strings function showBackupDownloadedNotification() { showNotification({ type: 'success', title: t('Storage.DimSyncEnabled'), body: t('Storage.AutoBackup'), duration: 15000, }); } ================================================ FILE: src/app/storage/DimApiWarningBanner.tsx ================================================ import { dimSyncErrorSelector, updateQueueLengthSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import HeaderWarningBanner from 'app/shell/HeaderWarningBanner'; import { useSelector } from 'react-redux'; /** * Shows an error banner in the header whenever we're having problems talking to DIM Sync. Goes away when we reconnect. */ export default function DimApiWarningBanner() { const syncError = useSelector(dimSyncErrorSelector); const updateQueueLength = useSelector(updateQueueLengthSelector); if (!syncError) { return null; } return ( <HeaderWarningBanner> <span> {t('Storage.DimSyncDown')}{' '} {updateQueueLength > 0 && t('Storage.UpdateQueueLength', { count: updateQueueLength })} </span> </HeaderWarningBanner> ); } ================================================ FILE: src/app/storage/ImportExport.tsx ================================================ import { ExportResponse } from '@destinyitemmanager/dim-api-types'; import FileUpload from 'app/dim-ui/FileUpload'; import { t } from 'app/i18next-t'; import { showNotification } from 'app/notifications/notifications'; import { AppIcon, downloadIcon } from 'app/shell/icons'; import { errorMessage } from 'app/utils/errors'; import React from 'react'; import { DropzoneOptions } from 'react-dropzone'; export default function ImportExport({ onExportData, onImportData, }: { onExportData: () => void; onImportData: (data: ExportResponse) => Promise<void>; }) { const importData: DropzoneOptions['onDrop'] = (acceptedFiles) => { if (acceptedFiles.length < 1) { showNotification({ type: 'error', title: t('Storage.ImportWrongFileType') }); return; } if (acceptedFiles.length > 1) { showNotification({ type: 'error', title: t('Storage.ImportTooManyFiles') }); return; } const reader = new FileReader(); reader.onload = async () => { if (reader.result && typeof reader.result === 'string') { try { // dispatch action here? const data = JSON.parse(reader.result) as ExportResponse; await onImportData(data); } catch (e) { showNotification({ type: 'error', title: t('Storage.ImportFailed', { error: errorMessage(e) }), }); } } }; const file = acceptedFiles[0]; if (file) { reader.readAsText(file); } else { showNotification({ type: 'error', title: t('Storage.ImportNoFile') }); } return false; }; const exportData = (e: React.MouseEvent) => { e.preventDefault(); onExportData(); }; return ( <> <span>{t('Storage.ImportExport')}</span> <button type="button" className="dim-button" onClick={exportData}> <AppIcon icon={downloadIcon} /> {t('Storage.Export')} </button> <FileUpload onDrop={importData} accept={{ 'application/json': ['.json'] }} title={t('Storage.Import')} /> </> ); } ================================================ FILE: src/app/storage/LocalStorageInfo.m.scss ================================================ @use '../variables.scss' as *; .warningBlock { color: var(--theme-text); background: $red; padding: 0.5em 1em; display: inline-block; } .gauge { flex: 1; background-color: rgb(0, 0, 0, 0.6); position: relative; min-height: 17px; display: flex; flex-direction: row; align-items: center; color: var(--theme-text); div { background-color: $green; position: absolute; top: 0; left: 0; bottom: 0; &.full { background-color: $red; } } } ================================================ FILE: src/app/storage/LocalStorageInfo.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'full': string; 'gauge': string; 'warningBlock': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/storage/LocalStorageInfo.tsx ================================================ import { t } from 'app/i18next-t'; import { percent } from 'app/shell/formatters'; import clsx from 'clsx'; import { useEffect, useState } from 'react'; import * as styles from './LocalStorageInfo.m.scss'; export default function LocalStorageInfo({ showDetails, className, }: { showDetails: boolean; className?: string; }) { const [browserMayClearData, setBrowserMayClearData] = useState(true); const [quota, setQuota] = useState<{ quota: number; usage: number }>(); useEffect(() => { if ('storage' in navigator && 'estimate' in navigator.storage) { navigator.storage.estimate().then(({ usage, quota }) => { if (usage && usage >= 0 && quota && quota >= 0) { setQuota({ usage, quota }); } }); } if ('storage' in navigator && 'persist' in navigator.storage) { navigator.storage.persisted().then((persistent) => { setBrowserMayClearData(!persistent); }); } }, []); if (!showDetails && !quota) { return null; } return ( <div className={className}> {showDetails && ( <> <h3>{t('Storage.IndexedDBStorage')}</h3> <p>{t(`Storage.Details.IndexedDBStorage`)}</p> {browserMayClearData && ( <p className={styles.warningBlock}>{t('Storage.BrowserMayClearData')}</p> )} </> )} {quota && ( <div> <div className={styles.gauge}> <div className={clsx({ [styles.full]: quota.usage / quota.quota > 0.9, })} style={{ width: percent(Math.max(quota.usage / quota.quota, 0.01)) }} /> </div> <p>{t('Storage.Usage', quota)}</p> </div> )} </div> ); } ================================================ FILE: src/app/storage/export-data.ts ================================================ import { DestinyVersion, ExportResponse } from '@destinyitemmanager/dim-api-types'; import { parseProfileKey } from 'app/dim-api/reducer'; import { ThunkResult } from 'app/store/types'; import { download } from 'app/utils/download'; /** * Export the local IDB data to a format the DIM API could import. */ export function exportLocalData(): ThunkResult<ExportResponse> { return async (_dispatch, getState) => { const dimApiState = getState().dimApi; const exportResponse: ExportResponse = { settings: dimApiState.settings, loadouts: [], tags: [], triumphs: [], itemHashTags: [], searches: [], }; for (const profileKey in dimApiState.profiles) { if (Object.prototype.hasOwnProperty.call(dimApiState.profiles, profileKey)) { const [platformMembershipId, destinyVersion] = parseProfileKey(profileKey); for (const loadout of Object.values(dimApiState.profiles[profileKey].loadouts)) { exportResponse.loadouts.push({ loadout, platformMembershipId, destinyVersion, }); } for (const annotation of Object.values(dimApiState.profiles[profileKey].tags)) { exportResponse.tags.push({ annotation, platformMembershipId, destinyVersion, }); } exportResponse.triumphs.push({ platformMembershipId, triumphs: dimApiState.profiles[profileKey].triumphs, }); } } exportResponse.itemHashTags = Object.values(dimApiState.itemHashTags); for (const destinyVersionStr in dimApiState.searches) { const destinyVersion = parseInt(destinyVersionStr, 10) as DestinyVersion; for (const search of dimApiState.searches[destinyVersion]) { exportResponse.searches.push({ destinyVersion, search, }); } } return exportResponse; }; } /** * Export the data backup as a file */ export function exportBackupData(data: ExportResponse) { download(JSON.stringify(data), 'dim-data.json', 'application/json'); } ================================================ FILE: src/app/storage/human-bytes.ts ================================================ /** * Prints a number of bytes in a more appropriate unit. */ export function humanBytes(size: number) { if (size <= 0) { return '0B'; } const i = Math.floor(Math.log(size) / Math.log(1024)); return `${(size / Math.pow(1024, i)).toFixed(2)} ${['B', 'KB', 'MB', 'GB', 'TB'][i]}`; } ================================================ FILE: src/app/storage/idb-keyval.ts ================================================ // This is a private copy of idb-keyval since https://github.com/jakearchibald/idb-keyval/pull/65 and https://github.com/jakearchibald/idb-keyval/pull/50 won't be merged export class Store { private readonly _dbName: string; private readonly _storeName: string; private _dbp: Promise<IDBDatabase> | undefined; constructor( dbName = 'keyval-store', readonly storeName = 'keyval', ) { this._dbName = dbName; this._storeName = storeName; } _init(): void { if (this._dbp) { return; } this._dbp = new Promise<IDBDatabase>((resolve, reject) => { const openreq = indexedDB.open(this._dbName, 1); openreq.onerror = () => reject(openreq.error ?? new Error('IDB open error')); openreq.onsuccess = () => resolve(openreq.result); // First time setup: create an empty object store openreq.onupgradeneeded = () => { openreq.result.createObjectStore(this._storeName); }; }).then((dbp) => { // On close, reconnect dbp.onclose = () => { this._dbp = undefined; }; return dbp; }); } _withIDBStore( type: IDBTransactionMode, callback: (store: IDBObjectStore) => void, ): Promise<void> { this._init(); return this._dbp!.then( (db) => new Promise<void>((resolve, reject) => { const transaction = db.transaction(this.storeName, type); transaction.oncomplete = () => resolve(); // Safari sometimes just rejects with null transaction.onerror = (e) => reject((e.target as IDBTransaction).error ?? new Error('IDB unknown error')); transaction.onabort = () => reject(transaction.error ?? new Error('IDB aborted')); callback(transaction.objectStore(this.storeName)); }), ); } _close(): Promise<void> { this._init(); return this._dbp!.then((db) => { db.close(); this._dbp = undefined; }); } _delete(): Promise<void> { this._close(); return new Promise((resolve, reject) => { const deletereq = indexedDB.deleteDatabase(this._dbName); deletereq.onerror = () => reject(deletereq.error ?? new Error('IDB delete error')); deletereq.onsuccess = () => resolve(); }); } } let store: Store; function getDefaultStore() { if (!store) { store = new Store(); } return store; } export function get<Type>(key: IDBValidKey, store = getDefaultStore()): Promise<Type> { let req: IDBRequest<Type>; return store ._withIDBStore('readonly', (store) => { req = store.get(key) as IDBRequest<Type>; }) .then(() => req.result); } export function set(key: IDBValidKey, value: unknown, store = getDefaultStore()): Promise<void> { return store._withIDBStore('readwrite', (store) => { store.put(value, key); }); } export function del(key: IDBValidKey, store = getDefaultStore()): Promise<void> { return store._withIDBStore('readwrite', (store) => { store.delete(key); }); } export function clear(store = getDefaultStore()): Promise<void> { return store._withIDBStore('readwrite', (store) => { store.clear(); }); } export function keys(store = getDefaultStore()): Promise<IDBValidKey[]> { const keys: IDBValidKey[] = []; return store ._withIDBStore('readonly', (store) => { // This would be store.getAllKeys(), but it isn't supported by Edge or Safari. // And openKeyCursor isn't supported by Safari. (store.openKeyCursor || store.openCursor).call(store).onsuccess = function () { if (!this.result) { return; } keys.push(this.result.key); this.result.continue(); }; }) .then(() => keys); } export function close(store = getDefaultStore()): Promise<void> { return store._close(); } export function deleteDatabase(store = getDefaultStore()): Promise<void> { return store._delete(); } // When the app gets frozen (iOS PWA), close the IDBDatabase connection window.addEventListener('freeze', () => { close(); }); ================================================ FILE: src/app/store/observerMiddleware.ts ================================================ import { Dispatch, isAction, Middleware, MiddlewareAPI } from 'redux'; import { createAction, createCustomAction, isActionOf } from 'typesafe-actions'; import { RootState } from './types'; export interface StoreObserver<T> { /** * A unique string for the observer, this must be globally unique. */ id: string; /** * A custom equality function. It not provided Objest.is will be used. */ equals?: (a: unknown, b: unknown) => boolean; /** * Whether the side effect should be run initially before any state changes are observed. */ runInitially?: boolean; /** * Function to create "something" which will be used to determine if the side effect should run. * Object.is is used for equality by default, so if creating new objects or arrays, provide a * suitable equality function. */ getObserved: (rootState: RootState) => T; /** * Runs the side effect providing both the previous and current version of the derived state. * When the `runInitially` flag is true, previous will be undefined on first run. */ sideEffect: (states: { previous: T | undefined; current: T; rootState: RootState }) => void; } /** * This is needed as the typesafe actions library throws a validation error for the observe action * when created with `createCustomAction`. * See https://github.com/DestinyItemManager/DIM/pull/10195/files#r1438195519 */ function isObserveAction(action: unknown): action is ReturnType<typeof observe> { return ( typeof action === 'object' && action !== null && 'type' in action && action.type === 'observer/OBSERVE' ); } // Need to user a higher order function to get the correct typings and inference, it will now correctly // type the value of T based on what the getObserved returns export const observe = <T>(storeObserver: StoreObserver<T>) => createCustomAction('observer/OBSERVE', (storeObserver: StoreObserver<T>) => ({ storeObserver }))( storeObserver, ); export const unobserve = createAction('observer/UNOBSERVE')<string>(); export const clearObservers = createAction('observer/CLEAR_OBSERVERS')(); export function observerMiddleware<D extends Dispatch>( api: MiddlewareAPI<D, RootState>, ): ReturnType<Middleware> { const observers = new Map<string, StoreObserver<unknown>>(); return (next) => (action) => { // Taken from the redux listener middleware, apparently some actions may not // be action objects, https://github.com/reduxjs/redux-toolkit/blob/0d1f7101e83865714cb512c850bc53ffaee2d5e5/packages/toolkit/src/listenerMiddleware/index.ts#L438-L440 if (!isAction(action)) { return next(action); } if (isObserveAction(action)) { const { storeObserver } = action; observers.set(storeObserver.id, storeObserver); if (storeObserver.runInitially) { storeObserver.sideEffect({ previous: undefined, current: storeObserver.getObserved(api.getState()), rootState: api.getState(), }); } return; } if (isActionOf(unobserve, action)) { observers.delete(action.payload); return; } if (isActionOf(clearObservers, action)) { observers.clear(); return; } const previousRootState = api.getState(); // Forward the state model so that we can compare and look for changes. const result = next(action); const currentRootState = api.getState(); for (const [_id, observer] of observers) { // Grab the slice or derivation the given observer cares about. const previous = observer.getObserved(previousRootState); const current = observer.getObserved(currentRootState); const equals = observer.equals || Object.is; if (!equals(previous, current)) { observer.sideEffect({ previous, current, rootState: currentRootState }); } } return result; }; } ================================================ FILE: src/app/store/reducers.ts ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { clarity } from 'app/clarity/reducer'; import { inGameLoadouts } from 'app/loadout/ingame/reducer'; import { streamDeck } from 'app/stream-deck/reducer'; import { vendors } from 'app/vendors/reducer'; import { Reducer, combineReducers } from 'redux'; import { PayloadAction } from 'typesafe-actions'; import { accounts } from '../accounts/reducer'; import { compare } from '../compare/reducer'; import { DimApiState, dimApi, initialState as dimApiInitialState } from '../dim-api/reducer'; import { farming } from '../farming/reducer'; import { inventory } from '../inventory/reducer'; import { loadouts } from '../loadout/reducer'; import { manifest } from '../manifest/reducer'; import { shell } from '../shell/reducer'; import { wishLists } from '../wishlists/reducer'; import { RootState } from './types'; const reducer: Reducer<RootState> = (state, action) => { const combinedReducers = combineReducers({ accounts, inventory, shell, loadouts, wishLists, farming, manifest, vendors, compare, clarity, inGameLoadouts, streamDeck, // Dummy reducer to get the types to work dimApi: (state: DimApiState = dimApiInitialState) => state, }); const intermediateState = combinedReducers(state, action); // Run the DIM API reducer last, and provide the current account along with it const dimApiState = dimApi( intermediateState.dimApi, action as PayloadAction<any, any>, currentAccountSelector(intermediateState), ); if (intermediateState.dimApi !== dimApiState) { return { ...intermediateState, dimApi: dimApiState, }; } return intermediateState; }; export default reducer; ================================================ FILE: src/app/store/store.ts ================================================ import { applyMiddleware, compose, legacy_createStore as createStore } from 'redux'; import { thunk } from 'redux-thunk'; import { observerMiddleware } from './observerMiddleware'; import allReducers from './reducers'; import { RootState } from './types'; declare global { interface Window { // eslint-disable-next-line @typescript-eslint/method-signature-style __REDUX_DEVTOOLS_EXTENSION_COMPOSE__(options: any): typeof compose; } } const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ serialize: false, actionsBlacklist: ['inventory/UPDATE', 'manifest/D1', 'manifest/D2'], stateSanitizer: (state: RootState) => state.inventory ? { ...state, inventory: '<<EXCLUDED>>', manifest: '<<EXCLUDED>>' } : state, }) : compose; const store = createStore<RootState, any>( allReducers, composeEnhancers(applyMiddleware(observerMiddleware, thunk)), ); // Allow hot-reloading reducers if (module.hot) { module.hot.accept('./reducers', () => { store.replaceReducer(allReducers); }); } export default store; ================================================ FILE: src/app/store/thunk-dispatch.ts ================================================ import { useDispatch } from 'react-redux'; import { DimThunkDispatch } from './types'; /** * A hook to access the redux `dispatch` function, compatible with hooks. This returns a `dispatch` that's typed * correctly for thunk actions. */ export const useThunkDispatch = useDispatch.withTypes<DimThunkDispatch>(); ================================================ FILE: src/app/store/types.ts ================================================ import type { ClarityState } from 'app/clarity/reducer'; import type { StreamDeckState } from 'app/stream-deck/reducer'; import type { VendorsState } from 'app/vendors/reducer'; import type { AnyAction } from 'redux'; import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; import type { AccountsState } from '../accounts/reducer'; import type { CompareState } from '../compare/reducer'; import type { DimApiState } from '../dim-api/reducer'; import type { FarmingState } from '../farming/reducer'; import type { InventoryState } from '../inventory/reducer'; import type { InGameLoadoutState } from '../loadout/ingame/reducer'; import type { LoadoutsState } from '../loadout/reducer'; import type { ManifestState } from '../manifest/reducer'; import type { ShellState } from '../shell/reducer'; import type { WishListsState } from '../wishlists/reducer'; // See https://github.com/piotrwitek/react-redux-typescript-guide#redux export interface RootState { readonly accounts: AccountsState; readonly inventory: InventoryState; readonly shell: ShellState; readonly loadouts: LoadoutsState; readonly wishLists: WishListsState; readonly farming: FarmingState; readonly manifest: ManifestState; readonly vendors: VendorsState; readonly compare: CompareState; readonly streamDeck: StreamDeckState; readonly dimApi: DimApiState; readonly clarity: ClarityState; readonly inGameLoadouts: InGameLoadoutState; } export type ThunkResult<R = void> = ThunkAction<Promise<R>, RootState, undefined, AnyAction>; export type DimThunkDispatch = ThunkDispatch<RootState, undefined, AnyAction>; export interface ThunkDispatchProp { dispatch: DimThunkDispatch; } ================================================ FILE: src/app/store-stats/AccountCurrencies.m.scss ================================================ .icon { height: 16px; width: 16px; } .text { overflow: hidden; text-overflow: ellipsis; } .faded { opacity: 0.7; } ================================================ FILE: src/app/store-stats/AccountCurrencies.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'faded': string; 'icon': string; 'text': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/store-stats/AccountCurrencies.tsx ================================================ import { destinyVersionSelector } from 'app/accounts/selectors'; import BungieImage from 'app/dim-ui/BungieImage'; import { t } from 'app/i18next-t'; import { currenciesSelector } from 'app/inventory/selectors'; import { useD2Definitions } from 'app/manifest/selectors'; import clsx from 'clsx'; import React, { memo } from 'react'; import { useSelector } from 'react-redux'; import * as styles from './AccountCurrencies.m.scss'; /** The account currencies (glimmer, shards, etc.) */ export default memo(function AccountCurrency() { let currencies = useSelector(currenciesSelector); const destinyVersion = useSelector(destinyVersionSelector); const defs = useD2Definitions(); let missingSilver = false; if ( destinyVersion === 2 && defs && !currencies.some((c) => c.itemHash === 3147280338 /* Silver */) ) { const silverDef = defs.InventoryItem.get(3147280338); missingSilver = true; currencies = [ ...currencies, { itemHash: silverDef.hash, quantity: 0, displayProperties: silverDef.displayProperties }, ]; } return currencies.length > 0 ? ( <React.Fragment key={currencies.map((c) => c.itemHash).join()}> {currencies.map((currency) => { const isMissingSilver = missingSilver && currency.itemHash === 3147280338; const title = isMissingSilver ? t('Inventory.MissingSilver') : `${currency.quantity.toLocaleString()} ${currency.displayProperties.name}`; return ( <React.Fragment key={currency.itemHash}> <BungieImage className={clsx(styles.icon, { [styles.faded]: isMissingSilver, })} src={currency.displayProperties.icon} title={title} /> <div className={clsx(styles.text, { [styles.faded]: isMissingSilver, })} title={title} > {isMissingSilver ? '???' : currency.quantity.toLocaleString()} </div> </React.Fragment> ); })} {/* add 0-2 blank slots to keep each currencyGroup rounded to a multiple of 3 (for css grid) */} {Array.from({ length: (3 - (currencies.length % 3)) % 3 }, (_, i) => ( <React.Fragment key={i}> <div /> <div /> </React.Fragment> ))} </React.Fragment> ) : null; }); ================================================ FILE: src/app/store-stats/CharacterStats.m.scss ================================================ @use '../variables' as *; @layer base { :global(.stat) { display: flex; flex-direction: row; align-items: center; line-height: 16px; white-space: nowrap; font-size: 11px; gap: 2px; @include phone-portrait { font-size: 12px; } img { height: 14px; width: 14px; } } } .boostedValue { color: $stat-modded; font-weight: bold; text-shadow: rgb(0, 0, 0, 0.5) 0 0 2px; } .armorStats { composes: flexRow from '../dim-ui/common.m.scss'; justify-content: space-between; align-items: center; width: 100%; } .powerFormula { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; justify-content: space-between; img { opacity: 0.6; height: 24px; width: 24px; } img[src^='data'] { filter: invert(1); } *[role='button'] { margin: -2px -4px; padding: 2px 4px; } > div:nth-child(2) { display: flex; flex-direction: row; &::before, &::after { font-size: 13px; color: var(--theme-header-characters-txt); margin-left: 4px; margin-right: 4px; text-decoration: none !important; } &::before { content: '='; } &::after { content: '+'; } } } .powerStat { font-size: 160%; } .tier { font-weight: bold; } .tooltipFootnote { opacity: 0.6; width: 80%; margin: 10px 0 0 auto; text-align: right; } .richTooltipWrapper { margin: 8px 0 0 0; } .asterisk { vertical-align: top; margin-left: 2px; } .dropLevel { display: flex; justify-content: space-between; } ================================================ FILE: src/app/store-stats/CharacterStats.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'armorStats': string; 'asterisk': string; 'boostedValue': string; 'dropLevel': string; 'powerFormula': string; 'powerStat': string; 'richTooltipWrapper': string; 'tier': string; 'tooltipFootnote': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/store-stats/CharacterStats.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import FractionalPowerLevel from 'app/dim-ui/FractionalPowerLevel'; import { PressTip } from 'app/dim-ui/PressTip'; import { showGearPower } from 'app/gear-power/gear-power'; import { t } from 'app/i18next-t'; import { ArtifactXP } from 'app/inventory/ArtifactXP'; import { ItemPowerSet } from 'app/inventory/ItemPowerSet'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { profileResponseSelector } from 'app/inventory/selectors'; import type { DimCharacterStat, DimStore } from 'app/inventory/store-types'; import { StorePowerLevel, powerLevelSelector } from 'app/inventory/store/selectors'; import { getLoadoutStats } from 'app/loadout-drawer/loadout-utils'; import { getSubclassPlugHashes } from 'app/loadout/loadout-item-utils'; import { Loadout, ResolvedLoadoutItem } from 'app/loadout/loadout-types'; import { useD2Definitions } from 'app/manifest/selectors'; import { getCharacterProgressions } from 'app/progress/selectors'; import { armorStats } from 'app/search/d2-known-values'; import AppIcon from 'app/shell/icons/AppIcon'; import { dimPowerIcon } from 'app/shell/icons/custom/Power'; import { RootState } from 'app/store/types'; import { filterMap, sumBy } from 'app/utils/collections'; import clsx from 'clsx'; import { BucketHashes } from 'data/d2/generated-enums'; import React from 'react'; import { useSelector } from 'react-redux'; import helmetIcon from '../../../destiny-icons/armor_types/helmet.svg'; import xpIcon from '../../images/xpIcon.svg'; import * as styles from './CharacterStats.m.scss'; import StatTooltip from './StatTooltip'; function CharacterPower({ stats }: { stats: PowerStat[] }) { return ( <div className={styles.powerFormula}> {stats.map((stat) => ( <PressTip key={stat.name} tooltip={() => ( <> {stat.name} {stat.problems?.hasClassified && `\n\n${t('Loadouts.Classified')}`} {stat.richTooltipContent && ( <> <hr /> <div className={styles.richTooltipWrapper}>{stat.richTooltipContent()}</div> </> )} </> )} > <div className="stat" aria-label={`${stat.name} ${stat.value}`} role={stat.onClick ? 'button' : 'group'} onClick={stat.onClick} > {typeof stat.icon === 'string' ? <img src={stat.icon} alt={stat.name} /> : stat.icon} <div className={styles.powerStat}> <FractionalPowerLevel power={stat.value} /> </div> {stat.problems?.hasClassified && <sup className={styles.asterisk}>*</sup>} </div> </PressTip> ))} </div> ); } interface PowerStat { value: number; icon: string | React.ReactNode; name: string; richTooltipContent?: () => React.ReactNode; onClick?: () => void; problems?: StorePowerLevel['problems']; } export function PowerFormula({ storeId }: { storeId: string }) { const defs = useD2Definitions(); const powerLevel = useSelector((state: RootState) => powerLevelSelector(state, storeId)); const profileResponse = useSelector(profileResponseSelector); const characterProgress = getCharacterProgressions(profileResponse); if (!defs || !profileResponse || !powerLevel) { return null; } const maxTotalPower: PowerStat = { value: powerLevel.maxTotalPower, icon: <AppIcon icon={dimPowerIcon} />, name: t('Stats.MaxTotalPower'), problems: { ...powerLevel.problems, notOnStore: false }, }; const maxGearPower: PowerStat = { value: powerLevel.maxEquippableGearPower, icon: helmetIcon, name: t('Stats.MaxGearPowerOneExoticRule'), // used to be t('Stats.MaxGearPowerAll') or t('Stats.MaxGearPower'), a translation i don't want to lose yet problems: powerLevel.problems, onClick: () => showGearPower(storeId), richTooltipContent: () => ( <> <ItemPowerSet items={powerLevel.highestPowerItems} powerFloor={Math.floor(powerLevel.maxGearPower)} /> <hr /> <div className={styles.dropLevel}> <span>{t('Stats.DropLevel')}*</span> <span> <FractionalPowerLevel power={powerLevel.dropPower} /> </span> </div> <div className={styles.tooltipFootnote}>* {t('General.ClickForDetails')}</div> </> ), }; // optional chaining here accounts for an edge-case, possible, but type-unadvertised, // missing artifact power bonus. please keep this here. const bonusPowerProgressionHash = profileResponse.profileProgression?.data?.seasonalArtifact?.powerBonusProgression ?.progressionHash; const artifactPower: PowerStat = { value: powerLevel.powerModifier, name: t('Stats.PowerModifier'), richTooltipContent: () => ( <ArtifactXP characterProgress={characterProgress} bonusPowerProgressionHash={bonusPowerProgressionHash} /> ), icon: xpIcon, }; const stats = artifactPower.value ? [maxTotalPower, maxGearPower, artifactPower] : [maxGearPower]; return <CharacterPower stats={stats} />; } /** * Display each of the main stats (Resistance, Discipline, etc) for a character. * This is used for both loadouts and characters - anything that has a character * stats list. This is only used for D2. */ export function CharacterStats({ stats, showTotal, equippedHashes, className, }: { /** * A list of stats to display. This should contain an entry for each stat in * `armorStats`, but if one is missing it won't be shown - you can use this to * show a subset of stats. */ stats: { [hash: number]: DimCharacterStat; }; /** Whether to show the total stat sum of the set. */ showTotal?: boolean; /** * Item hashes for equipped exotics, used to show more accurate cooldown * tooltips. */ equippedHashes: Set<number>; className?: string; }) { // Select only the armor stats, in the correct order const statInfos = filterMap(armorStats, (h) => stats[h]); return ( <div className={clsx(styles.armorStats, className)}> {showTotal && ( <div className={clsx(styles.tier, 'stat')}> {t('LoadoutBuilder.StatTotal', { total: sumBy(statInfos, (s) => s.value) })} </div> )} {statInfos.map((stat) => ( <PressTip key={stat.hash} tooltip={<StatTooltip stat={stat} equippedHashes={equippedHashes} />} > <div className={clsx('stat', { [styles.boostedValue]: stat.breakdown?.some( (change) => change.source === 'runtimeEffect', ), })} aria-label={`${stat.displayProperties.name} ${stat.value}`} role="group" > <BungieImage src={stat.displayProperties.icon} alt={stat.displayProperties.name} /> <div>{stat.value}</div> </div> </PressTip> ))} </div> ); } // TODO: just a plain "show stats" component /** * Show the stats for a DimStore. This is only used for D2 - D1 uses D1CharacterStats. */ export function StoreCharacterStats({ store }: { store: DimStore }) { const equippedItems = store.items.filter((i) => i.equipped); const subclass = equippedItems.find((i) => i.bucket.hash === BucketHashes.Subclass); // All equipped items const equippedHashes = new Set(equippedItems.map((i) => i.hash)); // Plus all subclass mods if (subclass?.sockets) { for (const socket of subclass.sockets.allSockets) { const hash = socket.plugged?.plugDef.hash; if (hash !== undefined) { equippedHashes.add(hash); } } } return <CharacterStats stats={store.stats} equippedHashes={equippedHashes} />; } /** * Show the stats for a DIM Loadout. This is only used for D2. */ // TODO: just take a FullyResolvedLoadout? export function LoadoutCharacterStats({ loadout, subclass, items, allMods, className, }: { loadout: Loadout; subclass?: ResolvedLoadoutItem; allMods: PluggableInventoryItemDefinition[]; items?: (ResolvedLoadoutItem | DimItem)[]; className?: string; }) { const defs = useD2Definitions()!; const equippedItems = items ?.filter((li) => ('loadoutItem' in li ? li.loadoutItem.equip && !li.missing : true)) .map((li) => ('loadoutItem' in li ? li.item : li)) ?? []; // All equipped items const equippedHashes = new Set(equippedItems.map((i) => i.hash)); // Plus all subclass mods for (const { plugHash } of getSubclassPlugHashes(subclass)) { equippedHashes.add(plugHash); } const stats = getLoadoutStats( defs, loadout.classType, subclass, equippedItems, allMods, loadout.parameters?.includeRuntimeStatBenefits ?? true, ); return ( <CharacterStats className={className} showTotal stats={stats} equippedHashes={equippedHashes} /> ); } ================================================ FILE: src/app/store-stats/ClarityCharacterStat.m.scss ================================================ @use '../variables' as *; @use '../dim-ui/tooltip-mixins' as *; .communityInsightSection { @include tooltip-section-color($communityBlue); h3 { opacity: 0.6; margin: 0; font-size: inherit; } table { border-collapse: collapse; width: 100%; } th { font-weight: normal; } th, td { opacity: 0.5; } /* stylelint-disable-next-line no-descending-specificity */ tbody th { text-align: right; padding-right: 6px; opacity: 1; } /* stylelint-disable-next-line no-descending-specificity */ thead th { border-bottom: 1px solid rgb(255, 255, 255, 0.2); &:first-child { border: none; } } img { vertical-align: bottom; margin-right: 4px; } .currentColumn { opacity: 1; } } .unit { font-weight: normal !important; line-height: 12px; font-size: 11px; } .override { display: block; font-size: 11px; } .value { text-align: right; padding-left: 6px; font-variant-numeric: tabular-nums; } ================================================ FILE: src/app/store-stats/ClarityCharacterStat.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'communityInsightSection': string; 'currentColumn': string; 'override': string; 'unit': string; 'value': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/store-stats/ClarityCharacterStat.tsx ================================================ import { ClarityCharacterStats } from 'app/clarity/descriptions/character-stats'; import { clarityCharacterStatsSelector } from 'app/clarity/selectors'; import BungieImage from 'app/dim-ui/BungieImage'; import { Tooltip } from 'app/dim-ui/PressTip'; import { t } from 'app/i18next-t'; import { useD2Definitions } from 'app/manifest/selectors'; import { timerDurationFromMsWithDecimal } from 'app/utils/time'; import { DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { StatHashes } from 'data/d2/generated-enums'; import { JSX } from 'react'; import { useSelector } from 'react-redux'; import * as styles from './ClarityCharacterStat.m.scss'; const statHashToClarityName: { [key: number]: keyof ClarityCharacterStats } = { [StatHashes.Weapons]: 'Mobility', [StatHashes.Health]: 'Resilience', [StatHashes.Class]: 'Recovery', [StatHashes.Super]: 'Intellect', [StatHashes.Grenade]: 'Discipline', [StatHashes.Melee]: 'Strength', }; /** * Use Clarity's database of cooldown info to show extended, accurate cooldown info for equipped subclass mods and exotics. */ export default function ClarityCharacterStat({ statHash, tier, equippedHashes, }: { statHash: number; tier: number; /** * Hashes of equipped/selected items and subclass plugs for this character or loadout. Can be limited to * exotic armor + subclass plugs - make sure to include default-selected subclass plugs. This is used to * determine which cooldowns to show and also to calculate any overrides from aspects or exotics. */ equippedHashes: Set<number>; }) { const defs = useD2Definitions()!; const clarityCharacterStats = useSelector(clarityCharacterStatsSelector); const clarityStatData = clarityCharacterStats?.[statHashToClarityName[statHash]]; if (!clarityStatData) { return null; } const abilityCooldowns: { cooldowns: number[]; item: DestinyInventoryItemDefinition; overrides: DestinyInventoryItemDefinition[]; }[] = []; const applicableOverrides = clarityStatData.Overrides.filter((o) => equippedHashes.has(o.Hash)); const abilitiesList = 'SuperAbilities' in clarityStatData ? clarityStatData.SuperAbilities : clarityStatData.Abilities; for (const a of abilitiesList) { if (!equippedHashes.has(a.Hash)) { continue; } let cooldowns = a.Cooldowns; const item = defs.InventoryItem.get(a.Hash); const overrides = []; // Apply cooldown overrides based on equipped items. for (const o of applicableOverrides) { const abilityIndex = (() => { const abilityIndex = o.Requirements.indexOf(a.Hash); if (abilityIndex !== -1) { return abilityIndex; } const subclassIdx = o.Requirements.findIndex((r) => r < 0 && equippedHashes.has(-r)); if (subclassIdx !== -1) { return subclassIdx; } if (o.Requirements.length === 1 && o.Requirements[0] === 0) { return 0; } return -1; })(); if (abilityIndex !== -1) { if (o.CooldownOverride?.some((v) => v > 0)) { cooldowns = o.CooldownOverride; } const scalar = o.Scalar?.[abilityIndex]; if (scalar) { cooldowns = cooldowns.map((v) => scalar * v); } overrides.push(defs.InventoryItem.get(o.Hash)); } } cooldowns = cooldowns.map((c) => Math.round(c)); abilityCooldowns.push({ cooldowns, item, overrides }); } // Cooldowns that are not about some specific ability const intrinsicCooldowns: JSX.Element[] = []; if ('TotalRegenTime' in clarityStatData) { intrinsicCooldowns.push( <StatTableRow key="TimeToFullHP" name={t('Stats.TimeToFullHP')} cooldowns={clarityStatData.TotalRegenTime.Array} tier={tier} unit="s" />, ); } else if ('WalkSpeed' in clarityStatData) { intrinsicCooldowns.push( <StatTableRow key="WalkingSpeed" name={t('Stats.WalkingSpeed')} cooldowns={clarityStatData.WalkSpeed.Array} tier={tier} unit={t('Stats.MetersPerSecond')} />, <StatTableRow key="StrafingSpeed" name={t('Stats.StrafingSpeed')} cooldowns={clarityStatData.StrafeSpeed.Array} tier={tier} unit={t('Stats.MetersPerSecond')} />, <StatTableRow key="CrouchingSpeed" name={t('Stats.CrouchingSpeed')} cooldowns={clarityStatData.CrouchSpeed.Array} tier={tier} unit={t('Stats.MetersPerSecond')} />, ); } else if ('ShieldHP' in clarityStatData) { intrinsicCooldowns.push( <StatTableRow key="ShieldHP" // t('Stats.TotalHP') // keep this around maybe? name={t('Stats.ShieldHP')} cooldowns={clarityStatData.ShieldHP.Array} tier={tier} unit={t('Stats.HP')} />, <StatTableRow key="DamageResistance" name={t('Stats.DamageResistance')} cooldowns={clarityStatData.PvEDamageResistance.Array} tier={tier} unit={t('Stats.Percentage')} />, <StatTableRow key="FlinchResistance" name={t('Stats.FlinchResistance')} cooldowns={clarityStatData.FlinchResistance.Array} tier={tier} unit={t('Stats.Percentage')} />, ); } if (intrinsicCooldowns.length + abilityCooldowns.length === 0) { return null; } return ( <Tooltip.Section className={styles.communityInsightSection}> <h3>{t('MovePopup.CommunityData')}</h3> <table> <thead> <tr> <th /> {tier - 1 >= 0 && ( <> <th colSpan={2} className={styles.value}> {t('LoadoutBuilder.TierNumber', { tier: tier - 1 })} </th> </> )} <th colSpan={2} className={clsx(styles.value, styles.currentColumn)}> {t('LoadoutBuilder.TierNumber', { tier })} </th> {tier + 1 <= 10 && ( <> <th colSpan={2} className={styles.value}> {t('LoadoutBuilder.TierNumber', { tier: tier + 1 })} </th> </> )} </tr> </thead> <tbody> {abilityCooldowns .sort((a, b) => a.cooldowns[tier] - b.cooldowns[tier]) .map(({ cooldowns, item, overrides }) => ( <StatTableRow key={item.hash} name={item.displayProperties.name} icon={item.displayProperties.icon} cooldowns={cooldowns} tier={tier} overrides={overrides} unit="s" /> ))} {intrinsicCooldowns} </tbody> </table> </Tooltip.Section> ); } function StatTableRow({ name, icon, cooldowns, tier, unit, overrides = [], }: { name: string; icon?: string; unit: string; tier: number; cooldowns: number[]; overrides?: DestinyInventoryItemDefinition[]; }) { const unitEl = <td className={styles.unit}>{unit}</td>; const seconds = unit === 's'; const formatValue = (val: number) => { if (seconds) { return timerDurationFromMsWithDecimal(val * 1000); } return val.toLocaleString(); }; const colspan = seconds ? 2 : 1; return ( <tr> <th> <span> {icon && <BungieImage src={icon} height={16} width={16} />} {name} </span> {overrides.map((o) => ( <span key={o.hash} className={styles.override}> {'+ '} {o.displayProperties.icon && ( <BungieImage src={o.displayProperties.icon} height={12} width={12} /> )} {o.displayProperties.name} </span> ))} </th> {tier - 1 >= 0 && ( <> <td className={styles.value} colSpan={colspan}> {formatValue(cooldowns[tier - 1])} </td> {!seconds && unitEl} </> )} <td className={clsx(styles.value, styles.currentColumn)} colSpan={colspan}> {formatValue(cooldowns[tier])} </td> {!seconds && <td className={clsx(styles.unit, styles.currentColumn)}>{unit}</td>} {tier + 1 <= 10 && ( <> <td className={styles.value} colSpan={colspan}> {formatValue(cooldowns[tier + 1])} </td> {!seconds && unitEl} </> )} </tr> ); } ================================================ FILE: src/app/store-stats/D1CharacterStats.m.scss ================================================ @use '../variables' as *; /* INT/DIS/STR bars */ .statBars { width: 100%; max-width: $emblem-width + 16px; display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 8px; gap: 4px; @include phone-portrait { margin-left: auto; margin-right: auto; } } .stat { display: grid; grid-template-columns: 16px repeat(5, 1fr); gap: 1px; align-items: center; > img { height: 14px; width: 14px; } } .bar { height: 7px; border-radius: 1px; background-color: gray; overflow: hidden; } .progress { height: 100%; background-color: white; &.complete { background-color: #fb9f28; } } ================================================ FILE: src/app/store-stats/D1CharacterStats.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bar': string; 'complete': string; 'progress': string; 'stat': string; 'statBars': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/store-stats/D1CharacterStats.tsx ================================================ import { D1StatHashes } from 'app/destiny1/d1-manifest-types'; import BungieImage from 'app/dim-ui/BungieImage'; import { PressTip } from 'app/dim-ui/PressTip'; import { t } from 'app/i18next-t'; import type { DimCharacterStat, DimStore } from 'app/inventory/store-types'; import { findItemsByBucket } from 'app/inventory/stores-helpers'; import { percent } from 'app/shell/formatters'; import clsx from 'clsx'; import { BucketHashes } from 'data/d2/generated-enums'; import * as styles from './D1CharacterStats.m.scss'; export function D1StoreCharacterStats({ store }: { store: DimStore }) { const subclass = findItemsByBucket(store, BucketHashes.Subclass).find((i) => i.equipped); return <D1CharacterStats stats={store.stats} subclassHash={subclass?.hash} />; } export function D1CharacterStats({ stats, subclassHash, }: { stats: DimStore['stats']; subclassHash?: number; }) { const statList = Object.values(stats); const tooltips = statList.map((stat) => { const tier = Math.floor(Math.min(300, stat.value) / 60); const next = t('Stats.TierProgress', { context: tier === 5 ? 'Max' : '', metadata: { context: ['max'] }, progress: tier === 5 ? stat.value : stat.value % 60, tier, nextTier: tier + 1, statName: stat.displayProperties.name, }); const cooldown = subclassHash ? getAbilityCooldown(subclassHash, stat.hash, tier) : undefined; if (cooldown) { switch (stat.hash) { case D1StatHashes.Intellect: return next + t('Cooldown.Super', { cooldown }); case D1StatHashes.Discipline: return next + t('Cooldown.Grenade', { cooldown }); case D1StatHashes.Strength: return next + t('Cooldown.Melee', { cooldown }); } } return next; }); return ( <div className={styles.statBars}> {statList.map((stat, index) => ( <PressTip key={stat.hash} tooltip={tooltips[index]}> <div className={styles.stat}> <BungieImage src={stat.displayProperties.icon} alt={stat.displayProperties.name} /> {getD1CharacterStatTiers(stat).map((n, index) => ( <div key={index} className={styles.bar}> <div className={clsx(styles.progress, { [styles.complete]: n / 60 === 1, })} style={{ width: percent(n / 60) }} /> </div> ))} </div> </PressTip> ))} </div> ); } function getD1CharacterStatTiers(stat: DimCharacterStat) { const tiers = new Array<number>(5); let remaining = stat.value; for (let t = 0; t < 5; t++) { remaining -= tiers[t] = remaining > 60 ? 60 : remaining; } return tiers; } // Cooldowns const cooldownsSuperA = ['5:00', '4:46', '4:31', '4:15', '3:58', '3:40']; const cooldownsSuperB = ['5:30', '5:14', '4:57', '4:39', '4:20', '4:00']; const cooldownsGrenade = ['1:00', '0:55', '0:49', '0:42', '0:34', '0:25']; const cooldownsMelee = ['1:10', '1:04', '0:57', '0:49', '0:40', '0:29']; // following code is from https://github.com/DestinyTrialsReport function getAbilityCooldown(subclass: number, statHash: D1StatHashes, tier: number) { switch (statHash) { case D1StatHashes.Intellect: switch (subclass) { case 2007186000: // Defender case 4143670656: // Nightstalker case 2455559914: // Striker case 3658182170: // Sunsinger return cooldownsSuperA[tier]; default: return cooldownsSuperB[tier]; } case D1StatHashes.Discipline: return cooldownsGrenade[tier]; case D1StatHashes.Strength: switch (subclass) { case 4143670656: // Nightstalker case 1716862031: // Gunslinger return cooldownsMelee[tier]; default: return cooldownsGrenade[tier]; } default: return ''; } } ================================================ FILE: src/app/store-stats/StatTooltip.m.scss ================================================ @use '../variables' as *; @use '../dim-ui/tooltip-mixins' as *; .title { display: flex; align-items: center; gap: 4px; > img { height: 24px; width: 24px; } > *:last-child { margin-left: auto; } } .value { font-weight: normal; } .row { display: contents; &.boostedValue { color: $stat-modded; } } .icon { width: 14px; height: 14px; padding-left: 2px; padding-right: 2px; } .breakdown { display: grid; grid-template-columns: min-content min-content 1fr min-content; grid-auto-rows: 1fr; } .breakdownValue { justify-self: end; } ================================================ FILE: src/app/store-stats/StatTooltip.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'boostedValue': string; 'breakdown': string; 'breakdownValue': string; 'icon': string; 'row': string; 'title': string; 'value': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/store-stats/StatTooltip.tsx ================================================ import { settingSelector } from 'app/dim-api/selectors'; import BungieImage from 'app/dim-ui/BungieImage'; import { useTooltipCustomization } from 'app/dim-ui/PressTip'; import { DimCharacterStat } from 'app/inventory/store-types'; import { statTier } from 'app/loadout-builder/utils'; import { edgeOfFateReleased, EFFECTIVE_MAX_STAT } from 'app/loadout/known-values'; import clsx from 'clsx'; import { useCallback } from 'react'; import { useSelector } from 'react-redux'; import ClarityCharacterStat from './ClarityCharacterStat'; import * as styles from './StatTooltip.m.scss'; /** * A rich tooltip for character-level stats like Mobility, Intellect, etc. */ export default function StatTooltip({ stat, equippedHashes, }: { stat: DimCharacterStat; /** * Hashes of equipped/selected items and subclass plugs for this character or loadout. Can be limited to * exotic armor + subclass plugs - make sure to include default-selected subclass plugs. */ equippedHashes: Set<number>; }) { const descriptionsToDisplay = useSelector(settingSelector('descriptionsToDisplay')); const useClarityInfo = descriptionsToDisplay !== 'bungie' && !edgeOfFateReleased; useTooltipCustomization({ getHeader: useCallback( () => ( <div className={styles.title}> {stat.displayProperties.icon && <BungieImage src={stat.displayProperties.icon} />} <div>{stat.displayProperties.name}</div> <div className={styles.value}>{`${stat.value} / ${EFFECTIVE_MAX_STAT}`}</div> </div> ), [stat.displayProperties.name, stat.value, stat.displayProperties.icon], ), }); return ( <> <div>{stat.displayProperties.description}</div> {stat.breakdown?.some((contribution) => contribution.source !== 'armorStats') && ( <> <hr /> <div className={styles.breakdown}> {stat.breakdown.map((contribution) => ( <div key={contribution.hash} className={clsx(styles.row, { [styles.boostedValue]: contribution.source === 'runtimeEffect', })} > <span> {contribution.source !== 'armorStats' && contribution.source !== 'subclassPlug' && contribution.count !== undefined && contribution.count > 1 && `${contribution.count}x`} </span> <span> {contribution.icon && <img className={styles.icon} src={contribution.icon} />} </span> <span>{contribution.name}</span> <span className={styles.breakdownValue}> {contribution.source !== 'armorStats' && contribution.value > 0 ? '+' : ''} {contribution.value} </span> </div> ))} </div> </> )} {useClarityInfo && ( <ClarityCharacterStat statHash={stat.hash} tier={statTier(stat.value)} equippedHashes={equippedHashes} /> )} </> ); } ================================================ FILE: src/app/store-stats/StoreStats.m.scss ================================================ @use '../variables.scss' as *; .statContainer { max-width: 230px; margin-top: 7px; gap: 2px; color: var(--theme-header-characters-txt); // When the whole container is hovered, draw a box around any buttons. When // the button itself is hovered, color in the background. This is to make it // easier for folks to understand that there's something here to click. *[role='button'] { position: relative; @include interactive($hover: true, $focus: true, $focusWithin: true) { background-color: var(--theme-item-polaroid-hover-border); background-color: color-mix( in srgb, var(--theme-item-polaroid-hover-border), transparent 80% ); } @include phone-portrait { div > &::after { content: ''; display: block; border-bottom: 1px dashed var(--theme-item-polaroid-hover-border); border-color: color-mix(in srgb, var(--theme-item-polaroid-hover-border), transparent 60%); position: absolute; bottom: 2px; left: 4px; right: 4px; } } @media (any-hover: hover) { div:hover > & { outline: 1px solid var(--theme-item-polaroid-hover-border); outline-color: color-mix(in srgb, var(--theme-item-polaroid-hover-border), transparent 60%); outline-offset: -1px; } } } } .vaultStats { composes: statContainer; display: grid; grid-template-columns: repeat(3, 16px minmax(min-content, 1fr)); grid-auto-rows: 16px; gap: 4px 2px; align-items: center; font-size: 11px; color: var(--theme-header-characters-txt); @include phone-portrait { flex: 1; margin-left: auto; margin-right: auto; } img { justify-self: center; } } .characterStats { composes: statContainer; composes: flexColumn from '../dim-ui/common.m.scss'; width: 100%; @include phone-portrait { margin-left: auto; margin-right: auto; align-items: center; } } .topRow { composes: flexRow from '../dim-ui/common.m.scss'; & > div { flex-basis: 0; flex-grow: 1; } } .setBonuses { --set-bonus-icon-size: 24px; display: flex; flex-direction: row-reverse; } ================================================ FILE: src/app/store-stats/StoreStats.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'characterStats': string; 'setBonuses': string; 'statContainer': string; 'topRow': string; 'vaultStats': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/store-stats/StoreStats.tsx ================================================ import type { DimStore } from 'app/inventory/store-types'; import { useCurrentSetBonus } from 'app/inventory/store/hooks'; import { SetBonusesStatus } from 'app/item-popup/SetBonus'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { PowerFormula, StoreCharacterStats } from '../store-stats/CharacterStats'; import AccountCurrencies from './AccountCurrencies'; import { D1StoreCharacterStats } from './D1CharacterStats'; import * as styles from './StoreStats.m.scss'; import VaultCapacity from './VaultCapacity'; /** Render the store stats for any store type (character or vault) */ export default function StoreStats({ store }: { store: DimStore }) { const isPhonePortrait = useIsPhonePortrait(); const setBonusStatus = useCurrentSetBonus(store.id); return store.isVault ? ( <div className={styles.vaultStats}> <AccountCurrencies /> {!isPhonePortrait && <VaultCapacity />} </div> ) : store.destinyVersion === 1 ? ( <D1StoreCharacterStats store={store} /> ) : ( <div className={styles.characterStats}> <div className={styles.topRow}> <PowerFormula storeId={store.id} /> <div className={styles.setBonuses}> <SetBonusesStatus setBonusStatus={setBonusStatus} store={store} /> </div> </div> <StoreCharacterStats store={store} /> </div> ); } ================================================ FILE: src/app/store-stats/VaultCapacity.m.scss ================================================ @use '../variables.scss' as *; .bucketTag { font-weight: bold; font-size: 14px; line-height: 14px; display: flex; align-items: center; justify-content: center; color: #888; pointer-events: none; img { height: 14px; width: 14px; filter: invert(1); opacity: 0.7; } } .full { color: var(--theme-text); font-weight: bold; } .bucketCount { width: fit-content; margin: -2px -4px -2px -20px; padding: 2px 4px 2px 20px; &[role='button'] { position: relative; @include phone-portrait { &::after { content: ''; display: block; border-bottom: 1px dashed var(--theme-item-polaroid-hover-border); border-color: color-mix(in srgb, var(--theme-item-polaroid-hover-border), transparent 60%); position: absolute; bottom: 2px; left: 4px; right: 4px; } } } } ================================================ FILE: src/app/store-stats/VaultCapacity.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bucketCount': string; 'bucketTag': string; 'full': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/store-stats/VaultCapacity.tsx ================================================ import { PressTip } from 'app/dim-ui/PressTip'; import { InventoryBucket, InventoryBuckets } from 'app/inventory/inventory-buckets'; import { bucketsSelector, currentStoreSelector, vaultSelector } from 'app/inventory/selectors'; import { DimStore } from 'app/inventory/store-types'; import { findItemsByBucket } from 'app/inventory/stores-helpers'; import { MaterialCountsTooltip, showMaterialCount, } from 'app/material-counts/MaterialCountsWrappers'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { compareByIndex } from 'app/utils/comparators'; import { emptyObject } from 'app/utils/empty'; import { LookupTable } from 'app/utils/util-types'; import clsx from 'clsx'; import { BucketHashes } from 'data/d2/generated-enums'; import vaultIcon from 'destiny-icons/armor_types/helmet.svg'; import consumablesIcon from 'destiny-icons/general/consumables.svg'; import modificationsIcon from 'destiny-icons/general/modifications.svg'; import React, { memo } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import * as styles from './VaultCapacity.m.scss'; const bucketIcons: LookupTable<BucketHashes, string> = { [BucketHashes.Modifications]: modificationsIcon, [BucketHashes.Consumables]: consumablesIcon, [BucketHashes.General]: vaultIcon, }; const vaultBucketOrder = [ // D1 3003523923, // Armor 4046403665, // Weapons // D2 BucketHashes.General, BucketHashes.Consumables, BucketHashes.Modifications, ]; /** How many items are in each vault bucket. DIM hides the vault bucket concept from users but needs the count to track progress. */ interface VaultCounts { [bucketHash: string]: { count: number; bucket: InventoryBucket }; } /** * DIM represents items in the vault different from how they actually are - we separate them by inventory bucket as if * the vault were a character, when really they're just big undifferentiated buckets. This re-calculates how full those * buckets are, for display. We could calculate this straight from the profile, but we want to be able to recompute it * when items move without reloading the profile. */ function computeVaultCounts( activeStore: DimStore | undefined, vault: DimStore | undefined, buckets: InventoryBuckets | undefined, ) { if (!activeStore || !vault || !buckets) { return emptyObject<VaultCounts>(); } const vaultCounts: VaultCounts = {}; for (const bucket of Object.values(buckets.byHash)) { // If this bucket can have items placed in the vault, count up how many of // that type are in the vault. if (bucket.vaultBucket) { // D2 has "account wide" buckets that are shared between characters but are // not the vault, and the items in them can *also* be vaulted. We represent // these as being owned by the "current character", and we consider them a // separate type of "vault" for the purposes of vault counts. if (bucket.accountWide) { const vaultBucketId = bucket.hash; vaultCounts[vaultBucketId] ??= { count: 0, bucket, }; vaultCounts[vaultBucketId].count += findItemsByBucket(activeStore, bucket.hash).length; } const vaultBucketId = bucket.vaultBucket.hash; vaultCounts[vaultBucketId] ??= { count: 0, bucket: bucket.accountWide ? bucket : bucket.vaultBucket, }; vaultCounts[vaultBucketId].count += findItemsByBucket(vault, bucket.hash).length; } } return vaultCounts; } const vaultCountsSelector = createSelector( currentStoreSelector, vaultSelector, bucketsSelector, computeVaultCounts, ); /** Current amounts and maximum capacities of the vault */ export default memo(function VaultCapacity() { const vaultCounts = useSelector(vaultCountsSelector); const mats = <MaterialCountsTooltip />; const isPhonePortrait = useIsPhonePortrait(); return ( <> {Object.keys(vaultCounts) .sort(compareByIndex(vaultBucketOrder, (id) => parseInt(id, 10))) .map((bucketIdStr) => { const bucketId = parseInt(bucketIdStr, 10) as BucketHashes; const { count, bucket } = vaultCounts[bucketId]; const isConsumables = bucketId === BucketHashes.Consumables; const title = isConsumables ? undefined : bucket.name; return ( <React.Fragment key={bucketId}> <div className={styles.bucketTag} title={title}> {bucketIcons[bucketId] ? ( <img src={bucketIcons[bucketId]} alt="" /> ) : ( bucket.name.substring(0, 1) )} </div> <PressTip className={styles.bucketCount} tooltip={isConsumables && !isPhonePortrait ? mats : undefined} placement="bottom" role={isConsumables ? 'button' : undefined} wide > <div title={title} className={clsx({ [styles.full]: count === bucket.capacity, })} onClick={isConsumables ? showMaterialCount : undefined} > {count}/{bucket.capacity} </div> </PressTip> </React.Fragment> ); })} </> ); }); ================================================ FILE: src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss ================================================ .icon { margin-right: 8px; } .link { text-decoration: none; } .link span { padding-right: 8px; } ================================================ FILE: src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'icon': string; 'link': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.tsx ================================================ import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import ActionButton from 'app/item-actions/ActionButton'; import { BucketHashes } from 'data/d2/generated-enums'; import streamDeckIcon from 'images/streamDeck.svg'; import { useMemo } from 'react'; import { useStreamDeckSelection } from '../stream-deck'; import * as styles from './OpenOnStreamDeckButton.m.scss'; export default function OpenOnStreamDeckButton({ item, label, type, }: { item: DimItem; label: boolean; type: 'inventory-item' | 'item'; }) { const options = useMemo( () => ({ type, item, }), [item, type], ); const deepLink = useStreamDeckSelection({ options, equippable: !item.notransfer || item.bucket.hash === BucketHashes.Subclass, }); if (!deepLink) { return null; } return ( <a href={deepLink} target="_blank" className={styles.link}> <ActionButton onClick={() => null}> <img src={streamDeckIcon} className={styles.icon} /> {label && <span>{t('MovePopup.OpenOnStreamDeck')}</span>} </ActionButton> </a> ); } ================================================ FILE: src/app/stream-deck/StreamDeckButton/StreamDeckButton.m.scss ================================================ @use '../../variables' as *; @use '../../dim-ui/tooltip-mixins' as *; .streamDeckButton { composes: resetButton from '../../dim-ui/common.m.scss'; position: relative; @container (max-width: 540px) { display: none; } & img { max-width: 19px; margin: 0 8px; margin-top: 2px; height: auto; } } .connected { background-color: #3fe700; // Just for fun - on Safari, on wide color displays, this is a very bright green background-color: lch(81% 132 132); height: 6px; width: 6px; border-radius: 50%; position: absolute; bottom: -1px; right: 5px; } .error { color: $red; padding: 2px; border-radius: 50%; position: absolute; bottom: -5px; right: 1px; :global(.app-icon) { font-size: 10px !important; display: block; } } .tooltipTitle { font-weight: bold; width: 100%; padding-bottom: 4px; margin-bottom: 4px; border-bottom: 1px solid rgb(255, 255, 255, 0.1); } .versionTable { background: rgb(255, 255, 255, 0.05); padding: 2px; width: 100%; margin-top: 4px; margin-bottom: 12px; td { padding: 8px; } td:first-child { font-weight: bold; padding-right: 8px; } tr:first-child td { border-bottom: 1px solid rgb(255, 255, 255, 0.1); } } ================================================ FILE: src/app/stream-deck/StreamDeckButton/StreamDeckButton.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'connected': string; 'error': string; 'streamDeckButton': string; 'tooltipTitle': string; 'versionTable': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/stream-deck/StreamDeckButton/StreamDeckButton.tsx ================================================ import { PressTip } from 'app/dim-ui/PressTip'; import { t } from 'app/i18next-t'; import { AppIcon, banIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import streamDeckIcon from 'images/streamDeck.svg'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { streamDeckSelector } from '../selectors'; import { streamDeckAuthorizationInit } from '../util/authorization'; import { STREAM_DECK_MINIMUM_VERSION, checkStreamDeckVersion } from '../util/version'; import * as styles from './StreamDeckButton.m.scss'; function StreamDeckTooltip({ version, error, needSetup, }: { version?: string; error?: boolean; needSetup?: boolean; }) { return ( <div> <div className={styles.tooltipTitle}>{t('StreamDeck.Tooltip.Title')}</div> {error ? ( <> <p>{t('StreamDeck.Tooltip.Error')}</p> <table className={styles.versionTable}> <tbody> <tr> <td>{t('StreamDeck.Tooltip.Application')}</td> <td>6.5</td> </tr> <tr> <td>{t('StreamDeck.Tooltip.Plugin')}</td> <td>{STREAM_DECK_MINIMUM_VERSION}</td> </tr> </tbody> </table> <div className={styles.tooltipTitle}>{t('StreamDeck.Tooltip.ExtensionIssue')}</div> <p>{t('StreamDeck.Tooltip.ErrorConnection')}</p> </> ) : ( <p> {needSetup ? ( t('StreamDeck.Tooltip.AuthRequired') ) : ( <> <strong>{t('StreamDeck.Tooltip.Version')}</strong> {version} </> )} </p> )} </div> ); } export default function StreamDeckButton() { const { connected, auth } = useSelector(streamDeckSelector); const [version, setVersion] = useState<string | undefined>(undefined); const updateVersion = async () => { try { const resp = await fetch('http://localhost:9120/version', { mode: 'cors', }); const text = await resp.text(); setVersion(text); } catch { setVersion(undefined); } }; useEffect(() => { updateVersion(); }, []); const error = !checkStreamDeckVersion(version); const needSetup = auth === undefined; const dispatch = useThunkDispatch(); return ( <PressTip tooltip={<StreamDeckTooltip version={version} error={error} needSetup={needSetup} />}> <button onClick={() => { updateVersion(); needSetup && dispatch(streamDeckAuthorizationInit()); }} type="button" className={styles.streamDeckButton} title={t('StreamDeck.Tooltip.Title')} > <img src={streamDeckIcon} /> {error ? ( <div className={styles.error}> <AppIcon icon={banIcon} /> </div> ) : ( connected && !needSetup && <div className={styles.connected} /> )} </button> </PressTip> ); } ================================================ FILE: src/app/stream-deck/StreamDeckSettings/StreamDeckSettings.m.scss ================================================ .button { display: flex; gap: 8px; align-items: center; text-decoration: none; } .link { text-decoration: none; } ================================================ FILE: src/app/stream-deck/StreamDeckSettings/StreamDeckSettings.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'button': string; 'link': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/stream-deck/StreamDeckSettings/StreamDeckSettings.tsx ================================================ import { t } from 'app/i18next-t'; import { AppIcon, faArrowCircleDown, faExternalLinkAlt } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import ExternalLink from 'app/dim-ui/ExternalLink'; import Checkbox from 'app/settings/Checkbox'; import { fineprintClass, settingClass } from 'app/settings/SettingsPage'; import { Settings } from 'app/settings/initial-settings'; import { lazyLoadStreamDeck, startStreamDeckConnection, stopStreamDeckConnection, } from 'app/stream-deck/stream-deck'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; import { streamDeckEnabled } from '../actions'; import { streamDeckEnabledSelector } from '../selectors'; import { streamDeckAuthorizationInit } from '../util/authorization'; import * as styles from './StreamDeckSettings.m.scss'; export default function StreamDeckSettings() { const dispatch = useThunkDispatch(); const enabled = useSelector(streamDeckEnabledSelector); const onStreamDeckChange = async (enabled: boolean) => { // on switch toggle set if Stream Deck feature is enabled or no dispatch(streamDeckEnabled(enabled)); // start or stop WebSocket connection if (enabled) { await lazyLoadStreamDeck(); dispatch(startStreamDeckConnection()); } else { dispatch(stopStreamDeckConnection()); } }; return ( <section id="stream-deck"> <h2>Elgato Stream Deck</h2> <div className={settingClass}> <Checkbox name={'streamDeckEnabled' as keyof Settings} label={t('StreamDeck.Enable')} value={enabled} onChange={onStreamDeckChange} /> <div className={fineprintClass}>{t('StreamDeck.FinePrint')}</div> <div> {!enabled ? ( <ExternalLink className={styles.link} href="https://marketplace.elgato.com/product/dim-stream-deck-11883ba5-c8db-4e3a-915f-612c5ba1b2e4" > <button type="button" className={clsx('dim-button', styles.button)}> <AppIcon icon={faArrowCircleDown} ariaHidden /> {t('StreamDeck.Install')} </button> </ExternalLink> ) : ( <button type="button" className={clsx('dim-button', styles.button)} onClick={() => dispatch(streamDeckAuthorizationInit())} > <i className={faExternalLinkAlt} /> <span>{t('StreamDeck.Authorize')}</span> </button> )} </div> </div> </section> ); } ================================================ FILE: src/app/stream-deck/actions.ts ================================================ import { createAction } from 'typesafe-actions'; export type SelectionType = 'item' | 'loadout' | 'inventory-item' | 'postmaster' | undefined; export interface StreamDeckAuth { instance: string; token: string; } /** * Change WebSocket status to connected (true) */ export const streamDeckConnected = createAction('stream-deck/CONNECTED')(); /** * Change WebSocket status to disconnected (false) */ export const streamDeckDisconnected = createAction('stream-deck/DISCONNECTED')(); /** * Update the selection type */ export const streamDeckSelection = createAction('stream-deck/SELECTION')<SelectionType>(); /** * Update the authorization */ export const streamDeckAuthorization = createAction('stream-deck/AUTHORIZATION')<StreamDeckAuth>(); /** * Update the plugin status */ export const streamDeckEnabled = createAction('stream-deck/ENABLED')<boolean>(); ================================================ FILE: src/app/stream-deck/async-module.ts ================================================ // async module import { t } from 'app/i18next-t'; import { currentStoreSelector } from 'app/inventory/selectors'; import { showNotification } from 'app/notifications/notifications'; import { observe, unobserve } from 'app/store/observerMiddleware'; import { RootState, ThunkResult } from 'app/store/types'; import { streamDeckConnected, streamDeckDisconnected } from 'app/stream-deck/actions'; import { SendToStreamDeckArgs, StreamDeckMessage } from 'app/stream-deck/interfaces'; import { handleStreamDeckMessage } from 'app/stream-deck/msg-handlers'; import { errorMessage } from 'app/utils/errors'; import { reportException } from 'app/utils/sentry'; import useSelection from './useStreamDeckSelection'; import { character, equippedItems, inventoryCounters, maxPower, metrics, postmaster, vault, } from './util/packager'; const STREAM_DECK_FARMING_OBSERVER_ID = 'stream-deck-farming-observer'; const STREAM_DECK_INVENTORY_OBSERVER_ID = 'stream-deck-inventory-observer'; let websocket: WebSocket; export async function sendToStreamDeck(msg: SendToStreamDeckArgs) { if (websocket?.readyState === WebSocket.OPEN) { websocket.send( JSON.stringify({ ...msg, }), ); } } let errorNotified = false; // collect and send data to the stream deck function refreshStreamDeck(state: RootState) { if (websocket.readyState === WebSocket.OPEN) { try { const store = currentStoreSelector(state); store && sendToStreamDeck({ action: 'state', data: { character: character(store), postmaster: postmaster(store), metrics: metrics(state), vault: vault(state), inventory: inventoryCounters(state), maxPower: maxPower(store, state), equippedItems: equippedItems(store), }, }); } catch (e) { reportException('streamdeck', e); if (!errorNotified) { showNotification({ type: 'error', title: t('StreamDeck.Error.Title'), body: t('StreamDeck.Error.Body', { error: errorMessage(e) }), duration: 10000, }); errorNotified = true; } } } } // stop the websocket's connection with the local stream deck instance function stop(): ThunkResult { return async (dispatch) => { websocket?.close(); dispatch(streamDeckDisconnected()); }; } // observe farming mode/inventory changes function registerObservers(): ThunkResult { return async (dispatch) => { // farming mode dispatch( observe({ id: STREAM_DECK_FARMING_OBSERVER_ID, runInitially: true, getObserved: (rootState) => rootState.farming.storeId, sideEffect: ({ current }) => { sendToStreamDeck({ action: 'farmingMode', data: Boolean(current), }); }, }), ); // inventory dispatch( observe({ id: STREAM_DECK_INVENTORY_OBSERVER_ID, runInitially: true, getObserved: (rootState) => rootState.inventory, sideEffect: ({ rootState }) => refreshStreamDeck(rootState), }), ); }; } // start the websocket's connection with the local stream deck instance function start(): ThunkResult { return async (dispatch, getState) => { const initWS = () => { const state = getState(); // if settings/manifest/profile are not loaded retry after 1s if ( !state.dimApi.globalSettingsLoaded || !state.manifest.destiny2CoreSettings || !state.inventory.profileResponse?.profileProgression ) { window.setTimeout(initWS, 1000); return; } const { enabled, auth } = state.streamDeck; // if stream deck is disabled stop and don't try to connect if (!enabled) { return; } // close the existing websocket if connected if (websocket?.readyState !== WebSocket.CLOSED) { websocket?.close(); } // if the plugin is enabled but the auth is not set stop if (!auth) { return; } // try to connect to the stream deck local instance websocket = new WebSocket(`ws://localhost:9120/${auth.instance}`); websocket.onopen = () => { // update the connection status dispatch(streamDeckConnected()); // register the observers dispatch(registerObservers()); }; websocket.onclose = () => { dispatch(streamDeckDisconnected()); // if the plugin is still enabled and the websocket is closed if (enabled && websocket.readyState === WebSocket.CLOSED) { // retry to re-connect after 2.5s window.setTimeout(initWS, 2500); } // unregister the observers dispatch(unobserve(STREAM_DECK_FARMING_OBSERVER_ID)); dispatch(unobserve(STREAM_DECK_INVENTORY_OBSERVER_ID)); }; websocket.onmessage = ({ data }) => { dispatch( handleStreamDeckMessage(JSON.parse(data as string) as StreamDeckMessage, auth.token), ); }; websocket.onerror = () => websocket.close(); }; initWS(); }; } // async module loaded in ./stream-deck.ts using lazy import export default { start, stop, useSelection, }; ================================================ FILE: src/app/stream-deck/interfaces.ts ================================================ import { DimStore } from 'app/inventory/store-types'; import { RootState, ThunkResult } from 'app/store/types'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { SelectionType } from './actions'; // trigger a pre-written search // choose a specific page (inventory, vendors, records, etc..) // choose if highlight items only or move search items to current store export interface SearchAction { action: 'search'; query: string; page: string; append?: boolean; pullItems?: boolean; sendToVault?: boolean; } // randomize the current character // both modes (weapon only / all) export interface RandomizeAction { action: 'randomize'; weaponsOnly: boolean; } // collect all items from postmaster export interface CollectPostmasterAction { action: 'collectPostmaster'; } // trigger refresh DIM export interface RefreshAction { action: 'refresh'; } // trigger refresh DIM export interface RequestPickerItemsAction { action: 'requestPickerItems'; device: string; query: string; } // enable/disable farming mode export interface FarmingModeAction { action: 'toggleFarmingMode'; } // maximize power export interface MaxPowerAction { action: 'equipMaxPower'; } // pull a selected item from other character/vault // (if the current character has already that item it will be moved to the vault) export interface PullItemAction { action: 'pullItem'; itemId: string; type: 'equip' | 'pull' | 'vault'; } // equip a selected loadout (for a specific store) // send the shareable link of a loadout to the Stream Deck export interface EquipLoadoutAction { action: 'equipLoadout'; loadout: string; character?: string; } // set the selection to item/loadout/postmaster export interface SelectionAction { action: 'selection'; type?: SelectionType; } // request perks definitions export interface RequestPerksAction { action: 'requestPerks'; } // | FreeBucketSlotAction export type StreamDeckMessage = ( | SearchAction | RandomizeAction | CollectPostmasterAction | RefreshAction | FarmingModeAction | MaxPowerAction | PullItemAction | EquipLoadoutAction | RequestPickerItemsAction | RequestPerksAction | SelectionAction ) & { token?: string }; // Types of messages sent to Stream Deck interface VaultArgs { vault: number; shards?: number; glimmer?: number; brightDust?: number; } interface MetricsArgs { gunsmith: number; triumphs: number; triumphsActive: number; battlePass: number; artifactIcon?: string; } interface PostmasterArgs { total: number; ascendantShards: number; enhancementPrisms: number; spoils: number; } interface MaxPowerArgs { artifact: number; base: string; total: string; } interface Character { icon: string; class: DestinyClass; background: string; } interface SendStateArgs { action: 'state'; data?: { character?: Character; postmaster?: PostmasterArgs; maxPower?: MaxPowerArgs; equippedItems?: string[]; metrics?: MetricsArgs; /** * @deprecated replaced by`inventory`. */ vault?: VaultArgs; inventory?: Record<string, number>; }; } interface SendFarmingModeArgs { action: 'farmingMode'; data: boolean; } interface SendPerksArgs { action: 'perks'; data: { title: string; image: string; }[]; } interface SendPickerItemsArgs { action: 'pickerItems'; data: { device: string; items: { item: string; icon: string; tier?: number; overlay?: string; isExotic?: boolean; element?: string; }[]; }; } export type SendToStreamDeckArgs = | SendStateArgs | SendFarmingModeArgs | SendPickerItemsArgs | SendPerksArgs; export interface HandlerArgs<T> { msg: T; state: RootState; store: DimStore; } type ActionMatching<TAction> = Extract<StreamDeckMessage, { action: TAction }>; export type MessageHandler = { [key in StreamDeckMessage['action']]: (args: HandlerArgs<ActionMatching<key>>) => ThunkResult; }; ================================================ FILE: src/app/stream-deck/msg-handlers.ts ================================================ // async module // serialize the data and send it if connected import { currentAccountSelector } from 'app/accounts/selectors'; import { startFarming, stopFarming } from 'app/farming/actions'; import { t } from 'app/i18next-t'; import { moveItemTo } from 'app/inventory/move-item'; import { allItemsSelector, currentStoreSelector, storesSelector, vaultSelector, } from 'app/inventory/selectors'; import { getStore } from 'app/inventory/stores-helpers'; import { itemMoveLoadout, maxLightLoadout, randomLoadout } from 'app/loadout-drawer/auto-loadouts'; import { applyLoadout } from 'app/loadout-drawer/loadout-apply'; import { pullFromPostmaster } from 'app/loadout-drawer/postmaster'; import { applyInGameLoadout } from 'app/loadout/ingame/ingame-loadout-apply'; import { allInGameLoadoutsSelector } from 'app/loadout/ingame/selectors'; import { loadoutsSelector } from 'app/loadout/loadouts-selector'; import { showNotification } from 'app/notifications/notifications'; import { accountRoute } from 'app/routes'; import { filterFactorySelector } from 'app/search/items/item-search-filter'; import { setRouterLocation, setSearchQuery } from 'app/shell/actions'; import { refresh } from 'app/shell/refresh-events'; import { RootState, ThunkResult } from 'app/store/types'; import { CollectPostmasterAction, EquipLoadoutAction, FarmingModeAction, HandlerArgs, MaxPowerAction, MessageHandler, PullItemAction, RandomizeAction, RequestPickerItemsAction, SearchAction, SelectionAction, StreamDeckMessage, } from 'app/stream-deck/interfaces'; import { delay } from 'app/utils/promises'; import { DamageType } from 'bungie-api-ts/destiny2'; import { streamDeckSelection } from './actions'; import { sendToStreamDeck } from './async-module'; import { perks, streamDeckClearId } from './util/packager'; // Calc location path function routeTo(state: RootState, path: string) { const account = currentAccountSelector(state); return account ? `${accountRoute(account)}/${path}` : undefined; } function refreshHandler(): ThunkResult { return async () => { refresh(); }; } function requestPickerItemsHandler({ msg, state, }: HandlerArgs<RequestPickerItemsAction>): ThunkResult { return async () => { const items = searchItems(state, msg.query); items.sort((a, b) => a.name.localeCompare(b.name)); sendToStreamDeck({ action: 'pickerItems', data: { device: msg.device, items: items.map((item) => ({ label: item.name, item: streamDeckClearId(item.index), icon: item.icon, tier: item.tier, overlay: item.iconOverlay, isExotic: item.isExotic, isCrafted: Boolean(item.crafted), element: item.element?.enumValue === DamageType.Kinetic ? undefined : item.element?.displayProperties?.icon, })), }, }); }; } function searchItems(state: RootState, query: string) { const allItems = allItemsSelector(state); const filter = filterFactorySelector(state)(query); return allItems.filter((i) => filter(i)); } function searchHandler({ msg, state, store }: HandlerArgs<SearchAction>): ThunkResult { return async (dispatch, getState) => { const searchOnly = !msg.pullItems && !msg.sendToVault; if (searchOnly) { // change page if needed if (!window.location.pathname.endsWith(msg.page)) { dispatch(setRouterLocation(routeTo(state, msg.page || 'inventory'))); // delay a bit to trigger the search await delay(250); } let query = state.shell.searchQuery; if (msg.append) { query += ` ${msg.query}`; } else if (query !== msg.query) { query = msg.query; } else { query = ''; } // update the search query dispatch(setSearchQuery(query)); } else if (msg.query) { // reset any previous search dispatch(setSearchQuery('')); // find items const searchedItems = searchItems(getState(), msg.query); // skip action if no items found if (searchedItems.length === 0) { return; } // move items to the vault or current store const targetStore = msg.sendToVault ? vaultSelector(state) : store; if (targetStore) { const loadout = itemMoveLoadout(searchedItems, targetStore); await dispatch(applyLoadout(targetStore, loadout, { allowUndo: true })); } } }; } // TODO move to a shared module function randomizeHandler({ msg, state, store }: HandlerArgs<RandomizeAction>): ThunkResult { return async (dispatch) => { const allItems = allItemsSelector(state); const loadout = randomLoadout( store, allItems, msg.weaponsOnly ? (i) => i.bucket?.sort === 'Weapons' : () => true, ); loadout && (await dispatch(applyLoadout(store, loadout, { allowUndo: true }))); }; } function collectPostmasterHandler({ store }: HandlerArgs<CollectPostmasterAction>): ThunkResult { return async (dispatch) => dispatch(pullFromPostmaster(store)); } // TODO move to a shared module function maximizePowerHandler({ state, store }: HandlerArgs<MaxPowerAction>): ThunkResult { return async (dispatch) => { const allItems = allItemsSelector(state); const loadout = maxLightLoadout(allItems, store); return dispatch(applyLoadout(store, loadout, { allowUndo: true })); }; } function farmingModeHandler({ state, store }: HandlerArgs<FarmingModeAction>): ThunkResult { return async (dispatch) => { if (state.farming.storeId) { return dispatch(stopFarming()); } else { return dispatch(startFarming(store?.id)); } }; } function equipLoadoutHandler({ msg, state }: HandlerArgs<EquipLoadoutAction>): ThunkResult { return async (dispatch) => { const stores = storesSelector(state); const store = msg.character ? getStore(stores, msg.character) : currentStoreSelector(state); if (!store) { return; } // In Game Loadouts if (msg.loadout.startsWith('ingame')) { const loadouts = allInGameLoadoutsSelector(state); const loadout = loadouts.find((it) => it.id === msg.loadout); return loadout && dispatch(applyInGameLoadout(loadout)); } // DIM Loadouts const loadouts = loadoutsSelector(state); const loadout = loadouts.find((it) => it.id === msg.loadout); return loadout && dispatch(applyLoadout(store, loadout, { allowUndo: true })); }; } function pullItemHandler({ msg, state, store }: HandlerArgs<PullItemAction>): ThunkResult { return async (dispatch) => { const allItems = allItemsSelector(state); const [item] = allItems.filter((it) => it.index.startsWith(msg.itemId)); const targetStore = msg.type === 'vault' ? vaultSelector(state) : store; const shouldEquip = msg.type === 'equip'; if (targetStore) { await dispatch(moveItemTo(item, targetStore, shouldEquip, item.amount)); } }; } function selectionHandler({ msg }: HandlerArgs<SelectionAction>): ThunkResult { return async (dispatch) => { dispatch(streamDeckSelection(msg.type)); }; } function requestPerksHandler(): ThunkResult { return async (_, getState) => { sendToStreamDeck({ action: 'perks', data: perks(getState()), }); }; } const handlers: MessageHandler = { refresh: refreshHandler, search: searchHandler, randomize: randomizeHandler, collectPostmaster: collectPostmasterHandler, equipMaxPower: maximizePowerHandler, toggleFarmingMode: farmingModeHandler, equipLoadout: equipLoadoutHandler, pullItem: pullItemHandler, requestPickerItems: requestPickerItemsHandler, selection: selectionHandler, requestPerks: requestPerksHandler, }; // handle actions coming from the stream deck instance export function handleStreamDeckMessage(msg: StreamDeckMessage, token: string): ThunkResult { return async (dispatch, getState) => { const state = getState(); const store = currentStoreSelector(state); if (!msg.token || msg.token !== token) { showNotification({ type: 'error', title: 'Stream Deck', body: t('StreamDeck.MissingAuthorization'), }); throw new Error(!msg.token ? 'missing-token' : 'invalid-token'); } if (store) { // handle stream deck actions const handler = handlers[msg.action] as (args: HandlerArgs<StreamDeckMessage>) => ThunkResult; dispatch(handler?.({ msg, state, store })); } }; } ================================================ FILE: src/app/stream-deck/reducer.ts ================================================ import { Reducer } from 'redux'; import { ActionType, getType } from 'typesafe-actions'; import * as actions from './actions'; // Redux Store Stream Deck State export interface StreamDeckState { // WebSocket status readonly enabled: boolean; // WebSocket status readonly connected: boolean; // Authorization readonly auth?: actions.StreamDeckAuth; // Selection type readonly selection?: actions.SelectionType; } type StreamDeckAction = ActionType<typeof actions>; const auth = localStorage.getItem('stream-deck-auth') ?? ''; const enabled = localStorage.getItem('stream-deck-enabled') === 'true'; // initial stream deck store state const streamDeckInitialState: StreamDeckState = { enabled, connected: false, selection: undefined, auth: auth ? (JSON.parse(auth) as actions.StreamDeckAuth) : undefined, }; export const streamDeck: Reducer<StreamDeckState, StreamDeckAction> = ( state: StreamDeckState = streamDeckInitialState, action: StreamDeckAction, ): StreamDeckState => { switch (action.type) { case getType(actions.streamDeckConnected): return { ...state, connected: true, }; case getType(actions.streamDeckDisconnected): return { ...state, connected: false, }; case getType(actions.streamDeckSelection): return { ...state, selection: action.payload, }; case getType(actions.streamDeckAuthorization): localStorage.setItem('stream-deck-auth', JSON.stringify(action.payload)); return { ...state, auth: action.payload, }; case getType(actions.streamDeckEnabled): localStorage.setItem('stream-deck-enabled', action.payload.toString()); return { ...state, enabled: action.payload, }; default: return state; } }; ================================================ FILE: src/app/stream-deck/selectors.ts ================================================ import { RootState } from 'app/store/types'; export const streamDeckSelector = (state: RootState) => state.streamDeck; export const streamDeckEnabledSelector = (state: RootState) => state.streamDeck.enabled; export const streamDeckSelectionSelector = (state: RootState) => state.streamDeck.selection; ================================================ FILE: src/app/stream-deck/stream-deck.ts ================================================ import { ThunkResult } from 'app/store/types'; import { type UseStreamDeckSelectionFn } from './useStreamDeckSelection'; export interface LazyStreamDeck { start?: () => ThunkResult; stop?: () => ThunkResult; useSelection?: UseStreamDeckSelectionFn; } const lazyLoaded: LazyStreamDeck = {}; // lazy load the stream deck module when needed export const lazyLoadStreamDeck = async () => { const core = await import(/* webpackChunkName: "streamdeck" */ './async-module'); // load only once if (!lazyLoaded.start) { Object.assign(lazyLoaded, { ...core.default, }); } }; // wrapped lazy loaded functions export const startStreamDeckConnection = () => lazyLoaded.start!(); export const stopStreamDeckConnection = () => lazyLoaded.stop!(); export const useStreamDeckSelection: UseStreamDeckSelectionFn = (...args) => lazyLoaded.useSelection?.(...args); ================================================ FILE: src/app/stream-deck/useStreamDeckSelection.ts ================================================ import { LoadoutItem } from '@destinyitemmanager/dim-api-types'; import { DimItem } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { InGameLoadout, Loadout } from 'app/loadout/loadout-types'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { RootState } from 'app/store/types'; import { DamageType, DestinyClass } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { useSelector } from 'react-redux'; import { streamDeckSelectionSelector } from './selectors'; import { STREAM_DECK_DEEP_LINK } from './util/authorization'; import { streamDeckClearId } from './util/packager'; export type StreamDeckSelectionOptions = | { type: 'in-game-loadout'; loadout: InGameLoadout; } | { type: 'loadout'; loadout: Loadout; store: DimStore; } | { type: 'item'; item: DimItem; } | { type: 'inventory-item'; item: DimItem; }; function findSubClassIcon(items: LoadoutItem[], state: RootState) { const defs = d2ManifestSelector(state); for (const item of items) { const def = defs?.InventoryItem.get(item.hash); // find subclass item if (def?.inventory?.bucketTypeHash === BucketHashes.Subclass) { return def.displayProperties.icon; } } } const toSelection = (data: StreamDeckSelectionOptions) => (state: RootState) => { switch (data.type) { case 'in-game-loadout': { const { loadout } = data; return { type: 'loadout', loadout: loadout.id, label: loadout.name, character: loadout.characterId, 'inGameIcon.icon': loadout.icon, 'inGameIcon.background': loadout.colorIcon, }; } case 'loadout': { const isAnyClass = data.loadout.classType === DestinyClass.Unknown; const { loadout, store } = data; return { type: 'loadout', loadout: loadout.id, label: loadout.name.toUpperCase(), subtitle: (isAnyClass ? '' : store.className) || loadout.notes || '-', character: isAnyClass ? undefined : store.id, icon: findSubClassIcon(loadout.items, state), }; } case 'item': { const { item } = data; return { type: 'item', label: item.name, subtitle: item.typeName, item: streamDeckClearId(item.index), tier: item.tier, icon: item.icon, overlay: item.iconOverlay, isExotic: item.isExotic, isSubClass: item.bucket.hash === BucketHashes.Subclass, isCrafted: Boolean(item.crafted), element: item.element?.enumValue === DamageType.Kinetic ? undefined : item.element?.displayProperties?.icon, }; } case 'inventory-item': { const { item } = data; return { type: 'inventory-item', label: item.name, subtitle: item.typeName, item: streamDeckClearId(item.index), icon: item.icon, isExotic: item.isExotic, }; } } }; const toSelectionHref = (canSelect: boolean, data: StreamDeckSelectionOptions) => (state: RootState) => { if (!canSelect) { return; } const params = toSelection(data)(state); const query = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined) { query.set(key, value as string); } } return `${STREAM_DECK_DEEP_LINK}/selection?${query.toString()}`; }; export interface UseStreamDeckSelectionArgs { options: StreamDeckSelectionOptions; equippable: boolean | undefined; } const types = { item: 'item', loadout: 'loadout', 'in-game-loadout': 'in-game-loadout', 'inventory-item': 'inventory-item', }; function useSelection({ equippable, options }: UseStreamDeckSelectionArgs): string | undefined { const type = types[options.type]; const selection = useSelector(streamDeckSelectionSelector); const canSelect = Boolean((equippable || type === 'inventory-item') && selection === type); return useSelector(toSelectionHref(canSelect, options)); } export default useSelection; export type UseStreamDeckSelectionFn = typeof useSelection; ================================================ FILE: src/app/stream-deck/util/authorization.ts ================================================ import { ThunkResult } from 'app/store/types'; import { streamDeckAuthorization } from '../actions'; import { startStreamDeckConnection, stopStreamDeckConnection } from '../stream-deck'; export const STREAM_DECK_DEEP_LINK = 'streamdeck://plugins/message/com.dim.streamdeck'; export function streamDeckAuthorizationInit(): ThunkResult { return async (dispatch) => { dispatch(stopStreamDeckConnection()); const auth = { instance: globalThis.crypto.randomUUID(), token: globalThis.crypto.randomUUID(), }; dispatch(streamDeckAuthorization(auth)); const query = new URLSearchParams(auth).toString(); window.open(`${STREAM_DECK_DEEP_LINK}/connect?${query}`); dispatch(startStreamDeckConnection()); }; } ================================================ FILE: src/app/stream-deck/util/packager.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import { allItemsSelector, currenciesSelector, vaultSelector } from 'app/inventory/selectors'; import { AccountCurrency, DimStore } from 'app/inventory/store-types'; import { findItemsByBucket, getArtifactBonus } from 'app/inventory/stores-helpers'; import { maxLightItemSet } from 'app/loadout-drawer/auto-loadouts'; import { getLight } from 'app/loadout-drawer/loadout-utils'; import { totalPostmasterItems } from 'app/loadout-drawer/postmaster'; import { currentSeasonPassHashSelector, d2ManifestSelector } from 'app/manifest/selectors'; import { getCharacterProgressions } from 'app/progress/selectors'; import { RootState } from 'app/store/types'; import { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; // find and get the quantity of a specif item type function getPostMasterItem(lostItems: DimItem[], hash: number) { return lostItems.find((it) => it.location.inPostmaster && it.hash === hash)?.amount || 0; } // find and get the quantity of a specif currency function getCurrency(currencies: AccountCurrency[], hash: number) { return currencies.find((curr) => curr.itemHash === hash)?.quantity; } // create the postmaster update data export function postmaster(store: DimStore) { const items = findItemsByBucket(store, BucketHashes.LostItems); return { total: totalPostmasterItems(store), ascendantShards: getPostMasterItem(items, 4257549985), enhancementPrisms: getPostMasterItem(items, 4257549984), spoils: getPostMasterItem(items, 3702027555), }; } // create the max power update data export function maxPower(store: DimStore, state: RootState) { const allItems = allItemsSelector(state); const maxLight = getLight(store, maxLightItemSet(allItems, store).equippable); const artifact = getArtifactBonus(store); return { total: (maxLight + artifact).toFixed(0), base: maxLight.toFixed(0), artifact, }; } // create the vault update data export function vault(state: RootState) { const vault = vaultSelector(state); if (!vault) { return; } const currencies = currenciesSelector(state); return { vault: vault.items.length, shards: getCurrency(currencies, 1022552290), glimmer: getCurrency(currencies, 3159615086), brightDust: getCurrency(currencies, 2817410917), }; } // seasonal hash from src/app/progress/Milestones.tsx function getCurrentSeason( state: RootState, profile: DestinyProfileResponse | undefined, ): [number?, number?, string?] { const defs = d2ManifestSelector(state); const currentSeasonPassHash = currentSeasonPassHashSelector(state); const season = profile?.profile?.data?.currentSeasonHash ? defs?.Season.get(profile.profile.data.currentSeasonHash) : undefined; const seasonPass = currentSeasonPassHash ? defs?.SeasonPass.get(currentSeasonPassHash) : undefined; if (!season) { return []; } return [ seasonPass?.rewardProgressionHash, seasonPass?.prestigeProgressionHash, season.artifactItemHash ? defs?.InventoryItem.get(season.artifactItemHash).displayProperties.icon : undefined, ]; } // create the metrics update data export function metrics(state: RootState) { const profile = state.inventory.profileResponse; const progression = getCharacterProgressions(profile)?.progressions ?? {}; const { lifetimeScore, activeScore } = profile?.profileRecords?.data || {}; const [battlePassHash, prestigeLevel, artifactIcon] = getCurrentSeason(state, profile); // battle pass level calc from src/app/progress/SeasonalRank.tsx const seasonProgress = progression[battlePassHash!]; const prestigeProgress = progression[prestigeLevel!]; const prestigeMode = seasonProgress?.level === seasonProgress?.levelCap; const seasonalRank = prestigeMode ? prestigeProgress?.level + seasonProgress?.levelCap : seasonProgress?.level; return { gunsmith: progression[1471185389]?.currentProgress ?? 0, triumphs: lifetimeScore ?? 0, triumphsActive: activeScore ?? 0, battlePass: battlePassHash ? seasonalRank : 0, artifactIcon, }; } export function streamDeckClearId(id: string) { return id.replace(/-.*/, ''); } export function equippedItems(store?: DimStore) { return store?.items.filter((it) => it.equipment).map((it) => streamDeckClearId(it.index)) ?? []; } export function inventoryCounters(state?: RootState) { return state?.inventory.stores .flatMap((it) => it.items) .filter((it) => it.bucket.inInventory) .reduce( (acc, it) => { const key = streamDeckClearId(it.index); if (acc[key]) { acc[key] += it.amount; } else { acc[key] = it.amount; } return acc; }, {} as Record<string, number>, ); } export function character(store: DimStore) { return { class: store.classType, icon: store.icon, background: store.background, }; } const PerksCategory = [3708671066, 1052191496]; const WeaponsHashes = [ BucketHashes.KineticWeapons, BucketHashes.EnergyWeapons, BucketHashes.PowerWeapons, ]; interface PerkDefinition { title: string; image: string; } export function perks(state: RootState) { const perks = new Map<string, PerkDefinition>(); const items = allItemsSelector(state); for (const item of items) { if (item.isExotic || WeaponsHashes.every((hash) => item.bucket.hash !== hash)) { continue; } const sockets = item.sockets?.allSockets; if (!sockets) { continue; } for (const socket of Object.values(sockets)) { if ( socket.isMod || !PerksCategory.some((hash) => socket.plugged?.plugDef?.itemCategoryHashes?.includes(hash)) ) { continue; } const plug = socket.plugged?.plugDef.displayProperties; if (!plug) { continue; } const definition = { title: plug.name, image: plug.icon, }; if (!perks.has(definition.title)) { perks.set(definition.title, definition); } } } return Array.from(perks.values()); } ================================================ FILE: src/app/stream-deck/util/version.ts ================================================ export const STREAM_DECK_MINIMUM_VERSION = '3.1.0'; const [minMajor, minMinor, minPatch] = STREAM_DECK_MINIMUM_VERSION.split('.'); export const checkStreamDeckVersion = (version: string | undefined) => { if (!version) { return false; } const [major, minor, patch] = version.split('.'); if (major < minMajor) { return false; } if (minor < minMinor) { return false; } return patch >= minPatch; }; ================================================ FILE: src/app/strip-sockets/StripSockets.m.scss ================================================ @use '../variables' as *; .insertButton { composes: dim-button from global; display: flex; flex-direction: row; align-items: center; transition: none; gap: 8px; } .stripSheet { max-width: 500px; margin: 0 auto; } .noSocketsMessage { margin: 2px 0 2px 8px; font-size: 14px; } .socketKindButton { border: 1px solid #666; cursor: pointer; margin: 1px 2px; padding: 8px; display: flex; flex-direction: row; height: 100%; box-sizing: border-box; @include interactive($hover: true) { background-color: rgb(255, 255, 255, 0.1); } > * { margin-right: 8px; } &:focus { outline: 1px solid var(--theme-accent-secondary); } } .buttonInfo { flex: 1; display: flex; flex-direction: column; } .buttonTitle { font-weight: bold; font-size: 13px; margin-bottom: 4px; &.selectedTitle { color: var(--theme-accent-primary); } } .selectedButton { border-color: var(--theme-accent-primary); &:focus { outline: none; } } .itemTypeIcon { filter: invert(1); width: 18px; height: 18px; vertical-align: bottom; } .iconList { --item-size: 32px; display: flex; flex-flow: row wrap; margin: 8px 8px; &:last-child { margin-bottom: 0; } > * { margin: 0 4px 4px 0; } } .plug { cursor: pointer; &.ok { opacity: 0.3; transform: scale(0.8); } &.failed { outline: 2px solid $red; } } ================================================ FILE: src/app/strip-sockets/StripSockets.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttonInfo': string; 'buttonTitle': string; 'failed': string; 'iconList': string; 'insertButton': string; 'itemTypeIcon': string; 'noSocketsMessage': string; 'ok': string; 'plug': string; 'selectedButton': string; 'selectedTitle': string; 'socketKindButton': string; 'stripSheet': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/strip-sockets/StripSockets.tsx ================================================ import { PressTip } from 'app/dim-ui/PressTip'; import Sheet from 'app/dim-ui/Sheet'; import { I18nKey, t, tl } from 'app/i18next-t'; import { locateItem } from 'app/inventory/locate-item'; import { destiny2CoreSettingsSelector, useD2Definitions } from 'app/manifest/selectors'; import { filterFactorySelector } from 'app/search/items/item-search-filter'; import { AppIcon, faCheckCircle, refreshIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { withCancel } from 'app/utils/cancel'; import clsx from 'clsx'; import chestArmorItem from 'destiny-icons/armor_types/chest.svg'; import ghostIcon from 'destiny-icons/general/ghost.svg'; import handCannonIcon from 'destiny-icons/weapons/hand_cannon.svg'; import { produce } from 'immer'; import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { useSelector } from 'react-redux'; import { useSubscription } from 'use-subscription'; import { DefItemIcon } from '../inventory/ItemIcon'; import { allItemsSelector } from '../inventory/selectors'; import * as styles from './StripSockets.m.scss'; import { SocketKind, StripAction, collectSocketsToStrip, doStripSockets } from './strip-sockets'; import { stripSocketsQuery$ } from './strip-sockets-actions'; const i18nKeys: NodeJS.Dict<I18nKey> = { shaders: tl('StripSockets.Shaders'), ornaments: tl('StripSockets.Ornaments'), weaponmods: tl('StripSockets.WeaponMods'), armormods: tl('StripSockets.ArmorMods'), discountedmods: tl('StripSockets.DiscountedMods'), subclass: tl('StripSockets.Subclass'), others: tl('StripSockets.Others'), }; /** * Error, OK, or still in our worklist */ type SocketState = string | 'ok' | 'todo'; type State = | { tag: 'selecting'; } | { tag: 'processing'; cancel: () => void; cancelling: boolean; socketList: StripAction[]; socketStates: SocketState[]; } | { tag: 'done'; socketList: StripAction[]; socketStates: SocketState[]; }; type UIAction = | { tag: 'cancel_process'; } | { tag: 'confirm_process'; socketList: StripAction[]; cancel: () => void; } | { tag: 'confirm_results'; } | { tag: 'notify_done'; success: boolean; } | { tag: 'notify_progress'; idx: number; error: string | undefined; }; function reducer(state: State, action: UIAction): State { switch (action.tag) { case 'cancel_process': if (state.tag === 'processing') { state.cancel(); return produce(state, (draft) => { draft.tag === 'processing' && (draft.cancelling = true); }); } else if (state.tag === 'done') { return { tag: 'selecting' }; } break; case 'confirm_process': if (state.tag === 'selecting') { return { tag: 'processing', cancel: action.cancel, cancelling: false, socketList: action.socketList, socketStates: Array<string>(action.socketList.length).fill('todo'), }; } break; case 'confirm_results': if (state.tag === 'done') { return { tag: 'selecting' }; } break; case 'notify_done': if (state.tag === 'processing') { if (action.success) { // completed -- show (varying) success return { tag: 'done', socketList: state.socketList, socketStates: state.socketStates, }; } else { // cancelled -- go back to selection return { tag: 'selecting' }; } } break; case 'notify_progress': if (state.tag === 'processing') { return produce(state, (draft) => { draft.tag === 'processing' && (draft.socketStates[action.idx] = action.error ?? 'ok'); }); } break; } return state; } export default function StripSockets() { const dispatch = useThunkDispatch(); const [state, stripDispatch] = useReducer(reducer, { tag: 'selecting' }); const [selectedSockets, setSelectedSockets] = useState<StripAction[]>([]); const query = useSubscription(stripSocketsQuery$); const isChoosing = state.tag === 'selecting'; const onCancel = () => { stripDispatch({ tag: 'cancel_process' }); }; const onConfirmSockets = useCallback( async (selectedSockets: StripAction[]) => { if (!isChoosing) { return; } const [cancelToken, cancel] = withCancel(); stripDispatch({ tag: 'confirm_process', socketList: selectedSockets, cancel }); try { await dispatch( doStripSockets(selectedSockets, cancelToken, (idx, error) => stripDispatch({ tag: 'notify_progress', idx, error }), ), ); stripDispatch({ tag: 'notify_done', success: true }); } catch { stripDispatch({ tag: 'notify_done', success: false }); } }, [dispatch, isChoosing], ); if (!query) { return null; } const header = ( <div> <h1> {isChoosing ? ( t('StripSockets.Choose') ) : state.tag === 'processing' ? ( <> <span> <AppIcon icon={refreshIcon} spinning={true} ariaHidden /> </span>{' '} {t('StripSockets.Running')} </> ) : ( t('StripSockets.Done') )} </h1> </div> ); let contents, footer; if (state.tag === 'selecting') { contents = <StripSocketsChoose query={query} reportSockets={setSelectedSockets} />; footer = ( <button type="button" className={styles.insertButton} onClick={() => onConfirmSockets(selectedSockets)} disabled={selectedSockets.length === 0} > <span> <AppIcon icon={faCheckCircle} ariaHidden />{' '} {t('StripSockets.Button', { numSockets: selectedSockets.length })} </span> </button> ); } else { // state is processing or done, so show the plug list contents = ( <StripSocketsProcess socketList={state.socketList} socketStates={state.socketStates} /> ); if (state.tag === 'processing') { footer = ( <button type="button" className={styles.insertButton} onClick={onCancel} disabled={state.cancelling} > <span>{t('StripSockets.Cancel')}</span> </button> ); } else { footer = ( <> <button type="button" className={styles.insertButton} onClick={() => stripDispatch({ tag: 'confirm_results' })} > <span>{t('StripSockets.Ok')}</span> </button> </> ); } } return ( <Sheet onClose={() => { onCancel(); stripSocketsQuery$.next(undefined); }} header={header} footer={footer} sheetClassName={styles.stripSheet} > {contents} </Sheet> ); } function StripSocketsProcess({ socketList, socketStates, }: { socketList: StripAction[]; socketStates: SocketState[]; }) { return ( <div className={styles.iconList}> {socketList.map((socket, idx) => { const state = socketStates[idx]; const icon = ( <div onClick={() => locateItem(socket.item)}> <DefItemIcon itemDef={socket.plugItemDef} /> </div> ); const failed = state !== 'ok' && state !== 'todo'; const key = `${socket.item.index}-${socket.socketIndex}`; const className = clsx('item', styles.plug, { [styles.ok]: state === 'ok', [styles.failed]: failed, }); return failed ? ( <PressTip minimal key={key} className={className} tooltip={state}> {icon} </PressTip> ) : ( <div key={key} className={className}> {icon} </div> ); })} </div> ); } function StripSocketsChoose({ query, reportSockets, }: { query: string; reportSockets: (sockets: StripAction[]) => void; }) { const defs = useD2Definitions()!; const destiny2CoreSettings = useSelector(destiny2CoreSettingsSelector)!; const allItems = useSelector(allItemsSelector); const filterFactory = useSelector(filterFactorySelector); const [activeKinds, setActiveKinds] = useState<SocketKind[]>([]); const socketKinds = useMemo(() => { if (!query) { return null; } const filterFunc = filterFactory(query); const filteredItems = allItems.filter((i) => i.sockets && filterFunc(i)); return collectSocketsToStrip(filteredItems, destiny2CoreSettings, defs); }, [allItems, defs, destiny2CoreSettings, filterFactory, query]); useEffect(() => { reportSockets( socketKinds?.filter((k) => k.items && activeKinds.includes(k.kind)).flatMap((k) => k.items) || [], ); }, [reportSockets, socketKinds, activeKinds]); return ( socketKinds && ( <> {socketKinds.length ? ( socketKinds.map( ({ kind, representativePlug, numWeapons, numArmor, numOthers, numApplicableSockets, }) => { const selected = activeKinds.includes(kind); const itemCats = [ { icon: handCannonIcon, num: numWeapons }, { icon: chestArmorItem, num: numArmor }, { icon: ghostIcon, num: numOthers }, ]; const labelKey = i18nKeys[kind]; const label = (labelKey && t(labelKey, { count: numApplicableSockets })) || `${numApplicableSockets}x ${representativePlug.itemTypeDisplayName}`; const onClick = () => { if (activeKinds.includes(kind)) { setActiveKinds(activeKinds.filter((k) => k !== kind)); } else { setActiveKinds([...activeKinds, kind]); } }; return ( <div key={kind} className={clsx(styles.socketKindButton, { [styles.selectedButton]: selected, })} onClick={onClick} role="button" tabIndex={0} > <div className="item" title={label}> <DefItemIcon itemDef={representativePlug} /> </div> <div className={styles.buttonInfo}> <div className={clsx(styles.buttonTitle, { [styles.selectedTitle]: selected, })} > {label} </div> </div> <div> {itemCats.map( ({ icon, num }, idx) => num > 0 && ( <React.Fragment key={idx}> <img src={icon} className={styles.itemTypeIcon} /> {num} <br /> </React.Fragment> ), )} </div> </div> ); }, ) ) : ( <div className={styles.noSocketsMessage}>{t('StripSockets.NoSockets')}</div> )} </> ) ); } ================================================ FILE: src/app/strip-sockets/strip-sockets-actions.ts ================================================ import { Observable } from 'app/utils/observable'; /** * The currently active search query that the Strip Sockets dialog (sheet) * is working with in its "selecting sockets" state. */ export const stripSocketsQuery$ = new Observable<string | undefined>(undefined); /** * Show the "Strip Sockets" dialog (sheet). */ export function stripSockets(query: string) { stripSocketsQuery$.next(query); } ================================================ FILE: src/app/strip-sockets/strip-sockets.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { canInsertPlug, insertPlug } from 'app/inventory/advanced-write-actions'; import { DimItem, DimSocket, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { isReducedModCostVariant } from 'app/loadout/mod-utils'; import { DEFAULT_ORNAMENTS } from 'app/search/d2-known-values'; import { ThunkResult } from 'app/store/types'; import { CancelToken } from 'app/utils/cancel'; import { count, uniqBy } from 'app/utils/collections'; import { errorMessage } from 'app/utils/errors'; import { Destiny2CoreSettings } from 'bungie-api-ts/core'; import { ItemCategoryHashes, PlugCategoryHashes } from 'data/d2/generated-enums'; export interface StripAction { item: DimItem; socketIndex: number; plugItemDef: PluggableInventoryItemDefinition; } /** * Groups up strippable sockets into some bigger categories like 'ornaments'. * * Add whatever return string you want to this, and optionally add a label to the * i18nKeys lookup in StripSocket.tsx. If it's not reflected there, the toggle button * will display the itemTypeDisplayName from an example strippable plug. */ function identifySocket(socket: DimSocket, plugDef: PluggableInventoryItemDefinition) { if (plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.Shaders)) { return 'shaders'; } else if (DEFAULT_ORNAMENTS.includes(socket.emptyPlugItemHash!)) { return 'ornaments'; } else if (plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.WeaponModsDamage)) { return 'weaponmods'; } else if (plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.ArmorMods)) { if (isReducedModCostVariant(plugDef.hash)) { return 'discountedmods'; } return 'armormods'; } else if ( plugDef.plug.plugCategoryHash === PlugCategoryHashes.WeaponTieringKillVfx || plugDef.plug.plugCategoryHash === PlugCategoryHashes.V900weaponModConfetti ) { return 'combatflair'; } else if (plugDef.plug.plugCategoryHash === PlugCategoryHashes.Hologram) { return 'others'; } // This could handle subclass options (fragments, aspects) but it wasn't quite clear // if they'd be useful, so they're intentionally left out here. } /** A made-up socket classification. */ export type SocketKind = NonNullable<ReturnType<typeof identifySocket>>; export function collectSocketsToStrip( filteredItems: DimItem[], destiny2CoreSettings: Destiny2CoreSettings | undefined, defs: D2ManifestDefinitions, ) { const socketsByKind: { [kind in SocketKind]?: StripAction[]; } = {}; for (const item of filteredItems) { for (const socket of item.sockets!.allSockets) { if ( socket.emptyPlugItemHash && socket.plugged && socket.plugged.plugDef.hash !== socket.emptyPlugItemHash && canInsertPlug(socket, socket.emptyPlugItemHash, destiny2CoreSettings, defs) ) { const plugDef = socket.plugged.plugDef; const kind = identifySocket(socket, plugDef); if (kind) { (socketsByKind[kind] ??= []).push({ item, socketIndex: socket.socketIndex, plugItemDef: plugDef, }); } } } } const socketKinds = []; for (const kind in socketsByKind) { const items = socketsByKind[kind as keyof typeof socketsByKind]!; const affectedItems = uniqBy(items, (i) => i.item.id); const numApplicableItems = affectedItems.length; const numWeapons = count(affectedItems, (i) => i.item.bucket.inWeapons); const numArmor = count(affectedItems, (i) => i.item.bucket.inArmor); const numOthers = numApplicableItems - numWeapons - numArmor; const numApplicableSockets = items.length; if (numApplicableSockets > 0) { // Choose a socket that would be cleared by this "kind button" and // show the current plug as a large icon for that button. // This immediately presents an example for what would happen if the user // decided to strip sockets of this kind. const representativePlug = items.at(-1)!.plugItemDef; socketKinds.push({ kind: kind as keyof typeof socketsByKind, items, representativePlug, numWeapons, numArmor, numOthers, numApplicableSockets, }); } } return socketKinds; } export function doStripSockets( socketList: StripAction[], cancelToken: CancelToken, progressCallback: (idx: number, errorMsg: string | undefined) => void, ): ThunkResult { return async (dispatch) => { for (let i = 0; i < socketList.length; i++) { cancelToken.checkCanceled(); const entry = socketList[i]; try { const socket = entry.item.sockets!.allSockets.find( (i) => i.socketIndex === entry.socketIndex, )!; await dispatch(insertPlug(entry.item, socket, socket.emptyPlugItemHash!)); progressCallback(i, undefined); } catch (e) { progressCallback(i, errorMessage(e)); } } }; } ================================================ FILE: src/app/themes/_theme-classic.scss ================================================ // Classic DIM theme .theme-classic { // App --theme-app-bg: #313233; // Background color for (currently only) Settings page sections. Distinguishes them from theme-app-bg. --theme-section-bg: #222; // App background gradient pattern --theme-app-bg-gradient: var(--theme-app-bg); // The color of PWA elements like the title bar. This cannot be a gradient. --theme-pwa-background: #000; // Header --theme-header-nav-bg: #000; --theme-header-characters-bg: var(--theme-app-bg); // Organizer --theme-organizer-row-odd-bg: var(--theme-app-bg); --theme-organizer-row-even-bg: #252627; // Background color for the search bar. Distinguishes it from theme-header-nav-bg. --theme-search-bg: var(--theme-app-bg); } ================================================ FILE: src/app/themes/_theme-dimdark.scss ================================================ // Basic DIM dark Theme .theme-dimdark { // Base colours --theme-accent-primary: #d56520; // Interaction styles (hover, active, focus) --theme-accent-secondary: #68a0b7; // Secondary interactions // Generic fills for modal backgrounds (see below) --theme-fill-modal: #0d0f14; // Base fill for popups, drawers, dropdowns etc --theme-fill-modal-actions: #161616; // Layered/contextual modals (search, item actions) // Text --theme-text: #efefef; --theme-text-invert: #efefef; --theme-text-secondary: #aaa; // Buttons --theme-button-bg: rgb(255, 255, 255, 0.1); --theme-button-bg-primary: rgb(255, 255, 255, 0.2); --theme-button-text: var(--theme-text); // Input field (text boxes and text areas) --theme-input-bg: #333; // Shadows --theme-drop-shadow: 0 0 18px 0 #000; // Tooltips --theme-tooltip-underline: transparent; --theme-tooltip-header-bg: #0a0a0f; --theme-tooltip-body-bg: #151523; --theme-tooltip-border: #8e8e9e; --theme-tooltip-minimal-bg: #5d5970; // App background gradient pattern --theme-app-bg-gradient-start: #1e222a; --theme-app-bg-gradient-end: #1e222a; --theme-app-bg-gradient: radial-gradient( circle at 50% 70px, var(--theme-app-bg-gradient-start) 0%, var(--theme-app-bg-gradient-end) 100% ); // App --theme-app-bg: var(--theme-app-bg-gradient); // Background color for (currently only) Settings page sections. Distinguishes them from theme-app-bg. --theme-section-bg: var(--theme-search-bg); // The color of PWA elements like the title bar. This cannot be a gradient. --theme-pwa-background: #1e222a; // Header --theme-header-nav-bg: var(--theme-app-bg-gradient); --theme-header-nav-slideout-menu-bg: var(--theme-fill-modal); --theme-header-characters-bg: var(--theme-app-bg-gradient); --theme-header-characters-txt: var(--theme-text); // Dropdown menu --theme-dropdown-menu-bg: var(--theme-fill-modal); // Search // Background color for the search bar. Distinguishes it from theme-header-nav-bg. --theme-search-bg: rgb(0, 0, 0, 0.4); --theme-search-dropdown-bg: var(--theme-fill-modal-actions); // Items --theme-item-polaroid: #000; --theme-item-polaroid-txt: var(--theme-text); --theme-item-polaroid-hover-border: #ddd; --theme-item-polaroid-masterwork: #745700; --theme-item-polaroid-masterwork-txt: var(--theme-text); --theme-item-polaroid-capped: #745700; --theme-item-polaroid-capped-txt: var(--theme-text); --theme-item-polaroid-godroll: var(--theme-text); --theme-item-polaroid-trashroll: var(--theme-text); --theme-item-polaroid-element-adjust-brightness: 100%; // Adjust brightness level of Arc, Strand, Void icons // Item popup --theme-item-popup-border: #222; --theme-item-popup-arrow: var(--theme-fill-modal-actions); --theme-item-popup-actions-bg: var(--theme-fill-modal-actions); --theme-item-popup-panel-bg: #333; // Panels for progress (masterwork, crafted, catalyst) and archetype --theme-item-popup-progress-bar-bg: #222; // Item feed --theme-item-feed-bg: var(--theme-fill-modal); // Item sheet (compare, armory) --theme-item-sheet-bg: var(--theme-fill-modal); // Organizer --theme-organizer-row-odd-bg: #11151c; --theme-organizer-row-even-bg: #1e222a; // Records --theme-record-redeemed: #c4b578; } ================================================ FILE: src/app/themes/_theme-europa.scss ================================================ // Europa theme .theme-europa { // Base colours --theme-accent-primary: #88cbed; // Interaction styles (hover, active, focus) --theme-accent-secondary: #f27b8a; // Secondary interactions // Generic fills for modal backgrounds (see below) --theme-fill-modal: #1d2b5e; // Base fill for popups, drawers, dropdowns etc --theme-fill-modal-actions: #0b2432; // Layered/contextual modals (search, item actions) // Text --theme-text: #e8f3f7; --theme-text-invert: #beeeff; --theme-text-secondary: #a9bcc3; // Buttons --theme-button-bg: rgb(255, 255, 255, 0.2); --theme-button-text: white; // Input field (text boxes and text areas) --theme-input-bg: #333; // Shadows --theme-drop-shadow: 0 0 18px 0 #000; // Tooltips --theme-tooltip-underline: var(--theme-accent-primary); --theme-tooltip-header-bg: #000; --theme-tooltip-body-bg: #1a1a1a; --theme-tooltip-border: #555; --theme-tooltip-minimal-bg: #5d5970; // App background gradient pattern --theme-app-bg-gradient-start: #5376ba; --theme-app-bg-gradient-end: #2a418f; --theme-app-bg-gradient: radial-gradient( circle at 50% 70px, var(--theme-app-bg-gradient-start) 0%, var(--theme-app-bg-gradient-end) 100% ); // App --theme-app-bg: var(--theme-app-bg-gradient); // Background color for (currently only) Settings page sections. Distinguishes them from theme-app-bg. --theme-section-bg: var(--theme-search-bg); // The color of PWA elements like the title bar. This cannot be a gradient. --theme-pwa-background: #1d2b5e; --theme-mobile-background: #1d2b5e; // Header --theme-header-nav-bg: #1d2b5e; --theme-header-nav-slideout-menu-bg: var(--theme-fill-modal); --theme-header-characters-bg: #2a418f; --theme-header-characters-txt: rgb(255, 255, 255, 0.625); // Dropdown menu --theme-dropdown-menu-bg: var(--theme-fill-modal); // Search // Background color for the search bar. Distinguishes it from theme-header-nav-bg. --theme-search-bg: rgb(255, 255, 255, 0.15); --theme-search-dropdown-bg: var(--theme-fill-modal-actions); --theme-search-dropdown-border: rgb(255, 255, 255, 0.2); // Items 'polaroid' framed icons --theme-item-polaroid: #fff; --theme-item-polaroid-txt: black; --theme-item-polaroid-masterwork: #88cbed; --theme-item-polaroid-masterwork-txt: #000; --theme-item-polaroid-capped: #f2a5ce; --theme-item-polaroid-capped-txt: #be0a99; --theme-item-polaroid-godroll: #0b486b; --theme-item-polaroid-trashroll: #d14334; // Item popup --theme-item-popup-border: #333; --theme-item-popup-arrow: #333; --theme-item-popup-actions-bg: var(--theme-fill-modal-actions); --theme-item-popup-panel-bg: rgb( 255, 255, 255, 0.06 ); // Panels for progress (masterwork, crafted, catalyst) and archetype --theme-item-popup-progress-bar-bg: rgb(0, 0, 0, 0.3); // Item feed --theme-item-feed-bg: var(--theme-fill-modal); // Item sheet (compare, armory) --theme-item-sheet-bg: var(--theme-fill-modal); // Organizer --theme-organizer-row-odd-bg: #2b5483; --theme-organizer-row-even-bg: #2e3d69; // Records --theme-record-redeemed: #17d0d2; // More vibrant P3 colourspace tints for displays that support them @supports (color: color(display-p3 1 1 1)) { --theme-accent-primary: oklch(71% 0.18 235.97); } } ================================================ FILE: src/app/themes/_theme-neomuna.scss ================================================ // Neomuna theme .theme-neomuna { // Base colours --theme-accent-primary: #fe2dde; // Interaction styles (hover, active, focus) --theme-accent-secondary: #f27b8a; // Secondary interactions // Generic fills for modal backgrounds (see below) --theme-fill-modal: #0b2432; // Base fill for popups, drawers, dropdowns etc --theme-fill-modal-actions: #0b2432; // Layered/contextual modals (search, item actions) // Text --theme-text: #e8f3f7; --theme-text-invert: #beeeff; --theme-text-secondary: #87adba; // Buttons --theme-button-bg: rgb(255, 255, 255, 0.2); --theme-button-text: white; // Input field (text boxes and text areas) --theme-input-bg: #333; // Shadows --theme-drop-shadow: 0 0 18px 0 #000; // Tooltips --theme-tooltip-underline: var(--theme-accent-primary); --theme-tooltip-header-bg: #000; --theme-tooltip-body-bg: #1a1a1a; --theme-tooltip-border: #555; --theme-tooltip-minimal-bg: #5d5970; // App background gradient pattern --theme-app-bg-gradient-start: #1f78ac; --theme-app-bg-gradient-end: #2d1137; --theme-app-bg-gradient: radial-gradient( circle at 50% 70px, var(--theme-app-bg-gradient-start) 0%, var(--theme-app-bg-gradient-end) 100% ); // App --theme-app-bg: var(--theme-app-bg-gradient); // Background color for (currently only) Settings page sections. Distinguishes them from theme-app-bg. --theme-section-bg: var(--theme-search-bg); // The color of PWA elements like the title bar. This cannot be a gradient. --theme-pwa-background: #0b2432; --theme-mobile-background: #0b2432; // Header --theme-header-nav-bg: #0b2432; --theme-header-nav-slideout-menu-bg: var(--theme-fill-modal); --theme-header-characters-bg: #10364d; --theme-header-characters-txt: rgb(255, 255, 255, 0.625); // Dropdown menu --theme-dropdown-menu-bg: var(--theme-fill-modal); // Search // Background color for the search bar. Distinguishes it from theme-header-nav-bg. --theme-search-bg: rgb(255, 255, 255, 0.15); --theme-search-dropdown-bg: var(--theme-fill-modal-actions); --theme-search-dropdown-border: rgb(255, 255, 255, 0.2); // Items 'polaroid' framed icons --theme-item-polaroid: #00c2e3; --theme-item-polaroid-txt: black; --theme-item-polaroid-masterwork: #c9009e; --theme-item-polaroid-masterwork-txt: #dec1d8; --theme-item-polaroid-capped: #ebb920; --theme-item-polaroid-capped-txt: #000; --theme-item-polaroid-godroll: white; --theme-item-polaroid-trashroll: #d14334; --theme-item-polaroid-element-adjust-brightness: 120%; // Adjust brightness level of Arc, Strand, Void icons // Item popup --theme-item-popup-border: #333; --theme-item-popup-arrow: #333; --theme-item-popup-actions-bg: var(--theme-fill-modal-actions); --theme-item-popup-panel-bg: rgb( 255, 255, 255, 0.06 ); // Panels for progress (masterwork, crafted, catalyst) and archetype --theme-item-popup-progress-bar-bg: rgb(0, 0, 0, 0.3); // Item feed --theme-item-feed-bg: var(--theme-fill-modal); // Item sheet (compare, armory) --theme-item-sheet-bg: var(--theme-fill-modal); // Organizer --theme-organizer-row-odd-bg: #2b5483; --theme-organizer-row-even-bg: #2e3d69; // Records --theme-record-redeemed: #ee77ef; // More vibrant P3 colourspace tints for displays that support them @supports (color: color(display-p3 1 1 1)) { --theme-accent-primary: oklch(61% 0.29 336.78); --theme-item-polaroid: oklch(74% 0.15 213.27); --theme-item-polaroid-masterwork: oklch(55% 0.26 340.71); } } ================================================ FILE: src/app/themes/_theme-pyramid.scss ================================================ // Pyramid Fleet theme .theme-pyramid { // Base colours --theme-accent-primary: #e34400; // Interaction styles (hover, active, focus) --theme-accent-secondary: #68a0b7; // Secondary interactions // Generic fills for modal backgrounds (see below) --theme-fill-modal: #000; // Base fill for popups, drawers, dropdowns etc --theme-fill-modal-actions: #161616; // Layered/contextual modals (search, item actions) // Text --theme-text: #ccc; --theme-text-invert: #fff; --theme-text-secondary: #777; // Buttons --theme-button-bg: rgb(255, 255, 255, 0.2); --theme-button-text: white; // Input field (text boxes and text areas) --theme-input-bg: #333; // Shadows --theme-drop-shadow: 0 0 18px 0 #000; // Tooltips --theme-tooltip-underline: var(--theme-accent-primary); --theme-tooltip-header-bg: #000; --theme-tooltip-body-bg: #1a1a1a; --theme-tooltip-border: #555; --theme-tooltip-minimal-bg: #5d5970; // App background gradient pattern --theme-app-bg-gradient-start: #1a1a1a; --theme-app-bg-gradient-end: #111; --theme-app-bg-gradient: radial-gradient( circle at 50% 70px, var(--theme-app-bg-gradient-start) 0%, var(--theme-app-bg-gradient-end) 100% ); // App --theme-app-bg: var(--theme-app-bg-gradient); // Background color for (currently only) Settings page sections. Distinguishes them from theme-app-bg. --theme-section-bg: var(--theme-search-bg); // The color of PWA elements like the title bar. This cannot be a gradient. --theme-pwa-background: #000; // Header --theme-header-nav-bg: black; --theme-header-nav-slideout-menu-bg: var(--theme-fill-modal); --theme-header-characters-bg: #101010; --theme-header-characters-txt: rgb(255, 255, 255, 0.625); // Dropdown menu --theme-dropdown-menu-bg: var(--theme-fill-modal); // Search // Background color for the search bar. Distinguishes it from theme-header-nav-bg. --theme-search-bg: rgb(255, 255, 255, 0.15); --theme-search-dropdown-bg: var(--theme-fill-modal-actions); --theme-search-dropdown-border: rgb(255, 255, 255, 0.2); // Items 'polaroid' framed icons --theme-item-polaroid: #456; --theme-item-polaroid-txt: white; --theme-item-polaroid-masterwork: #d19b00; --theme-item-polaroid-masterwork-txt: #000; --theme-item-polaroid-capped: #f55656; --theme-item-polaroid-capped-txt: #fff; --theme-item-polaroid-godroll: #fff; --theme-item-polaroid-trashroll: #d14334; --theme-item-polaroid-element-adjust-brightness: 85%; // Adjust brightness level of Arc, Strand, Void icons // Item popup --theme-item-popup-border: #333; --theme-item-popup-arrow: #333; --theme-item-popup-actions-bg: var(--theme-fill-modal-actions); --theme-item-popup-panel-bg: rgb( 255, 255, 255, 0.06 ); // Panels for progress (masterwork, crafted, catalyst) and archetype --theme-item-popup-progress-bar-bg: rgb(0, 0, 0, 0.3); // Item feed --theme-item-feed-bg: var(--theme-fill-modal); // Item sheet (compare, armory) --theme-item-sheet-bg: var(--theme-fill-modal); // Organizer --theme-organizer-row-odd-bg: #171717; --theme-organizer-row-even-bg: #2b2b2b; // Records --theme-record-redeemed: #c88861; // More vibrant P3 colourspace tints for displays that support them @supports (color: color(display-p3 1 1 1)) { --theme-accent-primary: oklch(61% 0.21 41.64); } } ================================================ FILE: src/app/themes/_theme-throneworld.scss ================================================ // Throne World theme .theme-throneworld { // Base colours --theme-accent-primary: #2ae9c6; // Interaction styles (hover, active, focus) --theme-accent-secondary: #c17e2b; // Secondary interactions // Generic fills for modal backgrounds (see below) --theme-fill-modal: #1b422c; // Base fill for popups, drawers, dropdowns etc --theme-fill-modal-actions: #161616; // Layered/contextual modals (search, item actions) // Text --theme-text: #fff; --theme-text-invert: #000; --theme-text-secondary: #eee; // Buttons --theme-button-bg: rgb(255, 255, 255, 0.2); --theme-button-text: white; // Input field (text boxes and text areas) --theme-input-bg: #333; // Shadows --theme-drop-shadow: 0 0 18px 0 #000; // Tooltips --theme-tooltip-underline: transparent; --theme-tooltip-header-bg: #0a0a0f; --theme-tooltip-body-bg: #151523; --theme-tooltip-border: #8e8e9e; --theme-tooltip-minimal-bg: #5d5970; // App background gradient pattern --theme-app-bg-gradient-start: #40a687; --theme-app-bg-gradient-end: #06562a; --theme-app-bg-gradient: radial-gradient( circle at 50% 70px, var(--theme-app-bg-gradient-start) 0%, var(--theme-app-bg-gradient-end) 100% ); // App --theme-app-bg: var(--theme-app-bg-gradient); // Background color for (currently only) Settings page sections. Distinguishes them from theme-app-bg. --theme-section-bg: var(--theme-search-bg); // The color of PWA elements like the title bar. This cannot be a gradient. --theme-pwa-background: #1b422c; --theme-mobile-background: #1b422c; // Header --theme-header-nav-bg: #1b422c; --theme-header-nav-slideout-menu-bg: var(--theme-fill-modal); --theme-header-characters-bg: #265c42; --theme-header-characters-txt: rgb(255, 255, 255, 0.8); // Dropdown menu --theme-dropdown-menu-bg: var(--theme-fill-modal); // Search // Background color for the search bar. Distinguishes it from theme-header-nav-bg. --theme-search-bg: rgb(0, 0, 0, 0.4); --theme-search-dropdown-bg: var(--theme-fill-modal-actions); --theme-search-dropdown-border: rgb(255, 255, 255, 0.2); // Items --theme-item-polaroid: #e3f9db; --theme-item-polaroid-txt: var(--theme-text-invert); --theme-item-polaroid-hover-border: #ddd; --theme-item-polaroid-masterwork: #9aad11; --theme-item-polaroid-masterwork-txt: black; --theme-item-polaroid-capped: #eab53c; --theme-item-polaroid-capped-txt: #b44e09; --theme-item-polaroid-godroll: #0b486b; --theme-item-polaroid-trashroll: #d14334; --theme-item-polaroid-element-adjust-brightness: 90%; // Adjust brightness level of Arc, Strand, Void icons // Item popup --theme-item-popup-border: #222; --theme-item-popup-arrow: var(--theme-fill-modal-actions); --theme-item-popup-actions-bg: var(--theme-fill-modal-actions); --theme-item-popup-panel-bg: #333; // Panels for progress (masterwork, crafted, catalyst) and archetype --theme-item-popup-progress-bar-bg: #222; // Item feed --theme-item-feed-bg: var(--theme-fill-modal); // Item sheet (compare, armory) --theme-item-sheet-bg: var(--theme-fill-modal); // Organizer --theme-organizer-row-odd-bg: #2c7c55; --theme-organizer-row-even-bg: #3c926c; // Records --theme-record-redeemed: #ecd610; } ================================================ FILE: src/app/themes/_theme-vexnet.scss ================================================ // Vex Network theme .theme-vexnet { // Base colours --theme-accent-primary: #5ed12c; // Interaction styles (hover, active, focus) --theme-accent-secondary: #f27b8a; // Secondary interactions // Generic fills for modal backgrounds (see below) --theme-fill-modal: #0d2f63; // Base fill for popups, drawers, dropdowns etc --theme-fill-modal-actions: #0b2432; // Layered/contextual modals (search, item actions) // Text --theme-text: #fff; --theme-text-invert: #000; --theme-text-secondary: #fff; // Buttons --theme-button-bg: rgb(255, 255, 255, 0.2); --theme-button-text: white; // Input field (text boxes and text areas) --theme-input-bg: #333; // Shadows --theme-drop-shadow: 0 0 18px 0 #000; // Tooltips --theme-tooltip-underline: var(--theme-accent-primary); --theme-tooltip-header-bg: #000; --theme-tooltip-body-bg: #1a1a1a; --theme-tooltip-border: #555; --theme-tooltip-minimal-bg: #5d5970; // App background gradient pattern --theme-app-bg-gradient-start: #65ccbd; --theme-app-bg-gradient-end: #195b80; --theme-app-bg-gradient: radial-gradient( circle at 50% 70px, var(--theme-app-bg-gradient-start) 0%, var(--theme-app-bg-gradient-end) 100% ); // App --theme-app-bg: var(--theme-app-bg-gradient); // Background color for (currently only) Settings page sections. Distinguishes them from theme-app-bg. --theme-section-bg: var(--theme-search-bg); // The color of PWA elements like the title bar. This cannot be a gradient. --theme-pwa-background: #0d2f63; --theme-mobile-background: #0d2f63; // Header --theme-header-nav-bg: #0d2f63; --theme-header-nav-slideout-menu-bg: var(--theme-fill-modal); --theme-header-characters-bg: #204c74; --theme-header-characters-txt: rgb(255, 255, 255, 0.625); // Dropdown menu --theme-dropdown-menu-bg: var(--theme-fill-modal); // Search // Background color for the search bar. Distinguishes it from theme-header-nav-bg. --theme-search-bg: rgb(255, 255, 255, 0.15); --theme-search-dropdown-bg: var(--theme-fill-modal-actions); --theme-search-dropdown-border: rgb(255, 255, 255, 0.2); // Items 'polaroid' framed icons --theme-item-polaroid: #98cbd5; --theme-item-polaroid-txt: black; --theme-item-polaroid-masterwork: #5ed12c; --theme-item-polaroid-masterwork-txt: #000; --theme-item-polaroid-capped: #ffc4c4; --theme-item-polaroid-capped-txt: #e54127; --theme-item-polaroid-godroll: white; --theme-item-polaroid-trashroll: #d14334; --theme-item-polaroid-element-adjust-brightness: 120%; // Adjust brightness level of Arc, Strand, Void icons // Item popup --theme-item-popup-border: #333; --theme-item-popup-arrow: #333; --theme-item-popup-actions-bg: var(--theme-fill-modal-actions); --theme-item-popup-panel-bg: rgb( 255, 255, 255, 0.06 ); // Panels for progress (masterwork, crafted, catalyst) and archetype --theme-item-popup-progress-bar-bg: rgb(0, 0, 0, 0.3); // Item feed --theme-item-feed-bg: var(--theme-fill-modal); // Item sheet (compare, armory) --theme-item-sheet-bg: var(--theme-fill-modal); // Organizer --theme-organizer-row-odd-bg: #2b5483; --theme-organizer-row-even-bg: #2e3d69; // Records --theme-record-redeemed: #e9ec10; // More vibrant P3 colourspace tints for displays that support them @supports (color: color(display-p3 1 1 1)) { --theme-accent-primary: oklch(72% 0.27 140.8); } } ================================================ FILE: src/app/themes/_theme.scss ================================================ @use '../variables.scss' as *; // Theme variables :root { // Base colours --theme-accent-primary: #{$dim-brand}; // Interaction styles (hover, active, focus) --theme-accent-secondary: #68a0b7; // Secondary interactions // Generic fills for modal backgrounds (see below) --theme-fill-modal: #000; // Base fill for popups, drawers, dropdowns etc --theme-fill-modal-actions: #161616; // Layered/contextual modals (search, item actions) // Text --theme-text: #fff; --theme-text-invert: #000; --theme-text-secondary: #aaa; // Buttons --theme-button-bg: rgb(255, 255, 255, 0.2); --theme-button-text: white; --theme-button-bg-primary: rgb(255, 255, 255, 0.3); // Input field (text boxes and text areas) --theme-input-bg: #333; // Shadows --theme-drop-shadow: 0 0 18px 0 #000; // Tooltips --theme-tooltip-underline: transparent; --theme-tooltip-header-bg: #0a0a0f; --theme-tooltip-body-bg: #151523; --theme-tooltip-border: #8e8e9e; --theme-tooltip-minimal-bg: #5d5970; // App background gradient pattern --theme-app-bg-gradient-start: hsl(240, 20%, 28%); --theme-app-bg-gradient-end: hsl(240, 27%, 12%); --theme-app-bg-gradient: radial-gradient( circle at 50% 70px, var(--theme-app-bg-gradient-start) 0%, var(--theme-app-bg-gradient-end) 100% ); // App --theme-app-bg: var(--theme-app-bg-gradient); // Background color for (currently only) Settings page sections. Distinguishes them from theme-app-bg. --theme-section-bg: var(--theme-search-bg); // The color of PWA elements like the title bar. This cannot be a gradient. --theme-pwa-background: #202034; --theme-mobile-background: #000; // Header --theme-header-nav-bg: var(--theme-app-bg-gradient); --theme-header-nav-slideout-menu-bg: var(--theme-fill-modal); --theme-header-characters-bg: var(--theme-app-bg-gradient); --theme-header-characters-txt: rgb(255, 255, 255, 0.8); // Dropdown menu --theme-dropdown-menu-bg: var(--theme-fill-modal); // Search // Background color for the search bar. Distinguishes it from theme-header-nav-bg. --theme-search-bg: rgb(0, 0, 0, 0.4); --theme-search-dropdown-bg: var(--theme-fill-modal-actions); --theme-search-dropdown-border: transparent; // Items --theme-item-polaroid: #ddd; --theme-item-polaroid-txt: var(--theme-text-invert); --theme-item-polaroid-hover-border: #ddd; --theme-item-polaroid-masterwork: #eade8b; --theme-item-polaroid-masterwork-txt: var(--theme-text-invert); --theme-item-polaroid-capped: #f5dc56; --theme-item-polaroid-capped-txt: #f2721b; --theme-item-polaroid-godroll: #0b486b; --theme-item-polaroid-trashroll: #d14334; --theme-item-polaroid-element-adjust-brightness: 70%; // Adjust for legibility of Arc, Strand, Void icons --theme-item-socket-border: #888; // Socketable slots (mods, fashion, sub-class/abilities) --theme-item-shaped-icon: #{$shaped}; // Item popup --theme-item-popup-border: #222; --theme-item-popup-arrow: var(--theme-fill-modal-actions); --theme-item-popup-actions-bg: var(--theme-fill-modal-actions); --theme-item-popup-panel-bg: #333; // Panels for progress (masterwork, crafted, catalyst) and archetype --theme-item-popup-progress-bar-bg: #222; // Item feed --theme-item-feed-bg: var(--theme-fill-modal); // Item sheet (compare, armory) --theme-item-sheet-bg: var(--theme-fill-modal); --theme-sheet-search-bg: rgb(255, 255, 255, 0.15); // Organizer --theme-organizer-row-odd-bg: #27263a; --theme-organizer-row-even-bg: #1d1c2b; // Records --theme-record-redeemed: #c4b578; } ================================================ FILE: src/app/utils/__snapshots__/csv.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`csv basic 1`] = ` "str,num,undef,nul,bool 123,5,,,true 734,4,,,false 567,4,,,false" `; exports[`csv no unpack 1`] = ` "str,arr,str2 123,"one,two,three",something 123,"one,two,three,four",blah" `; exports[`csv undef keys in first object 1`] = ` "str,num 123, 123,5" `; exports[`csv unpack 1`] = ` "str,arr 0,arr 1,arr 2,arr 3,str2 123,one,two,three,,something 123,one,two,three,four,blah" `; ================================================ FILE: src/app/utils/action-queue.ts ================================================ import { infoLog } from './log'; const _queue: Promise<unknown>[] = []; // A global queue of functions that will execute one after the other. The function must return a promise. // fn is either a blocking function or a function that returns a promise export function queueAction<K>(fn: () => Promise<K>): Promise<K> { const headPromise: Promise<unknown> = _queue.length ? _queue.at(-1)! : Promise.resolve(); // If available, run this task under a wake lock so the device doesn't sleep while the operation is running. const runPromise = async () => { let sentinel: WakeLockSentinel | undefined; if ('wakeLock' in navigator) { try { sentinel = await navigator.wakeLock.request('screen'); } catch (e) { infoLog('wakelock', 'Could not acquire screen wake lock', e); } } try { return await fn(); } finally { await sentinel?.release(); } }; // Execute fn regardless of the result of the existing promise. We // don't use finally here because finally can't modify the return value. const wrappedPromise = headPromise.then(runPromise, runPromise).then( (value) => { _queue.shift(); return value; }, (e) => { _queue.shift(); throw e; }, ); _queue.push(wrappedPromise); return wrappedPromise; } // Wrap a function to produce a function that will be queued when invoked export function queuedAction<T extends unknown[], K>( fn: (...args: T) => Promise<K>, context?: unknown, ): (...args: T) => Promise<K> { return (...args: T) => queueAction(() => fn.apply(context, args)); } ================================================ FILE: src/app/utils/app-badge.ts ================================================ import { isSteamBrowser } from './browsers'; export function setAppBadge(num?: number) { // Steam client updated to a version that crashes when you call setAppBadge if ('setAppBadge' in navigator && !isSteamBrowser()) { navigator.setAppBadge(num); } } export function clearAppBadge() { // Steam client updated to a version that crashes when you call setAppBadge if ('clearAppBadge' in navigator && !isSteamBrowser()) { navigator.clearAppBadge(); } } ================================================ FILE: src/app/utils/browsers.ts ================================================ // Utilities for browser detection. In general we avoid browser detection but // some bugs are not directly detectable. Keep user-agent detection here. const appStoreVersion = navigator.userAgent.includes('DIM AppStore'); /** Is this the App Store wrapper version of DIM? */ export function isAppStoreVersion() { return appStoreVersion; } const iOS = appStoreVersion || (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream); /** * Is this an iOS mobile browser (which all use Safari under the covers)? */ export function isiOSBrowser() { return iOS; } const windows = navigator.platform.includes('Win'); /** Is this a Windows machine? */ export function isWindows() { return windows; } const steam = navigator.userAgent.includes('Steam'); export function isSteamBrowser() { return steam; } const mac = appStoreVersion || /Mac|iPod|iPhone|iPad/.test(navigator.platform); export function isMac() { return mac; } const android = navigator.userAgent.includes('Android'); export function isAndroid() { return android; } export const isNativeDragAndDropSupported = () => { // Chrome on Android should support native DnD, but React-DnD may not be able // to work around some quirk of it. if (isAndroid()) { return false; } const div = document.createElement('div'); return 'draggable' in div && 'ondragstart' in div; }; ================================================ FILE: src/app/utils/cancel.ts ================================================ /** * A CancelToken should be passed to cancelable functions. Those functions should then check the state of the * token and return early, or use checkCanceled to throw a CanceledError if the token has been canceled. Callers * of cancelable functions should catch CanceledError. */ export interface CancelToken { readonly canceled: boolean; checkCanceled: () => void; } /** * Indicates that the function was canceled by a call to the cancellation token's cancel function. */ export class CanceledError extends Error { constructor() { super('canceled'); this.name = 'CanceledError'; } } /** * Returns a cancel token and a cancellation function. The token can be passed to functions and checked * to see whether it has been canceled. The function can be called to cancel the token. */ export function withCancel(): [CancelToken, () => void] { let isCanceled = false; return [ { get canceled() { return isCanceled; }, checkCanceled() { if (isCanceled) { throw new CanceledError(); } }, }, () => (isCanceled = true), ]; } export const neverCanceled = { get canceled() { return false; }, // eslint-disable-next-line @typescript-eslint/no-empty-function checkCanceled() {}, }; ================================================ FILE: src/app/utils/collections.test.ts ================================================ import { count, objectifyArray, reorder, uniqBy, wrap } from './collections'; describe('count', () => { test('counts elements that match the predicate', () => expect(count([1, 2, 3], (i) => i > 1)).toBe(2)); }); describe('objectifyArray', () => { test('keys objects by a property name that maps to an array', () => { const input = [{ key: [1, 3] }, { key: [2, 4] }]; const output = objectifyArray(input, 'key'); const expected = { '1': { key: [1, 3] }, '2': { key: [2, 4] }, '3': { key: [1, 3] }, '4': { key: [2, 4] }, }; expect(output).toEqual(expected); }); }); describe('wrap', () => { test('negative index', async () => { const index = wrap(-1, 2); expect(index).toBe(1); }); test('too large index', async () => { const index = wrap(3, 2); expect(index).toBe(1); }); test('too large index by a lot', async () => { const index = wrap(27, 5); expect(index).toBe(2); }); test('negative index by a lot', async () => { const index = wrap(-27, 5); expect(index).toBe(3); }); }); describe('uniqBy', () => { test('identity', async () => { const result = uniqBy(['a', 'b', 'a', 'c'], (i) => i); expect(result).toEqual(['a', 'b', 'c']); }); test('object values', async () => { // If the iteree function produces objects, they need to be reference equal to count as dupes const val1 = { val: 'b' }; const val2 = { val: 'other' }; const result = uniqBy(['a', 'b', 'a', 'c'], (i) => (i === 'b' ? val1 : val2)); expect(result).toEqual(['a', 'b']); }); test('complex func', async () => { const result = uniqBy([{ val: 'a' }, { val: 'b' }, { val: 'a' }, { val: 'c' }], (i) => i.val); expect(result).toEqual([{ val: 'a' }, { val: 'b' }, { val: 'c' }]); }); }); describe('reorder', () => { test('reorders', () => { expect(reorder([1, 2, 3, 4], 0, 2)).toEqual([2, 3, 1, 4]); }); }); ================================================ FILE: src/app/utils/collections.ts ================================================ /** * Count the number of values in the list that pass the predicate. */ export function count<T>( list: readonly T[], predicate: (value: T) => boolean | null | undefined, ): number { return list.reduce((total, item) => (predicate(item) ? total + 1 : total), 0); } /** * A single-pass filter and map function. Returning `undefined` from the mapping * function will skip the value. Falsy values are still included! * * Similar to https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map */ export function filterMap<In, Out>( list: readonly In[], fn: (value: In, index: number) => Out | undefined, ): Out[] { const result: Out[] = []; for (let i = 0; i < list.length; i++) { const mapped = fn(list[i], i); if (mapped !== undefined) { result.push(mapped); } } return result; } // Create a type from the keys of an object type that map to values of type PropType type PropertiesOfType<T, PropType> = keyof { [K in keyof T as T[K] extends PropType ? K : never]: T[K]; }; /** * This is similar to _.keyBy, but it specifically handles keying multiple times per item, where * the keys come from an array property. * * given the key 'key', turns * [ { key: [1, 3] }, { key: [2, 4] } ] * into { '1': { key: [1, 3] }, '2': { key: [2, 4], '3': { key: [1, 3] }, '4': { key: [2, 4] } } */ export function objectifyArray<T>(array: T[], key: PropertiesOfType<T, any[]>): NodeJS.Dict<T> { return array.reduce<NodeJS.Dict<T>>((acc, val) => { const prop = val[key] as string[]; for (const eachKeyName of prop) { acc[eachKeyName] = val; } return acc; }, {}); } /** * Given an index into an array, which may exceed the bounds of the array in either direction, * return a new index that "wraps around". * * @example * [0, 1][wrap(-1, 2)] === 1 */ export const wrap = (index: number, length: number) => { while (index < 0) { index += length; } while (index >= length) { index -= length; } return index; }; /** * A faster replacement for _.uniqBy that uses a Set internally */ export function uniqBy<T, K>(data: Iterable<T>, iteratee: (input: T) => K): T[] { const dedupe = new Set<K>(); const result: T[] = []; for (const d of data) { const mapped = iteratee(d); if (!dedupe.has(mapped)) { result.push(d); dedupe.add(mapped); } } return result; } /** * Immutably reorder a list by moving an element at index `startIndex` to * `endIndex`. Helpful for drag and drop. Returns a copy of the initial list. */ export function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); return result; } /** * A fast/light alternative to Object.keys(obj).length === 0. */ export function isEmpty<T extends object | undefined | null>(obj: T) { if (!obj) { return true; } // eslint-disable-next-line no-unreachable-loop for (const _key in obj) { return false; } return true; } type NotFalsey<T> = Exclude<T, false | null | 0 | 0n | '' | undefined>; /** * Removes falsy values (false, null, 0, 0n, '', undefined, NaN) from an array. * This is just as fast as es-toolkit's compact but it's less code. */ export function compact<T>(arr: readonly T[]): NotFalsey<T>[] { return arr.filter((item) => item) as NotFalsey<T>[]; } /** * Return a new object with the same keys, but values that are the result of * calling a mapping function on each value. If the mapping function returns * undefined, the key is omitted from the new object. This is slightly faster * than es-toolkit's mapValues plus it has the undefined-filtering behavior. */ export function mapValues<T extends object, K extends keyof T & string, V>( object: T, getNewValue: (value: Exclude<T[keyof T], undefined>, key: K) => V, ): { [K in keyof T]: V } { return Object.entries(object).reduce( (acc, [key, value]) => { if (value === undefined) { return acc; } const newValue = getNewValue(value as Exclude<T[keyof T], undefined>, key as K); if (newValue !== undefined) { acc[key as K] = newValue; } return acc; }, {} as { [K in keyof T]: V }, ); } /** * Invert produces a new object where the keys and values are swapped. This is * slightly faster than es-toolkit's invert and handles undefined values plus * optional conversion of string keys to values. */ export function invert<K extends string, V extends PropertyKey | undefined, VV = string>( obj: Record<K, V>, keyTransform?: (key: string) => VV, ): Record<Exclude<V, undefined>, VV> { return Object.entries<V>(obj).reduce( (acc, [key, value]) => { if (value !== undefined) { const newValue = keyTransform ? keyTransform(key) : key; if (newValue !== undefined) { // @ts-expect-error ts(2322) acc[value] = newValue as VV; } } return acc; }, {} as Record<Exclude<V, undefined>, VV>, ); } /** * A version of "maxBy" that returns the max of the mapping function, not the * original item from the array that *produced* the max value. This is * equivalent to `Math.max(...items.map(getValue))` but more efficient. */ export function maxOf<T>(items: readonly T[], getValue: (element: T) => number): number { let max = -Infinity; for (const element of items) { const value = getValue(element); if (value > max) { max = value; } } return max; } /** * A version of "minBy" that returns the min of the mapping function, not the * original item from the array that *produced* the min value. This is * equivalent to `Math.min(...items.map(getValue))` but more efficient. */ export function minOf<T>(items: readonly T[], getValue: (element: T) => number): number { let min = Infinity; for (const element of items) { const value = getValue(element); if (value < min) { min = value; } } return min; } /** * Sum the results of calling a mapping function on each element of the array. * If the array is empty, this function returns `0`. This is more * memory-efficient than es-toolkit's sumBy but could be removed if * https://github.com/toss/es-toolkit/pull/753 is merged. */ export function sumBy<T>(items: readonly T[], getValue: (element: T) => number): number { return items.reduce((total, x) => total + getValue(x), 0); } ================================================ FILE: src/app/utils/comparators.ts ================================================ export type Comparator<T> = (a: T, b: T) => -1 | 0 | 1; /** * Generate a comparator from a mapping function. * * @example * // Returns a comparator that compares items by power * compareBy((item) => item.power) */ export function compareBy<T>(fn: (arg: T) => number | string | undefined | boolean): Comparator<T> { return (a, b) => primitiveComparator(fn(a), fn(b)); } export function primitiveComparator( aVal: string | number | boolean | undefined, bVal: string | number | boolean | undefined, ) { // Undefined is neither greater than or less than anything. // This considers it less than everything (except another undefined). return aVal === bVal ? 0 // neither goes first : bVal === undefined ? 1 // b goes first : aVal === undefined || aVal < bVal ? -1 // a goes first : aVal > bVal ? 1 // b goes first : 0; // a fallback that would catch only invalid inputs } /** * Reverse the order of a comparator. */ export function reverseComparator<T>(compare: Comparator<T>): Comparator<T> { return (a, b) => compare(b, a); } /** * Chain multiple comparators together. If two values are equal according to one comparator, we try the next and so on. */ export function chainComparator<T>(...compares: Comparator<T>[]): Comparator<T> { return (a, b) => { for (const compare of compares) { const retval = compare(a, b); if (retval !== 0) { return retval; } } return 0; }; } export function compareByIndex<T, V>(list: V[], fn: (arg: T) => V): Comparator<T> { return compareBy((arg) => { const ix = list.indexOf(fn(arg)); return ix === -1 ? Number.MAX_SAFE_INTEGER : ix; }); } ================================================ FILE: src/app/utils/csv.test.ts ================================================ import { serializeCsv } from './csv'; test('csv basic', () => { const data = [ { str: '123', num: 5, undef: undefined, nul: null, bool: true }, // property order does not matter { str: '734', bool: false, undef: undefined, nul: null, num: 4 }, { str: '567', num: 4, undef: undefined, nul: null, bool: false }, ]; const output = serializeCsv(data, {}); expect(output).toMatchSnapshot(); }); test('csv undef keys in first object', () => { const data = [{ str: '123' }, { str: '123', num: 5 }]; const output = serializeCsv(data, {}); expect(output).toMatchSnapshot(); }); const arrayData = [ { str: '123', arr: ['one', 'two', 'three'], str2: 'something' }, { str: '123', str2: 'blah', arr: ['one', 'two', 'three', 'four'] }, ]; test('csv no unpack', () => { const output = serializeCsv(arrayData, {}); expect(output).toMatchSnapshot(); }); test('csv unpack', () => { const output = serializeCsv(arrayData, { unpackArrays: ['arr'] }); expect(output).toMatchSnapshot(); }); ================================================ FILE: src/app/utils/csv.ts ================================================ import { gaEvent } from 'app/google'; import Papa from 'papaparse'; import { maxOf } from './collections'; import { download } from './download'; import { errorLog } from './log'; type CsvPrimitive = string | number | boolean | undefined | null; export type CsvValue = CsvPrimitive | CsvPrimitive[]; export type CsvRow = Record<string, CsvValue>; export interface CsvExportOptions { /** * Unpack arrays behind these keys to Key 1, Key 2, ... * E.g. Perk -> Perk 1, Perk 2, ... */ unpackArrays?: string[]; } // only export for tests, because in JS the privacy boundary is the package... export function serializeCsv(data: CsvRow[], exportOptions: CsvExportOptions): string { const columnSet = new Set<string>(); const maxCountsByKey = Object.fromEntries( (exportOptions.unpackArrays ?? []).map( (key) => [ key, maxOf(data, (row) => { const val = row[key]; if (Array.isArray(val)) { return val.length; } errorLog('csv export', `key ${key} is not an array in CSV export data`); return 0; }), ] as const, ), ); const uniqueKeys = new Set(data.flatMap((row) => Object.keys(row))); for (const key of uniqueKeys) { const maxCount = maxCountsByKey[key]; if (maxCount === undefined) { columnSet.add(key); } else { for (let i = 0; i < maxCount; i++) { columnSet.add(`${key} ${i}`); } } } data = [...data]; for (let idx = 0; idx < data.length; idx++) { data[idx] = Object.fromEntries( Object.entries(data[idx]).flatMap(([key, value]): [string, CsvValue][] => { if (value === undefined || value === null) { return []; } if (maxCountsByKey[key] === undefined) { return [[key, value]] as const; } else if (!Array.isArray(value)) { const entryKey = `${key} 0`; return [[entryKey, value]] as const; } else { return value.map((val, idx) => { const entryKey = `${key} ${idx}`; return [entryKey, val] as const; }); } }), ); } return Papa.unparse(data, { columns: [...columnSet] }); } export function downloadCsv( filename: string, data: CsvRow[], exportOptions: CsvExportOptions = {}, ) { const filenameWithExt = `${filename}.csv`; const csv = serializeCsv(data, exportOptions); gaEvent('file_download', { file_name: filenameWithExt, file_extension: 'csv', }); // TODO: Replace PapaParse with a simpler/smaller CSV generator download(csv, filenameWithExt, 'text/csv'); } ================================================ FILE: src/app/utils/dim-error.ts ================================================ import { BungieError } from 'app/bungie-api/http-client'; import { I18nKey, t } from 'app/i18next-t'; import { PlatformErrorCodes } from 'bungie-api-ts/user'; import { convertToError } from './errors'; /** * An internal error that captures more error info for reporting. * * The message is typically a localized error message. */ export class DimError extends Error { /** A non-localized string to help identify/categorize errors for DIM developers. Usually the localization key of the message. */ code?: string; /** The error that caused this error, if there is one. Naming it 'cause' makes it automatically chain in Sentry. */ cause?: Error; /** Whether to show social links in the error report dialog. */ showSocials = true; /** Pass in just a message key to set the message to the localized version of that key, or override with the second parameter. */ constructor(messageKey: I18nKey, message?: string) { super(message || t(messageKey)); this.code = messageKey; this.name = 'DimError'; } public withError(error: unknown): DimError { this.cause = convertToError(error); return this; } public withNoSocials(): DimError { this.showSocials = false; return this; } /** * If this error is a Bungie API error, return its platform code. */ public bungieErrorCode(): PlatformErrorCodes | undefined { return this.cause instanceof BungieError ? this.cause.code : this.cause instanceof DimError ? this.cause.bungieErrorCode() : undefined; } } ================================================ FILE: src/app/utils/download.ts ================================================ import { tempContainer } from './temp-container'; /** Download a string as a file */ export function download(data: string, filename: string, type: string) { const a = document.createElement('a'); a.setAttribute('href', `data:${type};charset=utf-8,${encodeURIComponent(data)}`); a.setAttribute('download', filename); tempContainer.appendChild(a); a.click(); setTimeout(() => tempContainer.removeChild(a)); } ================================================ FILE: src/app/utils/empty.ts ================================================ /** * Stable empty versions of common data structures, to use in reducers. * * These always return the same instance so they'll always be referentially equal. */ const EMPTY_OBJ = Object.freeze({}); export function emptyObject<T extends Record<string, unknown> | Record<number, unknown>>(): T { return EMPTY_OBJ as T; } const EMPTY_ARRAY: readonly unknown[] = Object.freeze<unknown[]>([]); export function emptyArray<T>(): T[] { return EMPTY_ARRAY as T[]; } const EMPTY_SET = Object.freeze(new Set()); export function emptySet<T>(): Set<T> { return EMPTY_SET as Set<T>; } const EMPTY_MAP = Object.freeze(new Map()); export function emptyMap<K, V>(): Map<K, V> { return EMPTY_MAP as Map<K, V>; } ================================================ FILE: src/app/utils/errors.ts ================================================ /** * Produce an error message either from an Error object, or a stringy * representation of a non-Error object. Meant to be used when displaying or * logging errors from catch blocks. */ export function errorMessage(e: unknown): string { return e instanceof Error ? e.message : JSON.stringify(e); } /** * If the parameter is not an Error, wrap a stringified version of it in an * Error. Meant to be used from catch blocks where the thrown type is not known. */ export function convertToError(e: unknown): Error { if (e instanceof Error) { return e; } return new Error(JSON.stringify(e)); } ================================================ FILE: src/app/utils/functions.ts ================================================ export function noop(): void { return; } export function stubTrue(): true { return true; } export function stubFalse(): false { return false; } export function identity<T>(value: T): T { return value; } ================================================ FILE: src/app/utils/hooks.ts ================================================ import useResizeObserver from '@react-hook/resize-observer'; import { throttle } from 'es-toolkit'; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { Subscription, useSubscription } from 'use-subscription'; import { EventBus, Observable } from './observable'; /** * Subscribe to an EventBus. Use useCallback on the subscribeFn to prevent it * changing on every render. */ export function useEventBusListener<T>( eventBus: EventBus<T> | Observable<T>, subscribeFn: (value: T) => void, ) { useEffect(() => eventBus.subscribe(subscribeFn), [eventBus, subscribeFn]); } /** * Returns whether the shift key is held down (ignores focus) */ export function useShiftHeld() { const [shiftHeld, setShiftHeld] = useState(false); useEffect(() => { const shiftCheck = (e: KeyboardEvent) => !e.repeat && setShiftHeld(e.shiftKey); document.addEventListener('keydown', shiftCheck); document.addEventListener('keyup', shiftCheck); return () => { document.removeEventListener('keydown', shiftCheck); document.removeEventListener('keyup', shiftCheck); }; }, []); return shiftHeld; } /** * Sets a CSS variable to the height of the passed in ref. We could probably use resize observers but * just doing it on re-render seems to work. Don't overuse this. */ export function useSetCSSVarToHeight( ref: React.RefObject<HTMLElement | null>, propertyName: string, ) { const updateVar = useCallback( (height: number) => { document.querySelector('html')!.style.setProperty(propertyName, `${height}px`); }, [propertyName], ); useLayoutEffect(() => { updateVar(ref.current!.offsetHeight); }, [updateVar, ref]); useResizeObserver(ref, (entry) => updateVar((entry.target as HTMLElement).offsetHeight)); } /** * Like useState, but saves to/from LocalStorage. */ export function useLocalStorage<T>( key: string, initialValue: T, ): [T, (val: T | ((initial: T) => T)) => void] { const [storedValue, setStoredValue] = useState<T>((): T => { try { // Get from local storage by key const item = window.localStorage.getItem(key); // Parse stored json or if none return initialValue return item ? (JSON.parse(item) as T) : initialValue; } catch { return initialValue; } }); const setValue = (value: T | ((storedValue: T) => T)) => { // Allow value to be a function so we have same API as useState const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); }; return [storedValue, setValue]; } export function useThrottledSubscription<T>(observable: Observable<T>, delay: number) { const throttledObservable: Subscription<T> = useMemo( () => ({ getCurrentValue() { return observable.getCurrentValue(); }, subscribe(callback: () => void) { const throttled = throttle(callback, delay); const unsubscribe = observable.subscribe(throttled); return () => { unsubscribe(); throttled.cancel(); }; }, }), [observable, delay], ); const value = useSubscription(throttledObservable); return value; } /** * Determine a height for a given element based on its height and position * relative to the bottom of the viewport. */ export function useHeightFromViewportBottom( elementRef: React.RefObject<HTMLElement | null>, setHeightFromViewportBottom: (value: number) => void, itemHeight: number | undefined, withPadding: boolean, ) { const padding = withPadding ? 10 : 0; useEffect(() => { if (!window.visualViewport || !elementRef.current) { return; } const updateHeight = () => { const rect = elementRef.current!.getBoundingClientRect(); const { y, height } = rect; const { height: viewportHeight } = window.visualViewport!; // pixels remaining in viewport minus offset minus padding const pxAvailable = viewportHeight - y - height - padding; const heightFromBottom = itemHeight !== undefined ? Math.floor(pxAvailable / itemHeight) * itemHeight : Math.floor(pxAvailable); setHeightFromViewportBottom(heightFromBottom); }; updateHeight(); window.visualViewport.addEventListener('resize', updateHeight); return () => window.visualViewport?.removeEventListener('resize', updateHeight); }, [setHeightFromViewportBottom, elementRef, itemHeight, padding]); } export function usePageTitle(title: string, active?: boolean) { useEffect(() => { if (active !== false) { const titleElem = document.getElementsByTagName('title')[0]; titleElem.textContent = `DIM - ${title}`; return () => { titleElem.textContent = `DIM`; }; } }, [active, title]); } // On first render, focus the first focusable element. export function useFocusFirstFocusableElement(ref: React.RefObject<HTMLElement | null>) { useEffect(() => { if (ref.current) { const firstFocusable = ref.current.querySelector( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ); (firstFocusable as HTMLElement)?.focus(); } }, [ref]); } ================================================ FILE: src/app/utils/intl.test.ts ================================================ import { DIM_LANGS, DimLanguage } from 'app/i18n'; import { localizedIncludes, localizedSorter } from './intl'; const sortCases: [language: DimLanguage, input: string[], output: string[]][] = [ [ 'en', [ 'foo1', 'foo10', 'foo9', '🥺', '🤯', '\uD83D\uDE00', // 😀 ], ['🤯', '🥺', '😀', 'foo1', 'foo9', 'foo10'], ], ['de', ['foo', 'föo', 'ess', 'eß'], ['ess', 'eß', 'foo', 'föo']], ['ko', ['하', '가'], ['가', '하']], ['ja', ['あかさ', '赤け', 'アカコ'], ['アカコ', 'あかさ', '赤け']], [ 'es', [ 'ñ', '\u00F1', // ñ '\u006E\u0303', // ñ ], ['ñ', 'ñ', 'ñ'], ], // For the rest, mostly just test that it constructs correctly. We can add special test cases if we find things we want. ['es-mx', ['foo1', 'foo10', 'foo9'], ['foo1', 'foo9', 'foo10']], ['fr', ['foo1', 'foo10', 'foo9'], ['foo1', 'foo9', 'foo10']], ['it', ['foo1', 'foo10', 'foo9'], ['foo1', 'foo9', 'foo10']], ['pl', ['foo1', 'foo10', 'foo9'], ['foo1', 'foo9', 'foo10']], ['pt-br', ['foo1', 'foo10', 'foo9'], ['foo1', 'foo9', 'foo10']], ['ru', ['foo1', 'foo10', 'foo9'], ['foo1', 'foo9', 'foo10']], ['zh-chs', ['foo1', 'foo10', 'foo9'], ['foo1', 'foo9', 'foo10']], ['zh-cht', ['foo1', 'foo10', 'foo9'], ['foo1', 'foo9', 'foo10']], ]; it('should include a sorting test case for every supported DIM language', () => { expect(new Set(sortCases.map(([language]) => language))).toStrictEqual(new Set(DIM_LANGS)); }); // Test that we can construct this for every supported language test.each(sortCases)('localizedSorter: %s', (language, input, output) => { expect( // Map them into objects input .map((name) => ({ name, })) .sort( localizedSorter( language, // un-map the objects into their sort key (o) => o.name, ), ) // Map back to strings to make the matcher easier .map((o) => o.name), ).toStrictEqual(output); }); const includeCases: [language: DimLanguage, input: string, query: string, matches: boolean][] = [ ['en', 'bar', 'foobar', true], ['en', '😀', 'Laughing \uD83D\uDE00!!', true], ['en', '\uDE00', 'Laughing \uD83D\uDE00!!', true], // A bit weird - some unicode is composed of multiple other characters. In this case it sorta makes sense... ['en', '👨‍👩‍👧', '👨‍👩‍👧‍👦', true], ['en', 'föo', 'foobar', true], ['en', 'Föo', 'foobar', true], ['de', 'föo', 'foobar', false], ['ko', '가', '하가', true], ['ja', 'かさ', 'あかさ赤け', true], ['es', 'ño', 'niño', true], ['es-mx', 'ño', 'niño', true], // For the rest, mostly just test that it constructs correctly. We can add special test cases if we find things we want. ['fr', 'bar', 'foobar', true], ['it', 'bar', 'foobar', true], ['pl', 'bar', 'foobar', true], ['pt-br', 'bar', 'foobar', true], ['ru', 'bar', 'foobar', true], ['zh-chs', 'bar', 'foobar', true], ['zh-cht', 'bar', 'foobar', true], ]; it('should include an include test case for every supported DIM language', () => { expect(new Set(includeCases.map(([language]) => language))).toStrictEqual(new Set(DIM_LANGS)); }); test.each(includeCases)( 'localizedIncludes("%s", "%s")("%s") === %s', (language, query, input, matches) => { expect(localizedIncludes(language, query)(input)).toBe(matches); }, ); ================================================ FILE: src/app/utils/intl.ts ================================================ // Helpers for effectively using the browser's Intl.* tools // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl import { DimLanguage, browserLangToDimLang } from 'app/i18n'; import memoizeOne from 'memoize-one'; import { invert } from './collections'; import { Comparator } from './comparators'; import { stubTrue } from './functions'; // Our locale names don't line up with the BCP 47 tags for Chinese const dimLangToBrowserLang = invert(browserLangToDimLang); /** Map DIM's locale values to a [BCP 47 language tag](http://tools.ietf.org/html/rfc5646) */ function mapLocale(language: DimLanguage): Intl.UnicodeBCP47LocaleIdentifier { return dimLangToBrowserLang[language] ?? language; } const cachedSortCollator = memoizeOne( (language: DimLanguage) => new Intl.Collator(mapLocale(language), { // Consider "9" to come before "10" numeric: true, usage: 'sort', sensitivity: 'accent', }), ); const cachedSearchCollator = memoizeOne( (language: DimLanguage) => new Intl.Collator(mapLocale(language), { usage: 'search', sensitivity: 'base' }), ); const cachedListFormatter = memoizeOne( (language: DimLanguage) => new Intl.ListFormat(mapLocale(language)), ); export function localizedListFormatter(language: DimLanguage) { return cachedListFormatter(language); } /** * Return a sorting function that can sort arrays of type `T[]` in a locale-aware * way using some projection function on `T`. * * @example * ["foo10", "foo9"].sort(localizedSorter("en")) // ["foo9", "foo10"] */ export function localizedSorter<T>( language: DimLanguage, iteratee: (input: T) => string, ): Comparator<T> { const sortCollator = cachedSortCollator(language); return (a: T, b: T) => sortCollator.compare(iteratee(a), iteratee(b)) as 0 | 1 | -1; } /** * Return a version of `String.prototype.includes` that does locale-aware comparison. The query string is baked in. * * @example * const includes = localizedIncludes('en', 'föo'); * ["foobar", "barföo"].every((s) => includes(s)) // true */ export function localizedIncludes(language: DimLanguage, query: string) { if (query.length === 0) { return stubTrue; } const filterCollator = cachedSearchCollator(language); // Normalize the strings so we can compare the same abstract characters regardless of their original representation const normalizedQuery = query.normalize('NFC'); // Unfortunately Collator does not have a substring search method so we have to walk the string sequentially // See https://github.com/adobe/react-spectrum/blob/7f63e933e61f20891b4cf3f447ab817f918cb263/packages/%40react-aria/i18n/src/useFilter.ts#L58-L76 return (string: string) => { if (normalizedQuery.length === 0) { return true; } string = string.normalize('NFC'); let scan = 0; const sliceLen = normalizedQuery.length; for (; scan + sliceLen <= string.length; scan++) { const slice = string.slice(scan, scan + sliceLen); if (filterCollator.compare(normalizedQuery, slice) === 0) { return true; } } return false; }; } ================================================ FILE: src/app/utils/item-utils.ts ================================================ import { factionItemAligns } from 'app/destiny1/d1-factions'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { t } from 'app/i18next-t'; import { D1Item, DimItem, DimMasterwork, DimPlug, DimSocket, PluggableInventoryItemDefinition, } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { getSeason } from 'app/inventory/store/season'; import { knownModPlugCategoryHashes } from 'app/loadout/known-values'; import { D1BucketHashes } from 'app/search/d1-known-values'; import { ARTIFICE_PERK_HASH, killTrackerObjectivesByHash, killTrackerSocketTypeHash, tuningModToTunedStathash, } from 'app/search/d2-known-values'; import { damageNamesByEnum } from 'app/search/search-filter-values'; import { modSocketMetadata, ModSocketMetadata, modTypeTagByPlugCategoryHash, } from 'app/search/specialty-modslots'; import { DamageType, DestinyClass, DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2'; import artifactBreakerMods from 'data/d2/artifact-breaker-weapon-types.json'; import { BreakerTypeHashes, BucketHashes, ItemCategoryHashes, PlugCategoryHashes, StatHashes, } from 'data/d2/generated-enums'; import { objectifyArray } from './collections'; import { getArmor3TuningSocket } from './socket-utils'; // damage is a mess! // this function supports turning a destiny DamageType into a known english name // mainly for css purposes and the "is:arc" style filter names export const getItemDamageShortName = (item: DimItem): string | undefined => damageNamesByEnum[item.element?.enumValue ?? DamageType.None]; // these are helpers for identifying SpecialtySockets (combat style/raid mods). See specialty-modslots.ts const modMetadataBySocketTypeHash = objectifyArray(modSocketMetadata, 'socketTypeHashes'); // this has weird collisions but good enough for looking up mods with limited PCH compatibility, like raid slots // it can be used to find what mod metadata a plugged item belongs to export const modMetadataByPlugCategoryHash = objectifyArray( modSocketMetadata, 'compatiblePlugCategoryHashes', ); /** i.e. ['outlaw', 'forge', 'opulent', etc] */ export const modSlotTags = modSocketMetadata.map((m) => m.slotTag); // kind of silly but we are using a list of known mod hashes to identify specialty mod slots below const specialtySocketTypeHashes = modSocketMetadata.flatMap( (modMetadata) => modMetadata.socketTypeHashes, ); const specialtyModPlugCategoryHashes = modSocketMetadata.flatMap( (modMetadata) => modMetadata.compatiblePlugCategoryHashes, ); /** verifies an item is d2 armor and returns its specialty mod socket if any */ const getSpecialtySocket = (item?: DimItem): DimSocket | undefined => { if (item?.bucket.inArmor) { return item.sockets?.allSockets.find( (socket) => // check plugged -- non-artifice GoA armor still has the socket but nothing in it socket.plugged && // exotic armor 2.0 has this socket hidden if not upgraded to artifice armor yet socket.visibleInGame && specialtySocketTypeHashes.includes(socket.socketDefinition.socketTypeHash), ); } }; /** * Returns ModMetadatas if the item has one or more specialty mod slots. * * This no longer includes artifice. Use isArtifice function for that. */ export const getSpecialtySocketMetadata = (item?: DimItem): ModSocketMetadata | undefined => { const specialtySocket = getSpecialtySocket(item); if (!specialtySocket) { return; } return modMetadataBySocketTypeHash[specialtySocket.socketDefinition.socketTypeHash]; }; /** * returns mod type tag if the plugCategoryHash (from a mod definition's .plug) is known */ export const getModTypeTagByPlugCategoryHash = (plugCategoryHash: number): string | undefined => modTypeTagByPlugCategoryHash[plugCategoryHash as PlugCategoryHashes]; /** feed a **mod** definition into this */ export const isArmor2Mod = (item: DestinyInventoryItemDefinition): boolean => item.plug !== undefined && (knownModPlugCategoryHashes.includes(item.plug.plugCategoryHash) || specialtyModPlugCategoryHashes.includes(item.plug.plugCategoryHash)); /** accepts a DimMasterwork or lack thereof */ export function getMasterworkStatNames(mw: DimMasterwork | null) { return mw?.stats ?.filter((stat) => stat.isPrimary) .map((stat) => stat.name) .filter(Boolean) .join(', '); } /** Can this item be equipped by the given store? */ export function itemCanBeEquippedBy( item: DimItem, store: DimStore, allowPostmaster = false, ): boolean { if (store.isVault) { return false; } return ( itemCanBeEquippedByStoreId(item, store.id, store.classType, allowPostmaster) && (isD1Item(item) ? factionItemAligns(store, item) : true) ); } /** Can this item be equipped by the given (non-vault) store ID? */ export function itemCanBeEquippedByStoreId( item: DimItem, storeId: string, storeClassType: DestinyClass, allowPostmaster = false, ): boolean { return Boolean( item.equipment && (item.classified ? // we can't trust the classType of redacted items! they're all marked titan. // let's assume classified weapons are all-class item.bucket.inWeapons || // if it's equipped by this store, it's obviously equippable to this store! (item.owner === storeId && item.equipped) : // For the right class isClassCompatible(item.classType, storeClassType)) && // can be moved or is already here (!item.notransfer || item.owner === storeId) && (allowPostmaster || !item.location.inPostmaster), ); } export function nonPullablePostmasterItem(item: DimItem): boolean { return ( item.owner !== 'unknown' && !item.canPullFromPostmaster && Boolean(item.location.inPostmaster) ); } /** Could this be added to a loadout? */ export function itemCanBeInLoadout(item: DimItem): boolean { return ( item.equipment || (item.destinyVersion === 1 && (item.bucket.hash === BucketHashes.Consumables || // D1 had a "Material" type item.bucket.hash === BucketHashes.Materials)) ); } /** verifies an item has kill tracker mod slot, which is returned */ const getKillTrackerSocket = (item: DimItem): DimSocket | undefined => { if (item.bucket.inWeapons) { return item.sockets?.allSockets.find(isEnabledKillTrackerSocket); } }; /** Is this both a kill tracker socket, and the kill tracker is enabled? */ function isEnabledKillTrackerSocket(socket: DimSocket) { return (socket.plugged?.plugObjectives[0]?.objectiveHash ?? 0) in killTrackerObjectivesByHash; } /** Is this a kill tracker socket */ export function isKillTrackerSocket(socket: DimSocket) { return socket.socketDefinition.socketTypeHash === killTrackerSocketTypeHash; } export interface KillTracker { type: 'pve' | 'pvp' | 'gambit'; count: number; trackerDef: PluggableInventoryItemDefinition; } /** returns a socket's kill tracker info */ const getSocketKillTrackerInfo = ( socket: DimSocket | undefined, ): KillTracker | null | undefined => { const killTrackerPlug = socket?.plugged; return killTrackerPlug && plugToKillTracker(killTrackerPlug); }; export function plugToKillTracker(killTrackerPlug: DimPlug) { const type = killTrackerObjectivesByHash[killTrackerPlug.plugObjectives[0]?.objectiveHash]; const count = killTrackerPlug.plugObjectives[0]?.progress; if (type && count !== undefined) { return { type, count, trackerDef: killTrackerPlug.plugDef, }; } } /** returns an item's kill tracker info */ export const getItemKillTrackerInfo = (item: DimItem): KillTracker | null | undefined => getSocketKillTrackerInfo(getKillTrackerSocket(item)); const d1YearSourceHashes = { // tTK Variks CoE FoTL Kings Fall year2: [2659839637, 512830513, 1537575125, 3475869915, 1662673928], // RoI WoTM FoTl Dawning Raid Reprise year3: [2964550958, 4160622434, 3475869915, 3131490494, 4161861381], }; /** * Which "Year" of Destiny did this item come from? */ export function getItemYear( item: DimItem | DestinyInventoryItemDefinition, defs?: D2ManifestDefinitions, ) { if (('destinyVersion' in item && item.destinyVersion === 2) || 'displayProperties' in item) { const season = getSeason(item, defs); if (season < 27) { return season ? Math.floor(season / 4) + 1 : 0; } else { return season ? Math.floor((season - 27) / 2) + 8 : 0; } } else if (isD1Item(item)) { if (!item.sourceHashes) { return 1; } // determine what year this item came from based on sourceHash value // items will hopefully be tagged as follows // No value: Vanilla, Crota's End, House of Wolves // The Taken King (year 2): 460228854 // Rise of Iron (year 3): 24296771 // if sourceHash doesn't contain these values, we assume they came from // year 1 let year = 1; const ttk = item.sourceHashes.includes(d1YearSourceHashes.year2[0]); if ( ttk || item.infusable || item.sourceHashes.some((hash) => d1YearSourceHashes.year2.includes(hash)) ) { year = 2; } if ( !ttk && (item.classified || item.sourceHashes.some((hash) => d1YearSourceHashes.year3.includes(hash))) ) { year = 3; } return year; } else { return undefined; } } /** * Is this item a Destiny 1 item? Use this when you want the item to * automatically be typed as D1 item in the "true" branch of a conditional. * Otherwise you can just check "destinyVersion === 1". */ export function isD1Item(item: DimItem): item is D1Item { return item.destinyVersion === 1; } /** turns an item's list of stats into a dictionary of stats, keyed by stat hash */ export function getStatValuesByHash(item: DimItem, byWhichValue: 'base' | 'value') { const output: NodeJS.Dict<number> = {}; for (const stat of item.stats ?? []) { output[stat.statHash] = stat[byWhichValue]; } return output; } /** * Does this item have access to the Artifice mod slot that allows * the user to bump a stat by a small amount? */ export function isArtifice(item: DimItem) { return Boolean(item.sockets?.allSockets.some(isArtificeSocket)); } export function isArtificeSocket(socket: DimSocket) { // exotic armor has the artifice slot all the time, and it's usable when it's reported as visible return Boolean( socket.visibleInGame && socket.plugged && // in a better world, you'd only need to check this, because there's a "empty mod slot" item specifically for artifice slots. (socket.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.EnhancementsArtifice || // but some of those have the *generic* "empty mod slot" item plugged in, so we fall back to keeping an eye out for the intrinsic socket.plugged.plugDef.hash === ARTIFICE_PERK_HASH), ); } /** * Does this armor have the new-style armor masterwork in Edge of Fate, that grants +1 per MW tier, to the three lower stats? */ // TODO: May want to switch this to isLegacyArmorMasterwork eventually export function isArmor3(item: DimItem) { return Boolean(item.sockets?.allSockets.some(isArmor3MasterworkSocket)); } export function isArmor3MasterworkSocket(socket: DimSocket) { return ( socket.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.V460PlugsArmorMasterworks ); } /** * Are two Destiny classes compatible? e.g. can an item (firstClass) be equipped * by a character (secondClass)? True if they're the same class or one is the * wildcard class. */ export function isClassCompatible(firstClass: DestinyClass, secondClass: DestinyClass) { return ( firstClass === DestinyClass.Unknown || secondClass === DestinyClass.Unknown || firstClass === secondClass ); } /** * Can a loadout of classType `loadoutClass` use an item of class `itemClass`? * Global loadouts can only include items equippable by any class, so this is * more restrictive than `isClassCompatible` */ export function isItemLoadoutCompatible(itemClass: DestinyClass, loadoutClass: DestinyClass) { return itemClass === DestinyClass.Unknown || itemClass === loadoutClass; } const ichToBreakerType = Object.entries(artifactBreakerMods).reduce< Partial<Record<ItemCategoryHashes, BreakerTypeHashes>> >((memo, [breakerType, iches]) => { const breakerTypeNum = parseInt(breakerType, 10); for (const ich of iches) { memo[ich] = breakerTypeNum; } return memo; }, {}); /** * Get the effective breaker type of a weapon as granted by the seasonal * artifact. This does not include intrinsic breaker types (e.g. on some * exotics) so you should check item.breakerType first if you want the effective * overall breaker type, as intrinsic breaker beats artifact breaker. */ export function getSeasonalBreakerTypeHash(item: DimItem): number | undefined { if (item.destinyVersion === 2 && item.bucket.inWeapons && !item.breakerType) { for (const ich of item.itemCategoryHashes) { if (ichToBreakerType[ich]) { return ichToBreakerType[ich]; } } } } /** The full item type name shown as a subtitle in the item popup. e.g. "Hunter Gauntlets" */ export function itemTypeName(item: DimItem) { const classType = (item.classType !== DestinyClass.Unknown && // These already include the class name item.bucket.hash !== BucketHashes.ClassArmor && item.bucket.hash !== D1BucketHashes.Artifact && item.bucket.hash !== BucketHashes.Subclass && !item.classified && !( item.isExotic && [ ItemCategoryHashes.ArmorModsOrnamentsWarlock, ItemCategoryHashes.ArmorModsOrnamentsHunter, ItemCategoryHashes.ArmorModsOrnamentsTitan, ].some((h) => item.itemCategoryHashes.includes(h)) ) && item.classTypeNameLocalized[0].toUpperCase() + item.classTypeNameLocalized.slice(1)) || ''; const title = item.typeName && classType ? t('MovePopup.Subtitle.Type', { classType, typeName: item.typeName, }) : item.typeName || classType; if (!title) { return null; } return title; } /** * Returns [primary stat hash, secondary stat hash, tertiary stat hash] for armor 3.0. * Make sure the item is armor 3.0 upstream or these stat rankings might be misleading. */ export function getArmor3StatFocus(item: DimItem): StatHashes[] { return (item.stats?.filter((s) => s.statHash > 0 && s.base > 0) ?? []) .sort((a, b) => b.base - a.base) .map((s) => s.statHash); } /** * Returns the stat hash of the item's tunable stat. * This stat can be upgraded at the cost of another stat. * * Every armor with tuning has Balanced Tuning (3122197216) which provides +1 to several stats, * so this seeks an available plug item that's one of the +5/-5 mods. */ export function getArmor3TuningStat(item: DimItem): StatHashes | undefined { const reusablePlugItems = item.bucket.inArmor ? getArmor3TuningSocket(item)?.reusablePlugItems : undefined; if (!reusablePlugItems?.length) { return; } for (const { plugItemHash } of reusablePlugItems) { if (plugItemHash in tuningModToTunedStathash) { return tuningModToTunedStathash[plugItemHash]; } } return undefined; } ================================================ FILE: src/app/utils/log.ts ================================================ /* eslint-disable no-console */ // Track how long it's been since the page load started and add that to each // log. Most folks who paste us logs won't have timestamps enabled and they can // be very useful. const start = Date.now(); function logTime() { return (Date.now() - start) / 1000; } /** * A wrapper around console.log. Use this when you mean to have logging in the shipped app. * Otherwise, we'll prevent console.log from getting submitted via a lint rule. * * @param tag an informative label for categorizing this log * @example infoLog("Manifest", "The manifest loaded") */ export function infoLog(tag: string, message: unknown, ...args: unknown[]) { console.log(`[${tag}]`, logTime(), message, ...args); } /** * A wrapper around console.warn. Use this when you mean to have logging in the shipped app. * Otherwise, we'll prevent console.warn from getting submitted via a lint rule. * * @param tag an informative label for categorizing this log * @example warnLog("Manifest", "The manifest is out of date") */ export function warnLog(tag: string, message: unknown, ...args: unknown[]) { console.warn(`[${tag}]`, logTime(), message, ...args); } /** * A wrapper around console.warn that doesn't show the stack trace until you * expand it. Use this when you mean to have logging in the shipped app. * Otherwise, we'll prevent console.warn from getting submitted via a lint rule. * * @param tag an informative label for categorizing this log * @example warnLogCollapsedStack("Manifest", "The manifest is out of date") */ export function warnLogCollapsedStack(tag: string, message: unknown, ...args: unknown[]) { console.groupCollapsed(`[${tag}]`, logTime(), message); console.warn(`[${tag}]`, message, ...args); console.groupEnd(); } /** * A wrapper around console.error. Use this when you mean to have logging in the shipped app. * Otherwise, we'll prevent console.error from getting submitted via a lint rule. * * @param tag an informative label for categorizing this log * @example errorLog("Manifest", "The manifest failed to load") */ export function errorLog(tag: string, message: unknown, ...args: unknown[]) { console.error(`[${tag}]`, logTime(), message, ...args); } /** * A wrapper around console.time. Use this when you mean to have timing in the shipped app. * Otherwise, we'll prevent console.time from getting submitted via a lint rule. * * Unlike the real console.time, this returns a function that is used to end the timer. */ export function timer(tag: string, message: string) { // Note: This will log the time when the timer started, but the log entry will // only appear when it ends. const label = `[${tag}] ${logTime()} ${message}`; console.time(label); return () => console.timeEnd(label); } ================================================ FILE: src/app/utils/measure-memory.ts ================================================ // https://web.dev/monitor-total-page-memory-usage/ import { humanBytes } from 'app/storage/human-bytes'; import { infoLog } from './log'; // TODO: Revisit this once we can actually achieve crossOriginIsolated mode: https://web.dev/cross-origin-isolation-guide/ export function scheduleMemoryMeasurement() { // Check measurement API is available. if (!window.crossOriginIsolated) { return; } if (!performance.measureUserAgentSpecificMemory) { return; } const interval = measurementInterval(); setTimeout(performMeasurement, interval); } const MEAN_INTERVAL_IN_MS = 5 * 60 * 1000; function measurementInterval() { return -Math.log(Math.random()) * MEAN_INTERVAL_IN_MS; } async function performMeasurement() { // 1. Invoke performance.measureUserAgentSpecificMemory(). let result: MeasureMemoryResult; try { result = await performance.measureUserAgentSpecificMemory(); } catch (error) { if (error instanceof DOMException && error.name === 'SecurityError') { return; } // Rethrow other errors. throw error; } // 2. Record the result. infoLog( 'memory', `DIM is using ${humanBytes(result.bytes)} of memory.`, result.breakdown .filter((b) => b.bytes) .map((b) => `${b.types.join('/')}: ${humanBytes(b.bytes)}`) .join(', '), ); // 3. Schedule the next measurement. scheduleMemoryMeasurement(); } ================================================ FILE: src/app/utils/media-queries.ts ================================================ import { setPhonePortrait } from '../shell/actions'; import store from '../store/store'; // This seems like a good breakpoint for portrait based on https://material.io/devices/ // We can't use orientation:portrait because Android Chrome messes up when the keyboard is shown: https://www.chromestatus.com/feature/5656077370654720 const phoneWidthQuery = 'matchMedia' in window ? window.matchMedia('(max-width: 540px)') : undefined; phoneWidthQuery?.addEventListener('change', (e) => { store.dispatch(setPhonePortrait(e.matches)); }); /** * Return whether we're in phone-portrait mode right now. */ export function isPhonePortraitFromMediaQuery() { return Boolean(phoneWidthQuery?.matches); } ================================================ FILE: src/app/utils/memoize.test.ts ================================================ import { weakMemoize } from './memoize'; describe('weakMemoize', () => { // This is all we can test - we can't test the "weak" part test('caches results of computation', () => { const memoized = weakMemoize(({ arg }: { arg: string }) => ({ prop: arg })); const arg = { arg: 'foo' }; const val = memoized(arg); const val2 = memoized(arg); expect(val2).toBe(val); expect(memoized({ arg: 'bar' })).not.toBe(val); }); }); ================================================ FILE: src/app/utils/memoize.ts ================================================ /** * Produce a function that can memoize a calculation about an item. The cache is backed by * a WeakMap so when the item is garbage collected the cache is freed up too. */ export function weakMemoize<T extends object, R>(func: (arg0: T) => R): (arg1: T) => R { const cache = new WeakMap<T, R>(); return (arg: T): R => { if (cache.has(arg)) { return cache.get(arg)!; } const value = func(arg); cache.set(arg, value); return value; }; } ================================================ FILE: src/app/utils/observable.ts ================================================ type Subscription<T> = (value: T) => void; type Unsubscribe = () => void; /** * A minimal event emitter, as a replacement for RxJS Subject. Any type can be * an event. Use this when you want to subscribe to things that happen but don't * care about current/previous values. Supports multiple subscriptions to new * events. NOT compatible with use-subscription. Awkward name chosen to not conflict * with other existing types. */ export class EventBus<T> { private _subscriptions = new Set<Subscription<T>>(); /** * Notify all subscribers of an event. */ next(value: T) { for (const subscription of this._subscriptions) { subscription(value); } } /** * Add a subscription to events. Returns a function that can be used to unsubscribe. */ subscribe(callback: Subscription<T>): Unsubscribe { this._subscriptions.add(callback); return () => this._subscriptions.delete(callback); } } /** * A minimal Observable value, as a replacement for RxJS BehaviorSubject. * Supports multiple subscriptions to value changes and getting the last/current value. * Compatible with use-subscription: https://github.com/facebook/react/tree/master/packages/use-subscription */ export class Observable<T> { private _value: T; private _event = new EventBus<T>(); constructor(initialValue: T) { this._value = initialValue; } /** * Update the value of this observable and notify all subscribers. */ next(value: T) { this._value = value; this._event.next(value); } /** * Get the last value that was set for this observable. * This needs to be an arrow function so it is bound to the instance since use-subscription uses it that way. */ getCurrentValue = (): T => this._value; /** * Add a subscription to value changes. Returns a function that can be used to unsubscribe. * The subscription is not called until the value changes - if you want the value at subscription * time call getCurrentValue(). * This needs to be an arrow function so it is bound to the instance since use-subscription uses it that way. */ subscribe = (callback: Subscription<T>): Unsubscribe => this._event.subscribe(callback); } ================================================ FILE: src/app/utils/parallel-cores.ts ================================================ import { useLocalStorage } from './hooks'; const MAX_PARALLEL_CORES_KEY = 'maxParallelCores'; function getDefaultMaxParallelCores(): number { // Don't spin up a ton of threads for smaller problems, leave half the cores free return Math.max(1, Math.ceil((navigator.hardwareConcurrency || 1) / 2)); } export function useMaxParallelCores(): [number, (value: number) => void] { return useLocalStorage(MAX_PARALLEL_CORES_KEY, getDefaultMaxParallelCores()); } export function getMaxParallelCores(): number { try { const storedValue = window.localStorage.getItem(MAX_PARALLEL_CORES_KEY); return storedValue ? parseInt(storedValue, 10) : getDefaultMaxParallelCores(); } catch { return getDefaultMaxParallelCores(); } } ================================================ FILE: src/app/utils/perk-utils.ts ================================================ import perkToEnhanced from 'data/d2/trait-to-enhanced-trait.json'; import { invert } from './collections'; // map an enhanced perk hash to the unenhanced version. const enhancedToPerk = invert(perkToEnhanced, Number); /** Convert a perk hash to its enhanced version, if possible, else returns it unchanged (maybe it was already enhanced?) */ export function normalizeToEnhanced(perkHash: number) { return perkToEnhanced[perkHash] ?? perkHash; } /** Convert a perk hash to its un-enhanced version(s), if possible, else returns it unchanged (maybe it wasn't enhanced?) */ export function normalizeToUnenhanced(perkHash: number) { return enhancedToPerk[perkHash] ?? perkHash; } /** Return the hash of the enhanced version of this perk, assuming it is an un-enhanced perk. */ export function enhancedVersion(perkHash: number): number | undefined { return perkToEnhanced[perkHash]; } /** Return the hash of the unenhanced version of this perk, assuming it is an enhanced perk. */ export function unenhancedVersion(perkHash: number): number | undefined { return enhancedToPerk[perkHash]; } /** Is this hash one of our known enhanced versions of a regular perk? */ export function isEnhancedPerkHash(perkHash: number) { return perkHash in enhancedToPerk; } ================================================ FILE: src/app/utils/plug-descriptions.ts ================================================ import { Perk } from 'app/clarity/descriptions/descriptionInterface'; import { clarityDescriptionsSelector } from 'app/clarity/selectors'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { settingSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { DimItem, DimPlug, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { getStatSortOrder, isAllowedItemStat, isAllowedPlugStat } from 'app/inventory/store/stats'; import { isPlugStatActive, mapAndFilterInvestmentStats, } from 'app/inventory/store/stats-conditional'; import { activityModPlugCategoryHashes } from 'app/loadout/known-values'; import { useD2Definitions } from 'app/manifest/selectors'; import { DestinyClass, ItemPerkVisibility } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes, StatHashes, TraitHashes } from 'data/d2/generated-enums'; import { useSelector } from 'react-redux'; import modsWithoutDescription from '../../data/d2/mods-with-bad-descriptions.json'; import { compareBy } from './comparators'; import { unenhancedVersion } from './perk-utils'; import { isArmorArchetypePlug } from './socket-utils'; import { LookupTable } from './util-types'; export interface DimPlugPerkDescription { perkHash: number; name?: string; description?: string; requirement?: string; } export interface DimPlugDescriptions { perks: DimPlugPerkDescription[]; communityInsight: Perk | undefined; } // some stats are often referred to using different names // TODO: these need to be localized? const statNameAliases: LookupTable<StatHashes, string[]> = { [StatHashes.AimAssistance]: ['Aim Assist'], [StatHashes.AmmoCapacity]: ['Magazine Stat'], [StatHashes.ReloadSpeed]: ['Reload'], }; export function usePlugDescriptions( plug?: PluggableInventoryItemDefinition, stats?: { value: number; statHash: number; }[], ): DimPlugDescriptions { const defs = useD2Definitions(); const allClarityDescriptions = useSelector(clarityDescriptionsSelector); const descriptionsToDisplay = useSelector(settingSelector('descriptionsToDisplay')); const result: DimPlugDescriptions = { perks: [], communityInsight: undefined, }; if (!plug || !defs) { return result; } const showBungieDescription = !$featureFlags.clarityDescriptions || descriptionsToDisplay !== 'community'; const showCommunityDescription = $featureFlags.clarityDescriptions && descriptionsToDisplay !== 'bungie'; const showCommunityDescriptionOnly = $featureFlags.clarityDescriptions && descriptionsToDisplay === 'community'; // within this plug, let's not repeat any strings const statStrings = new Set<string>(); if (stats) { // preload the used string tracker with common text representations of stat modifications for (const stat of stats) { const statDef = defs.Stat.get(stat.statHash); if (statDef) { const statNames = [statDef.displayProperties.name].concat( statNameAliases[stat.statHash as StatHashes] ?? [], ); for (const statName of statNames) { if (stat.value < 0) { statStrings.add(`${stat.value} ${statName}`); statStrings.add(`${stat.value} ${statName} ▼`); } else { statStrings.add(`+${stat.value} ${statName}`); statStrings.add(`+${stat.value} ${statName} ▲`); statStrings.add(`Grants ${stat.value} ${statName}`); } } } } } const statAndBungieDescStrings = new Set<string>(statStrings); const perks = getPerkDescriptions(plug, defs, statAndBungieDescStrings); if (showCommunityDescription && allClarityDescriptions) { let clarityPerk = allClarityDescriptions[plug.hash]; // if we couldn't find a Clarity description for this perk, fall back to the non-enhanced perk variant if (!clarityPerk) { const regularPerkHash = unenhancedVersion(plug.hash); if (regularPerkHash) { clarityPerk = allClarityDescriptions[regularPerkHash]; } } if (clarityPerk) { result.communityInsight = clarityPerk; } } // if we don't have a community description, fall back to the Bungie description (if we aren't already // displaying it) if (showBungieDescription || (showCommunityDescriptionOnly && !result.communityInsight)) { result.perks.push( ...perks.map((p) => { if (isArmorArchetypePlug(plug)) { // Remove the unnecesary prose in Armor 3.0 Archetype descriptions return { ...p, description: p.description?.split('\n\n')[1] || p.description }; } return p; }), ); } return result; } function getPerkDescriptions( plug: PluggableInventoryItemDefinition, defs: D2ManifestDefinitions, usedStrings: Set<string>, ): DimPlugPerkDescription[] { const results: DimPlugPerkDescription[] = []; const plugDescription = plug.displayProperties.description || undefined; function addPerkDescriptions() { // filter out things with no displayable text, or that are meant to be hidden for (const perk of plug.perks) { if (perk.perkVisibility === ItemPerkVisibility.Hidden) { continue; } const sandboxPerk = defs.SandboxPerk.get(perk.perkHash); const perkName = sandboxPerk.displayProperties.name; let perkDescription = sandboxPerk.displayProperties.description || undefined; if (perkDescription) { if (usedStrings.has(perkDescription)) { perkDescription = undefined; } else { usedStrings.add(perkDescription); } } // Some perks are only active in certain activities (see Garden of Salvation raid mods) let perkRequirement = perk.requirementDisplayString || undefined; if (perkRequirement) { if (usedStrings.has(perkRequirement)) { perkRequirement = undefined; } else { usedStrings.add(perkRequirement); } } if (perkDescription || perkRequirement) { results.push({ perkHash: perk.perkHash, name: perkName && perkName !== plug.displayProperties.name ? perkName : undefined, description: perkDescription, requirement: perkRequirement, }); } } } function addDescriptionAsRequirement() { if (plugDescription && !usedStrings.has(plugDescription)) { results.push({ perkHash: -usedStrings.size, requirement: plugDescription, }); usedStrings.add(plugDescription); } } function addDescriptionAsFunctionality() { if (plugDescription && !usedStrings.has(plugDescription)) { results.push({ perkHash: -usedStrings.size, description: plugDescription, }); usedStrings.add(plugDescription); } } function addTooltipNotifsAsRequirement() { const notifs = plug.tooltipNotifications .map((notif) => notif.displayString) .filter((str) => !usedStrings.has(str)); for (const notif of notifs) { results.push({ perkHash: -usedStrings.size, requirement: notif, }); usedStrings.add(notif); } } function addCustomDescriptionAsFunctionality() { for (const mod of modsWithoutDescription.Harmonic) { if (plug.hash === mod) { results.push({ perkHash: -usedStrings.size, description: t('Mods.HarmonicModDescription'), }); usedStrings.add(t('Mods.HarmonicModDescription')); } } } /* Most plugs use the description field to describe their functionality. Some plugs (e.g. armor mods) store their functionality in their perk descriptions and use the description field for auxiliary info like requirements and caveats. For these plugs, we want to prioritize strings in the perks and only fall back to the actual description if we don't have any perks. Other plugs (e.g. Exotic catalysts) always use the description field to store their requirements. */ if (plug.traitHashes?.includes(TraitHashes.ItemExoticCatalyst)) { addPerkDescriptions(); addDescriptionAsRequirement(); } else if (plug.itemCategoryHashes?.includes(ItemCategoryHashes.ArmorMods)) { addPerkDescriptions(); // if we already have some displayable perks, this means the description is basically // a "requirements" string like "This mod's perks are only active" etc. (see Deep Stone Crypt raid mods) if (results.length > 0 && activityModPlugCategoryHashes.includes(plug.plug.plugCategoryHash)) { addDescriptionAsRequirement(); } else { addDescriptionAsFunctionality(); } } else if (plugDescription) { addDescriptionAsFunctionality(); } else { addPerkDescriptions(); } // Add custom descriptions created for mods who's description is hard to access or an accurate description isn't present addCustomDescriptionAsFunctionality(); // a fallback: if we still don't have any perk descriptions, at least keep the first perk for display. // there are mods like this (e.g. Elemental Armaments): no description, and annoyingly all perks are set // to ItemPerkVisibility.Hidden if (!results.length && plug.perks.length) { const firstPerk = plug.perks[0]; const sandboxPerk = defs.SandboxPerk.get(firstPerk.perkHash); const perkName = sandboxPerk.displayProperties.name; const perkDesc: DimPlugPerkDescription = { perkHash: firstPerk.perkHash, name: perkName && perkName !== plug.displayProperties.name ? perkName : undefined, }; if ( sandboxPerk.displayProperties.description && !usedStrings.has(sandboxPerk.displayProperties.description) ) { perkDesc.description = sandboxPerk.displayProperties.description; usedStrings.add(sandboxPerk.displayProperties.description); } if ( firstPerk.requirementDisplayString && !usedStrings.has(firstPerk.requirementDisplayString) ) { perkDesc.requirement = firstPerk.requirementDisplayString; usedStrings.add(firstPerk.requirementDisplayString); } if (perkDesc.description || perkDesc.requirement) { results.push(perkDesc); } } // Needs to be last added otherwise we can break the above statement causing a description to not be added if (plug.itemCategoryHashes?.includes(ItemCategoryHashes.ArmorMods)) { addTooltipNotifsAsRequirement(); } return results; } /** * Get plug stats based entirely on a static definition. Since we don't have an item, * the returned stats will be investment stats and not scaled to items; and any conditions * that depend on an item will succeed. */ export function getPlugDefStats( plugDef: PluggableInventoryItemDefinition, classType: DestinyClass | undefined, item: DimItem | undefined, ) { return mapAndFilterInvestmentStats(plugDef) .filter( (stat) => (isAllowedItemStat(stat.statTypeHash) || isAllowedPlugStat(stat.statTypeHash)) && isPlugStatActive(stat.activationRule, { classType, item, statHash: stat.statTypeHash }), ) .map((stat) => ({ statHash: stat.statTypeHash, // We completely lie here and turn investment stats 1:1 into displayed stats, // but that's a necessary consequence of operating without an item. It's mostly // correct for the 6 character stats that loadout mods and subclass plugs give, // which is where this function is used most. value: stat.value, })) .sort(compareBy((stat) => getStatSortOrder(stat.statHash))); } export function getDimPlugStats(item: DimItem, plug: DimPlug) { if (plug.stats) { return Object.entries(plug.stats) .map(([statHash, value]) => ({ statHash: parseInt(statHash, 10), value: value.value, })) .filter( (stat) => // Item stats are only shown if the item can actually benefit from them (isAllowedItemStat(stat.statHash) && item.stats?.some((itemStat) => itemStat.statHash === stat.statHash)) || isAllowedPlugStat(stat.statHash), ) .sort(compareBy((stat) => getStatSortOrder(stat.statHash))); } } ================================================ FILE: src/app/utils/promises.test.ts ================================================ import { noop } from './functions'; import { dedupePromise } from './promises'; describe('dedupePromise', () => { test('caches inflight promises', async () => { let outerResolve: (value: string) => void = noop; let outerReject: (e: Error) => void = noop; let promiseFunctionInvoked = 0; const deduped = dedupePromise( () => new Promise((resolve, reject) => { outerResolve = resolve; outerReject = reject; promiseFunctionInvoked++; }), ); // Multiple calls before the promise resolves return the same promise const promise1 = deduped(); const promise2 = deduped(); expect(promiseFunctionInvoked).toBe(1); outerResolve('foo'); // Since they're the same promise, they both resolve to the same value const [val1, val2] = await Promise.all([promise1, promise2]); expect(val1).toBe('foo'); expect(val2).toBe('foo'); // After the first promise resolved, calling again is a new promise const promise3 = deduped(); expect(promiseFunctionInvoked).toBe(2); // If the promise rejects, we see it outerReject(new Error('done')); await expect(async () => { await promise3; }).rejects.toThrow('done'); // And rejection also clears the cache so the next invocation gets a new promise const promise4 = deduped(); expect(promiseFunctionInvoked).toBe(3); outerResolve('baz'); const val4 = await promise4; expect(val4).toBe('baz'); }); }); ================================================ FILE: src/app/utils/promises.ts ================================================ /** * Transform an async function into a version that will only execute once at a time - if there's already * a version going, the existing promise will be returned instead of running it again. */ export function dedupePromise<T extends unknown[], K>( func: (...args: T) => Promise<K>, ): (...args: T) => Promise<K> { let promiseCache: Promise<K> | null = null; return async (...args: T) => { if (promiseCache) { return promiseCache; } promiseCache = func(...args); try { return await promiseCache; } finally { promiseCache = null; } }; } // setTimeout as a promise // TODO: consider using delay from 'es-toolkit' if we want to be able to cancel // the delay with an abort signal export function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } ================================================ FILE: src/app/utils/react.ts ================================================ import { ReactElement, ReactNode, cloneElement } from 'react'; /** places a divider between each element of arr */ export function addDividers<T extends React.ReactNode>( arr: T[], divider: ReactElement, ): ReactNode[] { // eslint-disable-next-line @eslint-react/no-clone-element return arr.flatMap((e, i) => [i ? cloneElement(divider, { key: `divider-${i}` }) : null, e]); } ================================================ FILE: src/app/utils/seasons.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { currentSeasonPassHashSelector } from 'app/manifest/selectors'; import { DestinyProfileResponse } from 'bungie-api-ts/destiny2'; import { useSelector } from 'react-redux'; export function useCurrentSeasonInfo( defs: D2ManifestDefinitions, profileInfo: DestinyProfileResponse | undefined, ) { const currentSeasonPassHash = useSelector(currentSeasonPassHashSelector); if (!defs || !profileInfo) { return { season: undefined, seasonPass: undefined, seasonPassStartEnd: undefined }; } const season = profileInfo.profile?.data?.currentSeasonHash ? defs.Season.get(profileInfo.profile.data.currentSeasonHash) : undefined; const seasonPass = currentSeasonPassHash ? defs.SeasonPass.get(currentSeasonPassHash) : undefined; return { season, seasonPass, seasonPassStartEnd: season?.seasonPassList.find( (s) => s.seasonPassHash === currentSeasonPassHash, ), }; } ================================================ FILE: src/app/utils/selectors.ts ================================================ import { RootState } from 'app/store/types'; // Note: Separate file (even from redux-utils) so that there are no non-type imports, // as it's otherwise prone to circular import dependencies that break selectors /** * Turn a selector output from reselect's createSelector which depends on some input * other than state, into a function that produces a selector function based on that * input. This makes it nicer to use in useSelector. * * @example * * const upperStoreIdSelector = createSelector( * (state: RootState, storeId: string) => storeId, * (storeId) => storeId.toUpperCase() * ) * * // Hard to put into useSelector: * const upperStoreId = useSelector((state: RootState) => upperStoreIdSelector(state, storeId)) * * const curriedUpperStoreIdSelector = currySelector(upperStoreIdSelector) * * // Nice: * const upperStoreId = useSelector(curriedUpperStoreIdSelector(storeId)) * * // You can still use it as an input for createSelector: * const otherSelector = createSelector( * curriedUpperStoreIdSelector.selector, * (upperStore) => upperStore.toLowerCase() * ) */ export function currySelector<K, R>( selector: (state: RootState, props: K) => R, ): ((props: K) => (state: RootState) => R) & { selector: (state: RootState, props: K) => R } { const fn = (props: K) => (state: RootState) => selector(state, props); fn.selector = selector; return fn; } ================================================ FILE: src/app/utils/sentry.ts ================================================ import { browserTracingIntegration } from '@sentry/browser'; import { BrowserOptions, captureException, init, setTag, setUser, withScope } from '@sentry/react'; import { BungieError } from 'app/bungie-api/http-client'; import { getToken } from 'app/bungie-api/oauth-tokens'; import { HashLookupFailure } from 'app/destiny2/definitions'; import { defaultLanguage } from 'app/i18n'; import { PlatformErrorCodes } from 'bungie-api-ts/user'; import { DimError } from './dim-error'; // DIM error codes to ignore and not report. This works regardless of language. const ignoreDimErrors: (string | PlatformErrorCodes)[] = [ 'BungieService.SlowResponse', 'BungieService.Difficulties', 'BungieService.Throttled', 'BungieService.Maintenance', 'BungieService.NotConnected', 'BungieService.NotConnectedOrBlocked', 'ItemService.ExoticError', PlatformErrorCodes.DestinyCannotPerformActionAtThisLocation, ]; const options: BrowserOptions = { enabled: $featureFlags.sentry, dsn: 'https://1367619d45da481b8148dd345c1a1330@sentry.io/279673', release: $DIM_VERSION, environment: $DIM_FLAVOR, ignoreErrors: [], sampleRate: $DIM_VERSION === 'beta' ? 0.5 : 0.01, // Sample Beta at 50%, Prod at 1% attachStacktrace: true, // Only send trace headers to our own server tracePropagationTargets: ['https://api.destinyitemmanager.com'], integrations: [ browserTracingIntegration({ beforeStartSpan: (context) => ({ ...context, // We could use the React-Router integration but it's annoying name: window.location.pathname .replace(/\/\d+\/d(1|2)/g, '/profileMembershipId/d$1') .replace(/\/vendors\/\d+/g, '/vendors/vendorId') .replace(/index\.html/, ''), }), }), ], tracesSampleRate: 0.001, // Performance traces at 0.1% beforeSend: (event, hint) => { const e = hint?.originalException; const underlyingError = e instanceof DimError ? e.cause : undefined; const code = underlyingError instanceof BungieError ? underlyingError.code : e instanceof DimError ? e.code : undefined; if (code && ignoreDimErrors.includes(code)) { return null; // drop report } if (e instanceof HashLookupFailure) { // Add the ID to the fingerprint so we don't collapse different errors event.fingerprint = ['{{ default }}', String(e.table), String(e.id)]; } if (e instanceof DimError) { // Replace the (localized) message with our code let message = e.code; if (e.bungieErrorCode()) { message = `${message} (${e.bungieErrorCode()})`; } event.message = message; // TODO: it might be neat to be able to pass attachments here too - such as the entire profile response! // Do deeper surgery to overwrite the localized message with the code if (event.exception?.values) { for (const ex of event.exception.values) { if (ex.value === e.message) { ex.value = message; } } } event.tags = { ...event.tags, code: e.code, }; if (underlyingError instanceof BungieError) { event.tags = { ...event.tags, bungieErrorCode: underlyingError.code, }; } if (underlyingError) { event.extra = { ...event.extra, cause: underlyingError, }; } } return event; }, }; // TODO: There's a redux integration but I'm worried it'd be too much trouble to trim out all the stuff we wouldn't want to report (by default it sends the whole action & state. // https://docs.sentry.io/platforms/javascript/guides/react/configuration/integrations/redux/ init(options); // Set user ID (membership ID) to help debug and to better count affected users const token = getToken(); if (token?.bungieMembershipId) { setUser({ id: token.bungieMembershipId }); } // Capture locale setTag('lang', defaultLanguage()); /** Sentry.io exception reporting */ export const reportException = (name: string, e: any, errorInfo?: Record<string, unknown>) => { // TODO: we can also do this in some situations to gather more feedback from users // Sentry.showReportDialog(); withScope((scope) => { setTag('context', name); if (errorInfo) { scope.setExtras(errorInfo); } captureException(e); }); }; ================================================ FILE: src/app/utils/socket-utils.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimItem, DimPlug, DimSocket, DimSocketCategory, DimSockets, PluggableInventoryItemDefinition, } from 'app/inventory/item-types'; import { craftedSocketCategoryHash, mementoSocketCategoryHash } from 'app/inventory/store/crafted'; import { isDeepsightResonanceSocket } from 'app/inventory/store/deepsight'; import { D2PlugCategoryByStatHash, GhostActivitySocketTypeHashes, armor2PlugCategoryHashes, weaponComponentPCHs, weaponMasterworkY2SocketTypeHash, } from 'app/search/d2-known-values'; import { DestinyInventoryItemDefinition, DestinySocketCategoryStyle, TierType, } from 'bungie-api-ts/destiny2'; import { emptyPlugHashes } from 'data/d2/empty-plug-hashes'; import { BucketHashes, ItemCategoryHashes, PlugCategoryHashes, SocketCategoryHashes, } from 'data/d2/generated-enums'; import { maxBy } from 'es-toolkit'; import { count, filterMap } from './collections'; import { isKillTrackerSocket } from './item-utils'; type WithRequiredProperty<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]>; }; function getSocketHashesByCategoryStyle( sockets: DimSockets, style: DestinySocketCategoryStyle, ): number[] { const socketCategory = sockets.categories.find( (category) => category.category.categoryStyle === style, ); return (socketCategory && getPlugHashesFromCategory(sockets, socketCategory)) || []; } function getPlugHashesFromCategory(sockets: DimSockets, category: DimSocketCategory) { return getSocketsByIndexes(sockets, category.socketIndexes) .map((socket) => socket.plugged?.plugDef.hash ?? NaN) .filter((val) => !isNaN(val)); } export function getSocketsWithStyle( sockets: DimSockets, style: DestinySocketCategoryStyle, ): DimSocket[] { const socketHashes = getSocketHashesByCategoryStyle(sockets, style); return sockets.allSockets.filter( (socket) => socket.plugged && socketHashes.includes(socket.plugged.plugDef.hash), ); } /** Is this socket a weapon's masterwork socket */ export function isWeaponMasterworkSocket(socket: DimSocket) { return ( socket.plugged?.plugDef.plug && (socket.plugged.plugDef.plug.uiPlugLabel === 'masterwork' || socket.plugged.plugDef.plug.plugCategoryIdentifier.includes('masterworks.stat') || socket.plugged.plugDef.plug.plugCategoryIdentifier.endsWith('_masterwork')) ); } /** Given an item and a list of socketIndexes, find all the sockets that match those indices, in the order the indexes were provided */ export function getSocketsByIndexes(sockets: DimSockets, socketIndexes: number[]) { return filterMap(socketIndexes, (i) => getSocketByIndex(sockets, i)); } /** Given a socketIndex, find the socket that matches that index */ export function getSocketByIndex(sockets: DimSockets, socketIndex: number) { return sockets.allSockets.find((s) => s.socketIndex === socketIndex); } /** Find all sockets on the item that belong to the given category hash */ export function getSocketsByCategoryHash( sockets: DimSockets | null, categoryHash: SocketCategoryHashes, ) { const category = sockets?.categories.find((c) => c.category.hash === categoryHash); if (!category || !sockets) { return []; } return getSocketsByIndexes(sockets, category.socketIndexes); } /** Find all sockets on the item that belong to the given category hash */ export function getSocketsByCategoryHashes( sockets: DimSockets | null, categoryHashes: SocketCategoryHashes[], ) { return categoryHashes.flatMap((categoryHash) => getSocketsByCategoryHash(sockets, categoryHash)); } /** Special case of getSocketsByCategoryHash that returns the first (presumably only) socket that matches the category hash */ export function getFirstSocketByCategoryHash(sockets: DimSockets, categoryHash: number) { const category = sockets?.categories.find((c) => c.category.hash === categoryHash); if (!category) { return undefined; } const socketIndex = category.socketIndexes[0]; return sockets.allSockets.find((s) => s.socketIndex === socketIndex); } function getSocketsByPlugCategoryIdentifier(sockets: DimSockets, plugCategoryIdentifier: string) { return sockets.allSockets.find((socket) => socket.plugged?.plugDef.plug.plugCategoryIdentifier.includes(plugCategoryIdentifier), ); } export function getWeaponArchetypeSocket(item: DimItem): DimSocket | undefined { if (item.bucket.inWeapons && item.sockets) { return getFirstSocketByCategoryHash(item.sockets, SocketCategoryHashes.IntrinsicTraits); } } export const getWeaponArchetype = (item: DimItem): PluggableInventoryItemDefinition | undefined => getWeaponArchetypeSocket(item)?.plugged?.plugDef; export function getIntrinsicArmorPerkSocket(item: DimItem): DimSocket | undefined { if (item.bucket.inArmor && item.sockets) { const largePerkCategory = item.sockets.categories.find( (c) => c.category.hash === SocketCategoryHashes.ArmorPerks_LargePerk, ); if (largePerkCategory) { const largePerkSocket = getSocketByIndex( item.sockets, largePerkCategory.socketIndexes.at(-1)!, ); if (largePerkSocket?.plugged?.plugDef.displayProperties.name) { return largePerkSocket; } } return getSocketsByPlugCategoryIdentifier(item.sockets, 'enhancements.exotic'); } } export function getArmorArchetype(item: DimItem) { return getArmorArchetypeSocket(item)?.plugged?.plugDef; } export function getArmorArchetypeSocket(item: DimItem): DimSocket | undefined { return item.sockets?.allSockets.find((s) => isArmorArchetypeSocket(s)); } function isArmorArchetypeSocket(socket: DimSocket) { return isArmorArchetypePlug(socket.plugged); } export function isArmorArchetypePlug(plug: DimPlug | DestinyInventoryItemDefinition | null) { const plugDef = plug && 'plugDef' in plug ? plug.plugDef : plug; return Boolean( plugDef?.plug?.plugCategoryHash === PlugCategoryHashes.ArmorArchetypes && plugDef.displayProperties.name, ); } /** * returns sockets that contains intrinsic perks even if those sockets are not * the "Intrinsic" socket. This handles the exotic class items that were added * in The Final Shape. Note that items with a normal intrinsic perk (so far) * won't have anything here, while exotic class items (so far) don't have a * normal intrinsic. */ export function getExtraIntrinsicPerkSockets(item: DimItem): DimSocket[] { if (!item.sockets) { return []; } return [ ...(item.isExotic && item.bucket.hash === BucketHashes.ClassArmor ? item.sockets.allSockets .filter((s) => s.isPerk && s.visibleInGame && socketContainsIntrinsicPlug(s)) // exotic class item intrinsics need to set isReusable false to avoid showing as selectable .map((s) => ({ ...s, isReusable: false })) : []), ]; } export function socketContainsPlugWithCategory( socket: DimSocket, category: PlugCategoryHashes, ): socket is WithRequiredProperty<DimSocket, 'plugged'> { // the above type predicate removes the need to null-check `plugged` after this call return socket.plugged?.plugDef.plug.plugCategoryHash === category; } /** * the "intrinsic" plug type is: * - weapon frames * - exotic weapon archetypes * - exotic armor special effect plugs * - the special invisible plugs that contribute to armor 2.0 stat rolls */ export function socketContainsIntrinsicPlug( socket: DimSocket, ): socket is WithRequiredProperty<DimSocket, 'plugged'> { // the above type predicate removes the need to null-check `plugged` after this call return ( socketContainsPlugWithCategory(socket, PlugCategoryHashes.Intrinsics) || socketContainsPlugWithCategory(socket, PlugCategoryHashes.ArmorStats) ); } /** * Is this one of the plugs that could possibly fit into this socket? This does not * check whether the plug is enabled or unlocked - only that it appears in the list of * possible plugs. */ export function plugFitsIntoSocket(socket: DimSocket, plugHash: number) { return ( socket.emptyPlugItemHash === plugHash || socket.plugSet?.plugs.some((dimPlug) => dimPlug.plugDef.hash === plugHash) || // TODO(#7793): This should use reusablePlugItems on the socket def // because the check should operate on static definitions. This is still // incorrect for quite a few blue-quality items because DIM throws away the data. socket.reusablePlugItems?.some((p) => p.plugItemHash === plugHash) ); } /** * Abilities and supers are "choice sockets", there might be a default * but it's not really a meaningful empty or reset option. * Still, this can be a useful to initialize user selections. */ export function getDefaultAbilityChoiceHash(socket: DimSocket) { const { singleInitialItemHash } = socket.socketDefinition; return singleInitialItemHash ? singleInitialItemHash : // Some sockets like Void 3.0 grenades don't have a singleInitialItemHash socket.plugSet!.plugs[0].plugDef.hash; } export const eventArmorRerollSocketIdentifiers: string[] = ['events.solstice.']; /** * With Solstice 2022, event armor has a ton of sockets for stat rerolling * and they take up a lot of space. No idea if this system will be around for * other armor but if it does, just add to this function. */ export function isEventArmorRerollSocket(socket: DimSocket) { return eventArmorRerollSocketIdentifiers.some((i) => socket.plugged?.plugDef.plug.plugCategoryIdentifier.startsWith(i), ); } export function isEnhancedPerk(plugDef: PluggableInventoryItemDefinition) { return ( plugDef.inventory!.tierType === TierType.Common && (plugDef.plug.plugCategoryHash === PlugCategoryHashes.Frames || plugDef.plug.plugCategoryHash === PlugCategoryHashes.Origins || weaponComponentPCHs.has(plugDef.plug.plugCategoryHash)) ); } export function countEnhancedPerks(sockets: DimSockets) { return count(sockets.allSockets, (s) => s.plugged && isEnhancedPerk(s.plugged.plugDef)); } export const aspectSocketCategoryHashes: SocketCategoryHashes[] = [ SocketCategoryHashes.Aspects_Abilities_Ikora, SocketCategoryHashes.Aspects_Abilities_Neomuna, SocketCategoryHashes.Aspects_Abilities_Stranger, SocketCategoryHashes.Aspects_Abilities, ]; export const fragmentSocketCategoryHashes: SocketCategoryHashes[] = [ SocketCategoryHashes.Fragments_Abilities_Ikora, SocketCategoryHashes.Fragments_Abilities_Stranger, SocketCategoryHashes.Fragments_Abilities_Neomuna, SocketCategoryHashes.Fragments_Abilities, ]; export const subclassAbilitySocketCategoryHashes: SocketCategoryHashes[] = [ SocketCategoryHashes.Abilities_Abilities, SocketCategoryHashes.Abilities_Abilities_Ikora, SocketCategoryHashes.Super, ]; export function isModCostVisible(plug: PluggableInventoryItemDefinition): boolean { return ( // hide cost if it's less than 1 (plug.plug.energyCost?.energyCost ?? 0) >= 1 && // subclass stuff is always 1 !plug.plug.plugCategoryIdentifier.endsWith('.fragments') && !plug.plug.plugCategoryIdentifier.endsWith('.trinkets') && // artifact unlocks happen to have the armor PCHs, but don't have // the "armor mod" ICH because they don't go in armor !( armor2PlugCategoryHashes.includes(plug.plug.plugCategoryHash) && !plug.itemCategoryHashes?.includes(ItemCategoryHashes.ArmorMods) ) ); } const ARMOR_STAT_CATEGORYSTYLE = 2251952357; function filterSocketCategories( categories: DimSocketCategory[], sockets: DimSockets, allowCategory: (cat: DimSocketCategory) => boolean, allowSocket: (socket: DimSocket) => boolean, ): Map<DimSocketCategory, DimSocket[]> { // Pre-calculate the list of sockets we'll display for each category const socketsByCategory = new Map<DimSocketCategory, DimSocket[]>(); for (const category of categories) { if (!allowCategory(category)) { continue; } const categorySockets = getSocketsByIndexes(sockets, category.socketIndexes).filter( (socketInfo) => (socketInfo.plugged || socketInfo.plugOptions[0])?.plugDef.displayProperties.name && allowSocket(socketInfo), ); if (categorySockets.length) { socketsByCategory.set(category, categorySockets); } } return socketsByCategory; } /** * Should this socket be excluded when we filter out empty sockets? * This shows empty catalyst sockets when the weapon has a catalyst * because it is useful info... */ export function isSocketEmpty(socket: DimSocket) { return ( socket.plugged && (socket.plugged.plugDef.hash === socket.emptyPlugItemHash || emptyPlugHashes.has(socket.plugged?.plugDef.hash)) && socket.plugged.plugDef.plug.plugCategoryHash !== PlugCategoryHashes.V400EmptyExoticMasterwork ); } export interface DisplayedSockets { intrinsicSocket?: DimSocket; perks?: DimSocketCategory; modSocketsByCategory: Map<DimSocketCategory, DimSocket[]>; } export function getDisplayedItemSockets( item: DimItem, excludeEmptySockets = false, ): DisplayedSockets | undefined { if (item.bucket.inWeapons) { return getWeaponSockets(item, { excludeEmptySockets }); } else { return getGeneralSockets(item, excludeEmptySockets); } } export function getSocketsByType( item: DimItem, type?: 'all' | 'traits' | 'cosmetics' | 'origin' | 'mods' | 'perks' | 'components', ): DimSocket[] { if (!item.sockets) { return []; } let sockets = []; const { modSocketsByCategory, perks } = getDisplayedItemSockets( item, /* excludeEmptySockets */ true, )!; if (perks) { sockets.push(...getSocketsByIndexes(item.sockets, perks.socketIndexes)); } switch (type) { case 'traits': sockets = sockets.filter(socketIsTrait); break; case 'origin': sockets = sockets.filter(socketIsOriginTrait); break; case 'cosmetics': { sockets.push(...[...modSocketsByCategory.values()].flat()); sockets = sockets.filter(socketIsCosmetic); break; } case 'mods': { sockets.push(...[...modSocketsByCategory.values()].flat()); sockets = sockets.filter(socketIsMod); break; } case 'components': { sockets.push(...[...modSocketsByCategory.values()].flat()); sockets = sockets.filter(socketIsWeaponComponent); break; } case 'perks': { sockets.push(...[...modSocketsByCategory.values()].flat()); sockets = sockets.filter(socketIsPerk); break; } default: { // Improve this when we use iterator-helpers sockets.push(...[...modSocketsByCategory.values()].flat()); sockets = sockets.filter( (s) => !( s.plugged && (s.plugged?.plugDef.itemCategoryHashes?.includes( ItemCategoryHashes.WeaponModsOriginTraits, ) || s.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Frames || s.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Intrinsics || s.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Shader || s.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Mementos || s.plugged.plugDef.plug.plugCategoryIdentifier.includes('skin')) ), ); break; } } sockets = sockets.filter( (s) => // we have a separate column for the kill tracker !isKillTrackerSocket(s) && // and for the regular weapon masterworks s.socketDefinition.socketTypeHash !== weaponMasterworkY2SocketTypeHash && // Remove "extra intrinsics" for exotic class items (!item.bucket.inArmor || !(s.isPerk && s.visibleInGame && socketContainsIntrinsicPlug(s))), ); return sockets; } export function getWeaponSockets( item: DimItem, options: { excludeEmptySockets?: boolean; includeFakeMasterwork?: boolean; }, ): DisplayedSockets | undefined { const { excludeEmptySockets = false, includeFakeMasterwork = false } = options; if (!item.sockets) { return undefined; } const archetypeSocket = getWeaponArchetypeSocket(item); const perks = item.sockets.categories.find( (c) => c.category.hash !== SocketCategoryHashes.IntrinsicTraits && c.socketIndexes.length && getSocketByIndex(item.sockets!, c.socketIndexes[0])?.isPerk, ); const excludedSocketCategoryHashes = [ craftedSocketCategoryHash, !item.crafted && mementoSocketCategoryHash, ]; const excludedPlugCategoryHashes = [ PlugCategoryHashes.GenericAllVfx, PlugCategoryHashes.CraftingPlugsWeaponsModsEnhancers, PlugCategoryHashes.CraftingPlugsWeaponsModsExtractors, // The weapon level socket is not interesting PlugCategoryHashes.CraftingPlugsWeaponsModsTransfusersLevel, // Hide catalyst socket for exotics with no known catalyst !item.catalystInfo && PlugCategoryHashes.V400EmptyExoticMasterwork, PlugCategoryHashes.V300WeaponDamageTypeEnergy, PlugCategoryHashes.V300WeaponDamageTypeAttack, PlugCategoryHashes.V300WeaponDamageTypeKinetic, ]; let moddedSockets: DimSockets = item.sockets; if (includeFakeMasterwork) { const allSockets = item.sockets.allSockets.map((socket) => { if (socket.socketDefinition.socketTypeHash !== weaponMasterworkY2SocketTypeHash) { return socket; } const mwHash = item.masterworkInfo?.stats?.find((s) => s.isPrimary)?.hash || 0; const plugCategory = D2PlugCategoryByStatHash.get(mwHash); let fullMasterworkPlug = socket.plugSet && plugCategory && maxBy( socket.plugSet.plugs.filter((p) => p.plugDef.plug.plugCategoryHash === plugCategory), (plugOption) => plugOption.plugDef.investmentStats[0]?.value, ); if (!fullMasterworkPlug) { return socket; } fullMasterworkPlug = { ...fullMasterworkPlug, plugDef: { ...fullMasterworkPlug.plugDef, displayProperties: { ...fullMasterworkPlug.plugDef.displayProperties, iconHash: 0, // use legacy icon so we can remove the '10' on it }, iconWatermark: '', // remove the '10' in the top right of the icon investmentStats: [], // remove the stats from the fake plug }, }; return { ...socket, plugged: fullMasterworkPlug, plugOptions: [fullMasterworkPlug], visibleInGame: true, reusablePlugItems: [], isPerk: true, // isPerk is required to prevent the socket from being selectable/modifiable }; }); moddedSockets = { ...item.sockets, allSockets, }; } const modSocketsByCategory = filterSocketCategories( moddedSockets.categories.toReversed(), moddedSockets, (category) => !excludedSocketCategoryHashes.includes(category.category.hash) && category !== perks, (socket) => (!excludeEmptySockets || !isSocketEmpty(socket)) && socket.plugged !== null && !excludedPlugCategoryHashes.includes(socket.plugged.plugDef.plug.plugCategoryHash) && socket !== archetypeSocket && !isDeepsightResonanceSocket(socket) && // only show memento socket if it isn't empty (socket.plugged.plugDef.plug.plugCategoryHash !== PlugCategoryHashes.CraftingRecipesEmptySocket || !isSocketEmpty(socket)), ); return { intrinsicSocket: archetypeSocket, perks, modSocketsByCategory, }; } // Sometimes we trust Bungie's advertised socket visibility information export const trustBungieVisibility = new Set<PlugCategoryHashes | undefined>([ // Artifice slots the game has marked as not visible (on un-upgraded exotics) PlugCategoryHashes.EnhancementsArtifice, // Stat tuning mod slots on all Armor 3.0 but only available at Tier 5 PlugCategoryHashes.CoreGearSystemsArmorTieringPlugsTuningMods, ]); export function getGeneralSockets( item: DimItem, excludeEmptySockets = false, ): Omit<DisplayedSockets, 'perks'> | undefined { if (!item.sockets) { return undefined; } const intrinsicSocket = getIntrinsicArmorPerkSocket(item); const isAllowedCategory = (c: DimSocketCategory) => // hide if this is the energy slot. it's already displayed in ItemDetails c.category.categoryStyle !== DestinySocketCategoryStyle.EnergyMeter && // hide if this is the emote wheel because we show it separately c.category.hash !== SocketCategoryHashes.Emotes && // Hidden sockets for intrinsic armor stats c.category.uiCategoryStyle !== ARMOR_STAT_CATEGORYSTYLE; const isAllowedSocket = (socketInfo: DimSocket) => socketInfo.socketIndex !== intrinsicSocket?.socketIndex && (!excludeEmptySockets || !isSocketEmpty(socketInfo)) && // don't include these weird little solstice stat rerolling mechanic sockets !isEventArmorRerollSocket(socketInfo) && // never include the "pay for artifice upgrade" slot on exotic armor socketInfo.plugged?.plugDef.plug.plugCategoryHash !== PlugCategoryHashes.EnhancementsArtificeExotic && // Hide armor masterwork payment socket for armor 2.0 since it's the same as the energy bar for them. (item.tier > 0 || !socketInfo.plugged?.plugDef.plug.plugCategoryIdentifier.startsWith( 'v460.plugs.armor.masterworks', )) && !( !socketInfo.visibleInGame && trustBungieVisibility.has(socketInfo.plugged?.plugDef.plug.plugCategoryHash) ) && // Ghost shells unlock an activity mod slot when masterworked and hide the dummy locked slot (item.bucket.hash !== BucketHashes.Ghost || socketInfo.socketDefinition.socketTypeHash !== (item.masterwork ? GhostActivitySocketTypeHashes.Locked : GhostActivitySocketTypeHashes.Unlocked)); const modSocketsByCategory = filterSocketCategories( item.sockets.categories, item.sockets, isAllowedCategory, isAllowedSocket, ); return { intrinsicSocket, modSocketsByCategory, }; } /** * Determine the perk selections that correspond to the "curated" roll for this socket. */ function getCuratedRollForSocket(defs: D2ManifestDefinitions, socket: DimSocket) { // We only build a larger list of plug options if this is a perk socket, since users would // only want to see (and search) the plug options for perks. For other socket types (mods, shaders, etc.) // we will only populate plugOptions with the currently inserted plug. const socketDef = socket.socketDefinition; let curatedRoll: number[] | null = null; if (socket.isPerk) { if (socketDef.reusablePlugSetHash) { // Get options from plug set, instead of live info const plugSet = defs.PlugSet.get(socketDef.reusablePlugSetHash); if (plugSet) { curatedRoll = plugSet.reusablePlugItems.map((p) => p.plugItemHash); } } else if (socketDef.reusablePlugItems) { curatedRoll = socketDef.reusablePlugItems.map((p) => p.plugItemHash); } } return curatedRoll; } /** Determine if the item has a curated roll, and if all of its perks match that curated roll. */ export function matchesCuratedRoll(defs: D2ManifestDefinitions, item: DimItem) { const legendaryWeapon = item.bucket?.sort === 'Weapons' && item.rarity === 'Legendary'; if (!legendaryWeapon) { return false; } const matchesCollectionsRoll = item.sockets?.allSockets // curatedRoll is only set for perk-style sockets .filter((socket) => socket.isPerk && socket.plugOptions.length && !isKillTrackerSocket(socket)) .map((socket) => ({ socket, curatedRoll: getCuratedRollForSocket(defs, socket), })) .filter(({ curatedRoll }) => curatedRoll) .every( ({ socket, curatedRoll }) => curatedRoll!.length === socket.plugOptions.length && socket.plugOptions.every((option, idx) => option.plugDef.hash === curatedRoll![idx]), ); return matchesCollectionsRoll; } /** Finds the item's tuning socket if it's enabled. This socket can slightly modify the armor's stats. */ export function getArmor3TuningSocket(item: DimItem): DimSocket | undefined { return item.sockets?.allSockets.find( (s) => // Ensures the socket is active (Tier 5 armor) s.visibleInGame && // Even the "empty slot" placeholder has the right plugCategoryHash s.plugged?.plugDef.plug.plugCategoryHash === PlugCategoryHashes.CoreGearSystemsArmorTieringPlugsTuningMods, ); } // // TODO: Use these functions other places in DIM where sockets are identified. // // TODO: This is wrong/insufficient. This would also find "Aggressive Frame." Upstream filtering is the only thing keeping this "correct". /** * Identifies a Trait. Think "Rampage" or "Subsistence". */ function socketIsTrait(socket: DimSocket) { return ( socket.plugged && (socket.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Frames || socket.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Intrinsics) ); } // TODO: Stop trusting itemCategoryHashes /** * Identifies an Origin Trait, introduced in Witch Queen, fixed Traits associated with sets of weapons. */ function socketIsOriginTrait(socket: DimSocket) { return socket.plugged?.plugDef.itemCategoryHashes?.includes( ItemCategoryHashes.WeaponModsOriginTraits, ); } // TODO: Stop trusting itemCategoryHashes // TODO: This would fail to find Enhanced Impact mod, due to the above, even though it finds other mods. /** * This could be almost anything. It's unclear what this is useful for. */ function socketIsPerk(socket: DimSocket) { return ( socket.plugged && socket.isPerk && (socket.plugged.plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.WeaponMods) || socket.plugged.plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.ArmorMods)) ); } // TODO: This should also find Combat Flair sockets? /** * This identifies shaders, mementos, and ornaments. */ function socketIsCosmetic(socket: DimSocket) { return ( socket.plugged && (socket.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Shader || socket.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Mementos || socket.plugged.plugDef.plug.plugCategoryIdentifier.includes('skin')) ); } // TODO: Stop trusting itemCategoryHashes // TODO: This would fail to find Enhanced Impact mod, due to the above, even though it finds other mods. // TODO: Improve this. /** * Find a square-looking mod on an item. * Things like "Mobility Mod" or "Stasis Targeting" or "Backup Mag". */ function socketIsMod(socket: DimSocket) { return ( socket.plugged && !socket.isPerk && (socket.plugged.plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.WeaponMods) || socket.plugged.plugDef.itemCategoryHashes?.includes(ItemCategoryHashes.ArmorMods)) && !( socket.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Shader || socket.plugged.plugDef.plug.plugCategoryHash === PlugCategoryHashes.Mementos || socket.plugged.plugDef.plug.plugCategoryIdentifier.includes('skin') ) ); } /** * Check to see if a socket contains a barrel, mag, etc. * A plug that contributes to a weapon's stats, but aren't its base stats, traits, or mods. */ function socketIsWeaponComponent(socket: DimSocket) { return weaponComponentPCHs.has(socket.plugged?.plugDef.plug.plugCategoryHash); } /** * Gets weapon components, like barrels, mags, etc. * Sockets that contribute to a weapon's stats, but aren't its base stats, traits, or mods. */ export function getWeaponComponentSockets(item: DimItem) { return (item.sockets?.allSockets ?? []).filter(socketIsWeaponComponent); } ================================================ FILE: src/app/utils/stats-set.test.ts ================================================ import { StatsSet } from './stats-set'; test('insertAll 1', () => { const inputs: [number[], string][] = [ [[1, 2, 3], 'a'], [[1, 1, 2], 'b'], [[1, 1, 1], 'c'], ]; const statsSet = new StatsSet<string>(); for (const [stats, item] of inputs) { statsSet.insert(stats, item); } // expect(statsSet).toMatchSnapshot('internal'); expect(statsSet.doBetterStatsExist([1, 2, 3])).toBe(false); expect(statsSet.doBetterStatsExist([1, 1, 2])).toBe(true); expect(statsSet.doBetterStatsExist([1, 1, 3])).toBe(true); expect(statsSet.doBetterStatsExist([1, 2, 2])).toBe(true); expect(statsSet.doBetterStatsExist([2, 1, 1])).toBe(false); expect(statsSet.doBetterStatsExist([1, 4, 0])).toBe(false); expect(statsSet.doBetterStatsExist([0, 0, 0])).toBe(true); }); ================================================ FILE: src/app/utils/stats-set.ts ================================================ /** * The internal structure of StatsSet is a trie (https://en.wikipedia.org/wiki/Trie) where * each level represents a consecutive stat in a stat array. */ interface StatNode<T> { /** The value of the stat */ value: number; /** The next set of stats. This array is ordered by the stat nodes' value, in decreasing order. */ next: StatNode<T>[]; /** The terminal node holds the actual inserted items */ items?: T[]; } /** * Result helper for distinguishing between different cases in the better stats helper */ const enum BetterStatsResult { // There exists a known set of stats with at least one higher stat than the input, and NO lower stats BETTER_STATS_EXIST = -1, // There exists a known set of stats with exactly the same stats as the input, but nothing better SAME = 0, // The input has at least one stat higher than any known set of stats HIGHER_STAT = 1, } /** * A StatsSet can be populated with a bunch of stats, and can then answer questions such as: * 1. Have we seen stats that are strictly better than the input stats? * 2. Get all the items with lower stats than the input stats. * * In general "stats" are just an ordered array of numbers, and can represent anything - item stats, set tiers, etc. */ export class StatsSet<T> { statNodes: StatNode<T>[] = []; /** * Insert a new line of stats and an associated value into the set. Input * stats must always be in the same order and have the same length. */ insert(stats: number[], item: T) { let currentNodes = this.statNodes; for (let statIndex = 0; statIndex < stats.length; statIndex++) { const stat = stats[statIndex]; const [insertionIndex, found] = findNode(currentNodes, stat); if (!found) { currentNodes.splice(insertionIndex, 0, { value: stat, next: [], }); } if (statIndex === stats.length - 1) { (currentNodes[insertionIndex].items ||= []).push(item); } currentNodes = currentNodes[insertionIndex].next; } } /** * Given all the stats this set knows about, are there any stats which are better than * this one? For this to be true at least one stat array that was inserted must have each * stat be better or equal to the input, without being equal to the input stats. For example, * if the stat set contains: * * ``` * { * [1, 2, 3], * [1, 1, 2], * [1, 1, 1] * } * ``` * * Then: * * * `doBetterStatsExist([1, 2, 3]) === false` * * `doBetterStatsExist([1, 2, 2]) === true` * * `doBetterStatsExist([2, 1, 1]) === false` * * See tests for more examples. */ doBetterStatsExist(stats: number[]) { // See if the input stats are lower than some other known set return betterStatsHelper(this.statNodes, stats, 0) === BetterStatsResult.BETTER_STATS_EXIST; } } /** * Return whether the input stats are higher, lower, or the same than stats we know about, in a recursive fashion. */ function betterStatsHelper<T>( nodes: StatNode<T>[], stats: number[], statIndex: number, ): BetterStatsResult { const stat = stats[statIndex]; // Iterate all nodes in descending value until the value is lower than our stat for (const node of nodes) { if (node.value < stat) { // If we get here and haven't returned, then the input has at least one stat better than any known set in this subtree return BetterStatsResult.HIGHER_STAT; } // At this point node.value is definitely >= stat // If this is the leaf nodes, just return based on the node value vs. our stat if (node.items) { return node.value > stat ? BetterStatsResult.BETTER_STATS_EXIST : node.value === stat ? BetterStatsResult.SAME : BetterStatsResult.HIGHER_STAT; } // Evaluate the subtree from this node const subResult = betterStatsHelper(node.next, stats, statIndex + 1); switch (subResult) { case BetterStatsResult.BETTER_STATS_EXIST: // If better stats exist in the subtree, and this node is better or the same as our input, // then this subtree contains a better stat set. return BetterStatsResult.BETTER_STATS_EXIST; case BetterStatsResult.SAME: // If our stats are exactly the same in the subtree, then it comes down // to whether our stat is higher than this node's value or if it's // equal. return node.value === stat ? BetterStatsResult.SAME : BetterStatsResult.BETTER_STATS_EXIST; case BetterStatsResult.HIGHER_STAT: // - Keep looking, this subtree didn't pan out because our input stat was higher than some of them } } // This can happen if nodes is empty - in which case we consider the input higher than any known stat since none are known return BetterStatsResult.HIGHER_STAT; } /** * Find a given node in a list by its value. Returns the index it was found and whether it was found. * If it wasn't found, the index is where it should be inserted. */ function findNode<T>(nodes: StatNode<T>[], val: number): [index: number, found: boolean] { // TODO: I guess re-inline this? It didn't end up being as reusable as I was hoping. // TODO: binary search (later). Right now it's just insertion sort if (nodes.length === 0) { return [0, false]; } else { for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) { const node = nodes[nodeIndex]; if (val > node.value) { return [nodeIndex, false]; } else if (val === node.value) { return [nodeIndex, true]; } } return [nodes.length, false]; } } ================================================ FILE: src/app/utils/stats.ts ================================================ import { AssumeArmorMasterwork, CustomStatWeights } from '@destinyitemmanager/dim-api-types'; import { DimItem } from 'app/inventory/item-types'; import { calculateAssumedMasterworkStats } from 'app/loadout-drawer/loadout-utils'; import { armorStats } from 'app/search/d2-known-values'; import { filterMap } from 'app/utils/collections'; import { getArmor3TuningStat, isArtifice } from 'app/utils/item-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { StatsSet } from './stats-set'; /** * Compute a set of items that are "stat lower" dupes. These are items for which * there exists another item with strictly better stats (i.e. better in at least * one stat and not worse in any stat). */ export function computeStatDupeLower( allItems: DimItem[], /** By default, the 6 armor stats. To optimize a custom stat, a subset is passed. */ relevantArmorStatHashes: number[] = armorStats, /** * A optional mapping function to get stats and their hashes from an item. * Otherwise, grabs hypothetical masterworked stat values for armor stats. * * This MAY CONTAIN stats that aren't in relevantArmorStatHashes. * They'll still be compared as statmixes, but missing from relevantArmorStatHashes means * they get ignored during the armor stat adjustment layer. * * This is used from LO to override the masterwork assumption behavior. */ getArmorStats?: (item: DimItem) => { statHash: number; value: number }[], ) { // disregard no-class armor const armor = allItems.filter((i) => i.bucket.inArmor && i.classType !== DestinyClass.Classified); getArmorStats ??= (item: DimItem) => { // Always compare items as if they were fully masterworked const masterworkedStatValues = calculateAssumedMasterworkStats(item, { assumeArmorMasterwork: AssumeArmorMasterwork.All, minItemEnergy: 1, }); return relevantArmorStatHashes.map((statHash) => ({ statHash, value: masterworkedStatValues[statHash] ?? 0, })); }; // A mapping from an item to a list of all of its stat configurations (artifice/tunable armor can have multiple). // This is just a cache to prevent recalculating it. const statsCache = new Map<DimItem, number[][]>(); for (const item of armor) { if (item.stats && item.power) { const optimizingStats = getArmorStats(item); const statMixes = [optimizingStats.map((s) => s.value)]; // Start with the unadjusted stat mix. const tuningStatHash = getArmor3TuningStat(item); if (tuningStatHash) { // If we can tune to benefit a relevant hash, include tuning mod tradeoff stat mixes. if (relevantArmorStatHashes.includes(tuningStatHash)) { // For each relevant hash (that isn't the tuned stat), // generate a variation where the tuning mod was used to raise the tuned stat, and lower this stat for (const loweredStatHash of relevantArmorStatHashes) { if (loweredStatHash !== tuningStatHash) { statMixes.push( optimizingStats.map(({ value, statHash }) => statHash === tuningStatHash ? value + 5 : loweredStatHash === statHash ? value - 5 : value, ), ); } } } // Stat hashes that would be affected if a Balanced Tuning Mod were applied (those with masterworked value 5 (the base was 0)) const balancedTuningStatHashes = filterMap(optimizingStats, ({ statHash, value }) => value === 5 ? statHash : undefined, ); // If we can apply Balanced Tuning Mod to benefit a relevant hash, include that stat mix. if (relevantArmorStatHashes.some((h) => balancedTuningStatHashes.includes(h))) { statMixes.push( optimizingStats.map(({ value, statHash }) => balancedTuningStatHashes.includes(statHash) ? value + 1 : value, ), ); } } else if (isArtifice(item)) { // ^ We assume armor cannot be both artifice and tunable. // Artifice armor can be +3 in any one stat, so we compute a separate // stat mix each with the relevant stat boosted and the others normal. for (const relevantStatHash of relevantArmorStatHashes) { statMixes.push( optimizingStats.map(({ value, statHash }) => relevantStatHash === statHash ? value + 3 : value, ), ); } } statsCache.set(item, statMixes); } } const dupes = new Set<string>(); // Group by class and armor type. Also, compare exotics with each other, not the general pool. const grouped = Object.values( Object.groupBy(armor, (i) => `${i.bucket.hash}-${i.classType}-${i.isExotic ? i.hash : ''}`), ); for (const group of grouped) { const statSet = new StatsSet<DimItem>(); // Add a mapping from stats => item to the statsSet for each item in the group for (const item of group) { const stats = statsCache.get(item); if (stats) { for (const statValues of stats) { statSet.insert(statValues, item); } } } // Now run through the items in the group again, checking against the fully // populated stats set to see if there's something better for (const item of group) { const stats = statsCache.get(item); // All configurations must have a better version somewhere for this to count as statlower if (stats?.every((statValues) => statSet.doBetterStatsExist(statValues))) { dupes.add(item.id); } } } return dupes; } export function collectRelevantStatHashes(weights: CustomStatWeights) { const relevantStatHashes: number[] = []; for (const statHash in weights) { const weight = weights[statHash]; if (weight && weight > 0) { relevantStatHashes.push(parseInt(statHash, 10)); } } return relevantStatHashes; } ================================================ FILE: src/app/utils/system-info.ts ================================================ import UAParser from 'ua-parser-js'; export const [systemInfo, browserName, browserVersion] = getSystemInfo(); function getSystemInfo() { const parser = new UAParser(); const { name: browserName, version: browserVersion } = parser.getBrowser(); const { name: osName, version: osVersion } = parser.getOS(); const userAgent = parser.getUA(); const dimAppStoreIndex = userAgent.indexOf('DIM AppStore'); let browserInfo = `${browserName} ${browserVersion}`; if (dimAppStoreIndex >= 0) { browserInfo = userAgent.substring(dimAppStoreIndex); } const info = `${browserInfo} - ${osName} ${osVersion}`; return [info, browserName, browserVersion] as const; } ================================================ FILE: src/app/utils/temp-container.scss ================================================ @use '../variables.scss' as *; #temp-container { // Position this at the very top of the screen. Most things won't care // (they're absolutely positioned themselves, but @textcomplete doesn't know // how to position itself within a positioning context that isn't the // document. position: absolute; top: 0; width: 100%; // Make sure things in the temp container display above others z-index: $tempContainerZindex; } ================================================ FILE: src/app/utils/temp-container.ts ================================================ import React from 'react'; import { createPortal } from 'react-dom'; import './temp-container.scss'; /** * A guaranteed-present element for attaching temporary elements to instead of * document.body. Using document.body triggers expensive style recalcs, at least * in Chrome. */ export const tempContainer = document.getElementById('temp-container')!; /** * Render the given children near the root of the page instead of in their existing component hierarchy. */ export function Portal({ children }: { children: React.ReactNode }) { return createPortal(children, tempContainer); } ================================================ FILE: src/app/utils/textarea-caret.ts ================================================ /* eslint-disable */ // The @textcomplete library uses this, but DIM wants to override it (via an // alias in Webpack) to have it append its test element to our tempContainer to // avoid expensive layout recalculation. // We'll copy the properties below into the mirror div. // Note that some browsers, such as Firefox, do not concatenate properties // into their shorthand (e.g. padding-top, padding-bottom etc. -> padding), import { tempContainer } from './temp-container'; // so we have to list every single property explicitly. const properties = [ 'direction', // RTL support 'boxSizing', 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does 'height', 'overflowX', 'overflowY', // copy the scrollbar for IE 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderStyle', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', // https://developer.mozilla.org/en-US/docs/Web/CSS/font 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform', 'textIndent', 'textDecoration', // might not make a difference, but better be safe 'letterSpacing', 'wordSpacing', 'tabSize', 'MozTabSize', ]; const isBrowser = typeof window !== 'undefined'; const isFirefox = isBrowser && (window as any).mozInnerScreenX !== null; export default function getCaretCoordinates( element: HTMLTextAreaElement | HTMLInputElement, position: number, ) { if (!isBrowser) { throw new Error( 'textarea-caret-position#getCaretCoordinates should only be called in a browser', ); } // The mirror div will replicate the textarea's style const div = document.createElement('div'); div.id = 'input-textarea-caret-position-mirror-div'; const style = div.style; const computed = window.getComputedStyle(element); const isInput = element.nodeName === 'INPUT'; // Default textarea styles style.whiteSpace = 'pre-wrap'; if (!isInput) { style.wordWrap = 'break-word'; } // only for textarea-s // Position off-screen style.position = 'absolute'; // required to return coordinates properly style.visibility = 'hidden'; // not 'display: none' because we want rendering // Transfer the element's properties to the div properties.forEach(function (prop) { if (isInput && prop === 'lineHeight') { // Special case for <input>s because text is rendered centered and line height may be != height if (computed.boxSizing === 'border-box') { const height = parseInt(computed.height); const outerHeight = parseInt(computed.paddingTop) + parseInt(computed.paddingBottom) + parseInt(computed.borderTopWidth) + parseInt(computed.borderBottomWidth); const targetHeight = outerHeight + parseInt(computed.lineHeight); if (height > targetHeight) { style.lineHeight = height - outerHeight + 'px'; } else if (height === targetHeight) { style.lineHeight = computed.lineHeight; } else { style.lineHeight = '0'; } } else { style.lineHeight = computed.height; } } else { // @ts-expect-error(7015) style[prop] = computed[prop]; } }); if (isFirefox) { // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275 if (element.scrollHeight > parseInt(computed.height)) { style.overflowY = 'scroll'; } } else { style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' } div.textContent = element.value.substring(0, position); // The second special handling for input type="text" vs textarea: // spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037 if (isInput) { div.textContent = div.textContent.replace(/\s/g, '\u00a0'); } const span = document.createElement('span'); // Wrapping must be replicated *exactly*, including when a long word gets // onto the next line, with whitespace at the end of the line before (#7). // The *only* reliable way to do that is to copy the *entire* rest of the // textarea's content into the <span> created at the caret position. // For inputs, just '.' would be enough, but no need to bother. span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all div.appendChild(span); // DIM: append to this element instead of body to avoid expensive recalcs const parent = tempContainer; parent.appendChild(div); const coordinates = { top: span.offsetTop + parseInt(computed['borderTopWidth']), left: span.offsetLeft + parseInt(computed['borderLeftWidth']), height: parseInt(computed['lineHeight']), }; parent.removeChild(div); return coordinates; } ================================================ FILE: src/app/utils/time.test.ts ================================================ import i18next from 'i18next'; import { setupi18n } from 'testing/test-utils'; import { i15dDurationFromMs, i15dDurationFromMsWithSeconds, timerDurationFromMs, timerDurationFromMsWithDecimal, } from './time'; beforeAll(() => { setupi18n(); }); test.each([ [1000, '0:00:01'], [0, '0:00:00'], [279241234, '3:05:34:01'], [20041234, '5:34:01'], ])('timerDurationFromMs(%s) === "%s"', (timestamp, expected) => { expect(timerDurationFromMs(timestamp)).toBe(expected); }); test.each([ [1000, '0:01'], [0, '0:00'], [279241234, '3:05:34:01.234'], [20041234, '5:34:01.234'], ])('timerDurationFromMs(%s) === "%s"', (timestamp, expected) => { expect(timerDurationFromMsWithDecimal(timestamp)).toBe(expected); }); describe('english localization', () => { beforeEach(() => { i18next.changeLanguage('en'); }); test.each([ [1000, '0:00'], [0, '0:00'], [86400000, '1 Day 0:00'], [279241234, '3 Days 5:34'], [20041234, '5:34'], ])('i15dDurationFromMs(%s) === "%s"', (timestamp, expected) => { expect(i15dDurationFromMs(timestamp)).toBe(expected); }); test.each([ [1000, '0:00'], [0, '0:00'], [86400000, '1d 0:00'], [279241234, '3d 5:34'], [20041234, '5:34'], ])('i15dDurationFromMs(%s) === "%s"', (timestamp, expected) => { expect(i15dDurationFromMs(timestamp, true)).toBe(expected); }); test.each([ [1000, '0:00:01'], [0, '0:00:00'], [279241234, '3 Days 5:34:01'], [20041234, '5:34:01'], ])('i15dDurationFromMs(%s) === "%s"', (timestamp, expected) => { expect(i15dDurationFromMsWithSeconds(timestamp)).toBe(expected); }); }); describe('japanese localization', () => { beforeEach(() => { i18next.changeLanguage('ja'); }); test.each([ [1000, '0:00'], [0, '0:00'], [86400000, '1日間 0:00'], [279241234, '3日間 5:34'], [20041234, '5:34'], ])('i15dDurationFromMs(%s) === "%s"', (timestamp, expected) => { expect(i15dDurationFromMs(timestamp)).toBe(expected); }); test.each([ [1000, '0:00'], [0, '0:00'], [86400000, '1日間 0:00'], [279241234, '3日間 5:34'], [20041234, '5:34'], ])('i15dDurationFromMs(%s) === "%s"', (timestamp, expected) => { expect(i15dDurationFromMs(timestamp, true)).toBe(expected); }); test.each([ [1000, '0:00:01'], [0, '0:00:00'], [279241234, '3日間 5:34:01'], [20041234, '5:34:01'], ])('i15dDurationFromMs(%s) === "%s"', (timestamp, expected) => { expect(i15dDurationFromMsWithSeconds(timestamp)).toBe(expected); }); }); ================================================ FILE: src/app/utils/time.ts ================================================ import { t } from 'app/i18next-t'; /** * splits a number of milliseconds into [days, hours, minutes, seconds, milliseconds] * * negative durations are treated as 0 */ function durationFromMs(ms: number) { ms = Math.floor(Math.max(0, ms)); const days = Math.floor(ms / 86400000); // 86400000 ms per day ms %= 86400000; // ms now has full days taken out const hours = Math.floor(ms / 3600000); // 3600000 ms per hour ms %= 3600000; // ms now has full hours taken out const minutes = Math.floor(ms / 60000); // 60000 ms per minute ms %= 60000; // ms now has full minutes taken out const seconds = Math.floor(ms / 1000); // 1000 ms per second ms %= 1000; // ms now has full seconds taken out return [days, hours, minutes, seconds, ms]; } /** * print a number of milliseconds as d:h:m:s * * negative durations are treated as 0 */ export function timerDurationFromMs(milliseconds: number, minSegments = 3) { const duration = durationFromMs(milliseconds).slice(0, -1); while (duration[0] === 0 && duration.length > minSegments) { duration.shift(); } return duration.map((u, i) => `${u}`.padStart(i === 0 ? 0 : 2, '0')).join(':'); } /** * print a number of milliseconds as m:s.ms * * negative durations are treated as 0 */ export function timerDurationFromMsWithDecimal(milliseconds: number) { const duration = durationFromMs(milliseconds); while (duration[0] === 0 && duration.length > 3) { duration.shift(); } const ms = duration.pop()!; duration[duration.length - 1] = (duration.at(-1)! * 1000 + ms) / 1000; return duration.map((u, i) => (i !== 0 && u < 10 ? `0${u}` : u)).join(':'); } /** * print a number of milliseconds as something like "4d 0:51", * containing days, minutes, and hours. * uses i18n to choose an appropriate substitute for that "d" * * negative durations are treated as 0 */ export function i15dDurationFromMs(milliseconds: number, compact = false) { const [days, hours, minutes] = durationFromMs(milliseconds); const hhMM = `${hours}:${`${minutes}`.padStart(2, '0')}`; return days ? `${t('Countdown.Days', { count: days, context: compact ? 'compact' : '', metadata: { context: ['compact'] }, })} ${hhMM}` : `${hhMM}`; } /** * print a number of milliseconds as something like "4d 0:51:23", * containing days, minutes, seconds, and hours. * uses i18n to choose an appropriate substitute for that "d" * * negative durations are treated as 0 */ export function i15dDurationFromMsWithSeconds(milliseconds: number) { const [days, hours, minutes, seconds] = durationFromMs(milliseconds); const hhMM = `${hours}:${minutes.toString().padStart(2, '0')}:${seconds .toString() .padStart(2, '0')}`; return days ? `${t('Countdown.Days', { count: days, context: '', metadata: { context: ['compact'] }, })} ${hhMM}` : `${hhMM}`; } ================================================ FILE: src/app/utils/undo-redo-history.test.ts ================================================ import { act, renderHook } from '@testing-library/react'; import { newLoadout } from 'app/loadout-drawer/loadout-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { useHistory } from './undo-redo-history'; // Hilariously when I looked up a guide to testing React hooks... it was for testing a useUndo hook: // https://kentcdodds.com/blog/how-to-test-custom-react-hooks test('allows you to undo and redo loadout edits', () => { const initialLoadout = newLoadout('', [], DestinyClass.Hunter); const { result } = renderHook(() => useHistory(initialLoadout)); // assert initial state expect(result.current.canUndo).toBe(false); expect(result.current.canRedo).toBe(false); expect(result.current.state).toEqual(initialLoadout); // make a change act(() => { result.current.setState((loadout) => ({ ...loadout, name: 'foo' })); }); // assert new state expect(result.current.canUndo).toBe(true); expect(result.current.canRedo).toBe(false); expect(result.current.state.name).toEqual('foo'); // another change act(() => { result.current.setState((loadout) => ({ ...loadout, items: [{ id: '2', hash: 1, amount: 1, equip: true }], })); }); // assert new state expect(result.current.canUndo).toBe(true); expect(result.current.canRedo).toBe(false); expect(result.current.state.items).toEqual([{ id: '2', hash: 1, amount: 1, equip: true }]); expect(result.current.state.name).toEqual('foo'); // undo act(() => { result.current.undo(); }); // assert "undone" state expect(result.current.canUndo).toBe(true); expect(result.current.canRedo).toBe(true); expect(result.current.state.items).toEqual([]); expect(result.current.state.name).toEqual('foo'); // undo again act(() => { result.current.undo(); }); // assert "double-undone" state expect(result.current.canUndo).toBe(false); expect(result.current.canRedo).toBe(true); expect(result.current.state.items).toEqual([]); expect(result.current.state.name).toEqual(''); // redo act(() => { result.current.redo(); }); // assert undo + undo + redo state expect(result.current.canUndo).toBe(true); expect(result.current.canRedo).toBe(true); expect(result.current.state.items).toEqual([]); expect(result.current.state.name).toEqual('foo'); // add fourth value act(() => { result.current.setState((loadout) => ({ ...loadout, name: 'bar' })); }); // assert final state (note the lack of "third") expect(result.current.canUndo).toBe(true); expect(result.current.canRedo).toBe(false); expect(result.current.state.items).toEqual([]); expect(result.current.state.name).toEqual('bar'); }); ================================================ FILE: src/app/utils/undo-redo-history.ts ================================================ import { useHotkey } from 'app/hotkeys/useHotkey'; import { useCallback, useReducer } from 'react'; interface History<S> { state: S; undoStack: S[]; redoStack: S[]; } type StateUpdateFunction<S> = (oldState: S) => S; interface SetAction<S> { type: 'set'; update: StateUpdateFunction<S>; } interface UndoAction { type: 'undo'; } interface RedoAction { type: 'redo'; } type Action<S> = SetAction<S> | UndoAction | RedoAction; function historyReducer<S>(oldState: History<S>, action: Action<S>): History<S> { switch (action.type) { case 'set': { const { undoStack, state } = oldState; return { state: action.update(state), undoStack: [...undoStack, state], redoStack: [], }; } case 'undo': { const { undoStack, redoStack, state } = oldState; if (undoStack.length < 1) { return oldState; } const previousState = undoStack.at(-1)!; return { state: previousState, undoStack: undoStack.slice(0, -1), redoStack: [...redoStack, state], }; } case 'redo': { const { undoStack, redoStack, state } = oldState; if (redoStack.length < 1) { return oldState; } const nextState = redoStack.at(-1)!; return { state: nextState, undoStack: [...undoStack, state], redoStack: redoStack.slice(0, -1), }; } } } function initializer<S>(state: S): History<S> { return { state, undoStack: [], redoStack: [], }; } export function useHistory<S>(initialState: S): { state: S; setState: (f: StateUpdateFunction<S>) => void; undo: () => void; redo: () => void; canUndo: boolean; canRedo: boolean; } { const [{ state, undoStack, redoStack }, dispatch] = useReducer( historyReducer<S>, initialState, initializer, ); const setState = useCallback( (f: StateUpdateFunction<S>) => dispatch({ type: 'set', update: f }), [], ); const undo = useCallback(() => dispatch({ type: 'undo' }), []); const redo = useCallback(() => dispatch({ type: 'redo' }), []); useHotkey('mod+z', '', undo); useHotkey('mod+shift+z', '', redo); return { state, setState, undo, redo, canUndo: undoStack.length > 0, canRedo: redoStack.length > 0, }; } ================================================ FILE: src/app/utils/useWhatChanged.ts ================================================ import { useRef } from 'react'; import { infoLog } from './log'; /** * A debugging hook that will print out what changed since the last render! * * @example * * useWhatChanged('MyComponent', {prop1, prop2, state1, state2}); */ export function useWhatChanged<T extends Record<string, unknown>>(name: string, params: T) { const previousState = useRef<T>(undefined); if (!previousState.current) { infoLog('useWhatChanged', `${name} first render`); } else { for (const [key, val] of Object.entries(params)) { const previousVal = previousState.current[key]; if (val !== previousVal) { infoLog('useWhatChanged', `${name} ${key}`, previousVal, val); } } } previousState.current = params; } ================================================ FILE: src/app/utils/util-types.ts ================================================ /** Generic TypeScript type utilities */ /** Extracts the type of elements of an array */ export type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never; /** * A lookup table from key to value, where not all keys may be mapped. We use these often * to special case some logic for certain subsets of strings or hashes. Use Record<K,V> if * the table is meant to be complete for all possible values of K. This can also be helpful * to re-type imported JSON files as lookup tables. */ export type LookupTable<K extends keyof any, V> = { readonly [P in K]?: V | undefined; }; /** * A convenience for a lookup table keyed by a hash (number). This also accepts strings * since you can use string version of numbers to read into objects keyed by number just fine, * and JSON files are always keyed by string. */ export type HashLookup<V> = LookupTable<number | string, V>; /** * A convenience for a lookup table keyed by a string. Equivalent to NodeJS.ReadonlyDict. */ export type StringLookup<V> = LookupTable<string, V>; /** performs `key in obj` but properly narrows `key` */ /*@__INLINE__*/ export function isIn<O extends Record<string | number, any>>(key: keyof O, obj: O): key is keyof O { return key in obj; } /** performs `Object.values()` but properly types the values when the input object has number keys. */ /*@__INLINE__*/ export function objectValues<T>( obj: { [key: string]: T } | { [key: number]: T } | ArrayLike<T>, ): T[] { return Object.values(obj); } ================================================ FILE: src/app/vendors/Cost.m.scss ================================================ .cost { composes: flexRow from '../dim-ui/common.m.scss'; align-items: center; justify-content: flex-end; font-size: 10px; img { height: 12px; width: 12px; margin-left: 4px; } } ================================================ FILE: src/app/vendors/Cost.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'cost': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/Cost.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import { useD2Definitions } from 'app/manifest/selectors'; import { DestinyItemQuantity } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import * as styles from './Cost.m.scss'; /** * Display a single item + quantity as a "cost". */ export default function Cost({ cost, className, }: { cost: DestinyItemQuantity; className?: string; }) { const defs = useD2Definitions()!; const currencyItem = defs.InventoryItem.get(cost.itemHash); if (!currencyItem) { return null; } return ( <div className={clsx(styles.cost, className)} title={`${cost.quantity.toLocaleString()} ${currencyItem.displayProperties.name}`} > {cost.quantity.toLocaleString()} <BungieImage height={12} width={12} src={currencyItem.displayProperties.icon} /> </div> ); } ================================================ FILE: src/app/vendors/Vendor.m.scss ================================================ @use '../variables.scss' as *; .icon { width: 30px; height: 30px; } .location { text-transform: none; letter-spacing: normal; font-size: 12px; flex: 1; opacity: 0.6; text-align: left; } .title { :global(.collapse-handle) span { display: flex; flex-direction: row; align-items: center; } } .countdown { font-size: 12px; text-align: right; margin-left: 8px; white-space: nowrap; } .titleDetails { display: flex; flex-flow: row wrap; align-items: center; gap: 0 6px; @include phone-portrait { flex-direction: column; align-items: flex-start; margin: 4px 0; } } @keyframes glow { from { box-shadow: 0 0 6px -10px rgb(174, 244, 175, 0.6); } to { box-shadow: 0 0 6px 6px rgb(174, 244, 175, 0.6); } } ================================================ FILE: src/app/vendors/Vendor.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'countdown': string; 'glow': string; 'icon': string; 'location': string; 'title': string; 'titleDetails': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/Vendor.tsx ================================================ import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import React from 'react'; import BungieImage from '../dim-ui/BungieImage'; import CollapsibleTitle from '../dim-ui/CollapsibleTitle'; import Countdown from '../dim-ui/Countdown'; import * as styles from './Vendor.m.scss'; import VendorItems from './VendorItems'; import { D2Vendor } from './d2-vendors'; export function VendorLocation({ children }: { children: React.ReactNode }) { return <span className={styles.location}>{children}</span>; } export function VendorIcon({ src }: { src: string }) { return <BungieImage src={src} className={styles.icon} />; } /** * An individual Vendor in the "all vendors" page. Use SingleVendor for a page that only has one vendor on it. */ export default function Vendor({ vendor, ownedItemHashes, currencyLookups, characterId, }: { vendor: D2Vendor; ownedItemHashes?: Set<number>; currencyLookups: { [itemHash: number]: number; }; characterId: string; }) { const placeString = Array.from( new Set( [vendor.destination?.displayProperties.name, vendor.place?.displayProperties.name].filter( (n) => n?.length, ), ), ).join(', '); let refreshTime = vendor.component && new Date(vendor.component.nextRefreshDate); if (refreshTime?.getFullYear() === 9999) { refreshTime = undefined; } return ( <div id={vendor.def.hash.toString()}> <CollapsibleTitle className={styles.title} title={ <> <BungieImage src={ vendor.def.displayProperties.smallTransparentIcon || vendor.def.displayProperties.icon } className={styles.icon} /> <div className={styles.titleDetails}> <div> <RichDestinyText text={vendor.def.displayProperties.name} /> </div> <VendorLocation>{placeString}</VendorLocation> </div> </> } extra={refreshTime && <Countdown endTime={refreshTime} className={styles.countdown} />} sectionId={`d2vendor-${vendor.def.hash}`} // hi! this sectionId formatting matters for dispatching vendor detail api requests. // please modify carefully and see how it's used in vendorsNeedingComponents in loadAllVendors > <VendorItems vendor={vendor} ownedItemHashes={ownedItemHashes} currencyLookups={currencyLookups} characterId={characterId} /> </CollapsibleTitle> </div> ); } ================================================ FILE: src/app/vendors/VendorItem.m.scss ================================================ @use '../variables.scss' as *; .tile { cursor: pointer; height: 54px; width: 123px; } .ownershipIcon { width: calc(var(--item-size) / 4) !important; height: calc(var(--item-size) / 4) !important; font-size: calc(var(--item-size) / 6) !important; vertical-align: -0.125em; border-radius: 50%; padding: 3px; box-sizing: border-box; position: absolute; top: calc(#{$item-border-width} - 4px - var(--item-size) / 4 + var(--item-size)); right: $item-border-width + 2px; box-shadow: 0 0 2px rgb(0, 0, 0, 0.8); display: flex; justify-content: center; } .acquiredIcon { composes: ownershipIcon; background: #3c94ff; } .ownedIcon { composes: ownershipIcon; background: $acquiredGreen; } .lockedIcon { composes: ownershipIcon; background: grey; align-items: center; } .vendorItem { composes: flexColumn from '../dim-ui/common.m.scss'; align-items: center; position: relative; width: min-content; text-align: center; :global(.item) { cursor: pointer; } :global(.item-img) { object-fit: cover; } } .unavailable { :global(.item) { opacity: 0.3; } } .cost { font-size: 10px; margin-top: 1px; text-align: center; } ================================================ FILE: src/app/vendors/VendorItem.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'acquiredIcon': string; 'cost': string; 'lockedIcon': string; 'ownedIcon': string; 'ownershipIcon': string; 'tile': string; 'unavailable': string; 'vendorItem': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/VendorItemComponent.tsx ================================================ import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { ItemPopupExtraInfo } from 'app/item-popup/item-popup'; import { DestinyCollectibleState } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import React, { use } from 'react'; import BungieImage from '../dim-ui/BungieImage'; import ConnectedInventoryItem from '../inventory/ConnectedInventoryItem'; import ItemPopupTrigger from '../inventory/ItemPopupTrigger'; import '../progress/milestone.scss'; import { AppIcon, faCheck, lockIcon } from '../shell/icons'; import Cost from './Cost'; import * as styles from './VendorItem.m.scss'; import { SingleVendorSheetContext } from './single-vendor/SingleVendorSheetContainer'; import { VendorItem } from './vendor-item'; export default function VendorItemComponent({ item, owned, characterId, }: { item: VendorItem; owned: boolean; characterId?: string; }) { if (item.displayTile) { const showVendor = use(SingleVendorSheetContext); return ( <div className={styles.vendorItem}> <a onClick={() => showVendor?.({ characterId, vendorHash: item.previewVendorHash })}> <BungieImage className={styles.tile} title={item.displayProperties.name} src={item.displayProperties.icon} /> </a> {item.displayProperties.name} </div> ); } if (!item.item) { return null; } const acquired = item.collectibleState !== undefined && !(item.collectibleState & DestinyCollectibleState.NotAcquired); // Can't buy more copies of emblems or bounties other than repeatables. const ownershipRule = item.item?.itemCategoryHashes.includes(ItemCategoryHashes.Emblems) || (item.item?.itemCategoryHashes.includes(ItemCategoryHashes.Bounties) && !item.item.itemCategoryHashes.includes(ItemCategoryHashes.RepeatableBounties)); const mod = item.item.itemCategoryHashes.includes(ItemCategoryHashes.Mods_Mod); const unavailable = !item.canBeSold || (owned && ownershipRule); return ( <VendorItemDisplay item={item.item} // do not allow dimming from filtering, since the D2 vendors page hides non-matching items entirely allowFilter={false} unavailable={unavailable} owned={owned} locked={item.locked} acquired={acquired} extraData={{ failureStrings: item.failureStrings, characterId, owned, acquired, mod }} > {item.costs.length > 0 && ( <div> {item.costs.map((cost) => ( <Cost key={cost.itemHash} cost={cost} className={styles.cost} /> ))} </div> )} </VendorItemDisplay> ); } export function VendorItemDisplay({ allowFilter, unavailable, owned, locked, acquired, item, extraData, children, }: { allowFilter?: boolean; /** i.e. greyed out */ unavailable?: boolean; owned?: boolean; locked?: boolean; acquired?: boolean; item: DimItem; extraData?: ItemPopupExtraInfo; children?: React.ReactNode; }) { return ( <div className={clsx(styles.vendorItem, { [styles.unavailable]: unavailable, })} > <ItemPopupTrigger item={item} extraData={extraData}> {(ref, onClick) => ( <ConnectedInventoryItem item={item} allowFilter={allowFilter ?? true} ref={ref} onClick={onClick} /> )} </ItemPopupTrigger> {children} {owned ? ( <AppIcon className={styles.ownedIcon} icon={faCheck} title={t('MovePopup.Owned')} /> ) : acquired ? ( <AppIcon className={styles.acquiredIcon} icon={faCheck} title={t('MovePopup.Acquired')} /> ) : ( locked && ( <AppIcon className={styles.lockedIcon} icon={lockIcon} title={t('MovePopup.LockUnlock.Locked')} /> ) )} </div> ); } ================================================ FILE: src/app/vendors/VendorItems.m.scss ================================================ @use '../variables.scss' as *; .currencies { display: flex; flex-flow: row wrap; gap: 4px 10px; font-size: 12px; margin-top: 4px; justify-content: flex-end; float: right; @include phone-portrait { margin-right: var(--item-margin); } } .currencyIcon { vertical-align: bottom; height: 16px; width: 16px; } .itemCategories { composes: flexWrap from '../dim-ui/common.m.scss'; margin-bottom: 1.5em; gap: 4px 32px; float: left; margin-top: 8px; @include phone-portrait { flex-flow: column nowrap; gap: 8px 0; } } .categoryTitle { margin-top: 10px; text-transform: uppercase; font-size: 12px; color: #ccc; margin-bottom: 4px; &:first-child { margin-top: 0; } } .vendorItems { composes: flexWrap from '../dim-ui/common.m.scss'; gap: var(--item-margin); } .vendorContents { @include phone-portrait { margin-left: var(--inventory-column-padding); margin-right: calc(var(--inventory-column-padding) - var(--item-margin)); } } ================================================ FILE: src/app/vendors/VendorItems.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'categoryTitle': string; 'currencies': string; 'currencyIcon': string; 'itemCategories': string; 'vendorContents': string; 'vendorItems': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/VendorItems.tsx ================================================ import { PressTip } from 'app/dim-ui/PressTip'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { t } from 'app/i18next-t'; import { useD2Definitions } from 'app/manifest/selectors'; import FactionIcon from 'app/progress/FactionIcon'; import { ReputationRank } from 'app/progress/ReputationRank'; import { DestinyVendorProgressionType } from 'bungie-api-ts/destiny2'; import focusingItemOutputs from 'data/d2/focusing-item-outputs.json'; import BungieImage from '../dim-ui/BungieImage'; import VendorItemComponent from './VendorItemComponent'; import * as styles from './VendorItems.m.scss'; import { D2Vendor } from './d2-vendors'; // ignore what i think is the loot pool preview on some tower vendors? const ignoreCategories = ['category_preview']; /** * Display the items for a single vendor, organized by category. */ export default function VendorItems({ vendor, ownedItemHashes, currencyLookups, characterId, }: { vendor: D2Vendor; ownedItemHashes?: Set<number>; currencyLookups?: { [itemHash: number]: number; }; characterId: string; }) { const defs = useD2Definitions()!; if (!vendor.items.length) { return <div className={styles.vendorContents}>{t('Vendors.NoItems')}</div>; } const itemsByCategory = Map.groupBy(vendor.items, (item) => item.displayCategoryIndex); itemsByCategory.delete(undefined); const faction = vendor.def.factionHash ? defs.Faction.get(vendor.def.factionHash) : undefined; const factionProgress = vendor?.component?.progression; return ( <div className={styles.vendorContents}> {vendor.currencies.length > 0 && ( <div className={styles.currencies}> {vendor.currencies.map((currency) => ( <div key={currency.hash}> {(currencyLookups?.[currency.hash] || 0).toLocaleString()}{' '} <BungieImage height={16} width={16} className={styles.currencyIcon} src={currency.displayProperties.icon} title={currency.displayProperties.name} /> </div> ))} </div> )} <div className={styles.itemCategories}> {faction && factionProgress && ( <div> <h3 className={styles.categoryTitle}>{t('Vendors.Engram')}</h3> <div className={styles.vendorItems}> {factionProgress && (vendor.def.vendorProgressionType !== DestinyVendorProgressionType.Default ? ( <ReputationRank progress={factionProgress} /> ) : ( <PressTip minimal tooltip={`${factionProgress.progressToNextLevel}/${factionProgress.nextLevelAt}`} > <div> <FactionIcon factionProgress={factionProgress} factionDef={faction} vendor={vendor.component} /> </div> </PressTip> ))} </div> </div> )} {[...itemsByCategory.entries()].map( ([categoryIndex, items]) => categoryIndex !== undefined && vendor.def.displayCategories[categoryIndex] && !ignoreCategories.includes(vendor.def.displayCategories[categoryIndex].identifier) && ( <div key={categoryIndex}> <h3 className={styles.categoryTitle}> <RichDestinyText text={ vendor.def.displayCategories[categoryIndex]?.displayProperties.name || 'Unknown' } ownerId={characterId} /> </h3> <div className={styles.vendorItems}> {items.map( (vendorItem) => vendorItem.item && ( <VendorItemComponent key={vendorItem.vendorItemIndex} item={vendorItem} owned={Boolean( ownedItemHashes?.has(vendorItem.item.hash) || vendorItem.owned || (vendorItem.item.hash in focusingItemOutputs && ownedItemHashes?.has(focusingItemOutputs[vendorItem.item.hash]!)), )} characterId={characterId} /> ), )} </div> </div> ), )} </div> </div> ); } ================================================ FILE: src/app/vendors/Vendors.m.scss ================================================ @use '../variables' as *; .buttons { composes: flexColumn from '../dim-ui/common.m.scss'; margin: 8px 0; gap: 8px; @include phone-portrait { padding: 0 10px; } } ================================================ FILE: src/app/vendors/Vendors.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'buttons': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/Vendors.tsx ================================================ import CheckButton from 'app/dim-ui/CheckButton'; import PageWithMenu from 'app/dim-ui/PageWithMenu'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { t } from 'app/i18next-t'; import { useLoadStores } from 'app/inventory/store/hooks'; import { getCurrentStore } from 'app/inventory/stores-helpers'; import { useD2Definitions } from 'app/manifest/selectors'; import { useSetting } from 'app/settings/hooks'; import ErrorPanel from 'app/shell/ErrorPanel'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { usePageTitle } from 'app/utils/hooks'; import { DestinyCurrenciesComponent } from 'bungie-api-ts/destiny2'; import { PanInfo, motion } from 'motion/react'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { DestinyAccount } from '../accounts/destiny-account'; import CharacterSelect from '../dim-ui/CharacterSelect'; import ErrorBoundary from '../dim-ui/ErrorBoundary'; import { sortedStoresSelector } from '../inventory/selectors'; import Vendor from './Vendor'; import * as styles from './Vendors.m.scss'; import VendorsMenu from './VendorsMenu'; import { setShowUnacquiredOnly } from './actions'; import { D2VendorGroup, filterVendorGroups } from './d2-vendors'; import { useLoadVendors } from './hooks'; import { ownedVendorItemsSelector, showUnacquiredVendorItemsOnlySelector, vendorGroupsForCharacterSelector, vendorItemFilterSelector, vendorsByCharacterSelector, } from './selectors'; import { hideVendorSheet } from './single-vendor/single-vendor-sheet'; /** * The "All Vendors" page for D2 that shows all the rotating vendors. */ export default function Vendors({ account }: { account: DestinyAccount }) { const defs = useD2Definitions(); const dispatch = useThunkDispatch(); const isPhonePortrait = useIsPhonePortrait(); const stores = useSelector(sortedStoresSelector); const vendors = useSelector(vendorsByCharacterSelector); usePageTitle(t('Vendors.Vendors')); const shouldFilterToUnacquired = useSelector(showUnacquiredVendorItemsOnlySelector); const [hideSilverItems, setHideSilverItems] = useSetting('vendorsHideSilverItems'); // once the page is loaded, user can select this const [userSelectedStoreId, setUserSelectedStoreId] = useState<string>(); // each render without a user selection, retry getting a character ID to use const storeId = userSelectedStoreId || getCurrentStore(stores)?.id; let vendorGroups = useSelector(vendorGroupsForCharacterSelector(storeId)); const ownedItemHashes = useSelector(ownedVendorItemsSelector(storeId)); const vendorFilter = useSelector(vendorItemFilterSelector(storeId)); useLoadStores(account); useLoadVendors(account, storeId); // Hide the vendor sheets when switching characters useEffect(() => { hideVendorSheet(); }, [storeId]); // Turn off "show unacquired only" when leaving Vendors page // to prevent it from applying to artifact on the Inventory page useEffect( () => () => { dispatch(setShowUnacquiredOnly(false)); }, [dispatch], ); const handleSwipe = (_e: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { // Velocity is in px/ms if (Math.abs(info.offset.x) < 10 || Math.abs(info.velocity.x) < 300) { return; } const direction = -Math.sign(info.velocity.x); const characters = stores.filter((s) => !s.isVault); const selectedStoreIndex = storeId ? characters.findIndex((s) => s.id === storeId) : characters.findIndex((s) => s.current); if (direction > 0 && selectedStoreIndex < characters.length - 1) { setUserSelectedStoreId(characters[selectedStoreIndex + 1].id); } else if (direction < 0 && selectedStoreIndex > 0) { setUserSelectedStoreId(characters[selectedStoreIndex - 1].id); } }; const vendorData = storeId ? vendors[storeId] : undefined; const vendorsResponse = vendorData?.vendorsResponse; if (!vendorsResponse && vendorData?.error) { return ( <div className="dim-page"> <ErrorPanel error={vendorData.error} /> </div> ); } if (!stores.length) { return ( <div className="dim-page"> <ShowPageLoading message={t('Loading.Profile')} /> </div> ); } const selectedStore = stores.find((s) => s.id === storeId)!; const currencyLookups = vendorsResponse?.currencyLookups.data?.itemQuantities; vendorGroups = filterVendorGroups(vendorGroups, vendorFilter); return ( <PageWithMenu> <PageWithMenu.Menu> {selectedStore && ( <CharacterSelect stores={stores} selectedStore={selectedStore} onCharacterChanged={setUserSelectedStoreId} /> )} {selectedStore && ( <div className={styles.buttons}> <CheckButton name="filter-to-unacquired" checked={shouldFilterToUnacquired} onChange={(val) => dispatch(setShowUnacquiredOnly(val))} > {t('Vendors.FilterToUnacquired')} </CheckButton> <CheckButton name="vendorsHideSilverItems" checked={hideSilverItems} onChange={setHideSilverItems} > {t('Vendors.HideSilverItems')} </CheckButton> </div> )} {!isPhonePortrait && vendorGroups && <VendorsMenu groups={vendorGroups} />} </PageWithMenu.Menu> <PageWithMenu.Contents> <motion.div className="horizontal-swipable" onPanEnd={handleSwipe}> {vendorGroups && currencyLookups && defs ? ( vendorGroups.map((group) => ( <VendorGroup key={group.def.hash} group={group} ownedItemHashes={ownedItemHashes} currencyLookups={currencyLookups} characterId={selectedStore.id} /> )) ) : ( <ShowPageLoading message={t('Loading.Vendors')} /> )} </motion.div> </PageWithMenu.Contents> </PageWithMenu> ); } function VendorGroup({ group, ownedItemHashes, currencyLookups, characterId, }: { group: D2VendorGroup; ownedItemHashes?: Set<number>; currencyLookups: DestinyCurrenciesComponent['itemQuantities']; characterId: string; }) { return ( <> <h2>{group.def.categoryName}</h2> {group.vendors.map((vendor) => ( <ErrorBoundary key={vendor.def.hash} name="Vendor"> <Vendor vendor={vendor} ownedItemHashes={ownedItemHashes} currencyLookups={currencyLookups} characterId={characterId} /> </ErrorBoundary> ))} </> ); } ================================================ FILE: src/app/vendors/VendorsMenu.m.scss ================================================ .vendorMenuItem > span:not(.app-icon) { text-transform: none; letter-spacing: normal; } ================================================ FILE: src/app/vendors/VendorsMenu.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'appIcon': string; 'vendorMenuItem': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/VendorsMenu.tsx ================================================ import BungieImage from 'app/dim-ui/BungieImage'; import RichDestinyText from 'app/dim-ui/destiny-symbols/RichDestinyText'; import PageWithMenu from 'app/dim-ui/PageWithMenu'; import React from 'react'; import { D2VendorGroup } from './d2-vendors'; import * as styles from './VendorsMenu.m.scss'; export default function VendorsMenu({ groups }: { groups: readonly D2VendorGroup[] }) { return ( <> {groups.map((group) => ( <React.Fragment key={group.def.hash}> <PageWithMenu.MenuHeader>{group.def.categoryName}</PageWithMenu.MenuHeader> {group.vendors.map((vendor) => ( <PageWithMenu.MenuButton anchor={vendor.def.hash.toString()} key={vendor.def.hash} className={styles.vendorMenuItem} > <BungieImage src={ vendor.def.displayProperties.smallTransparentIcon || vendor.def.displayProperties.icon } /> <RichDestinyText text={vendor.def.displayProperties.name} /> </PageWithMenu.MenuButton> ))} </React.Fragment> ))} </> ); } ================================================ FILE: src/app/vendors/actions.ts ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { settingsSelector } from 'app/dim-api/selectors'; import { manifestSelector } from 'app/manifest/selectors'; import { ThunkResult } from 'app/store/types'; import { filterMap } from 'app/utils/collections'; import { chainComparator, compareBy, compareByIndex } from 'app/utils/comparators'; import { convertToError } from 'app/utils/errors'; import { infoLog } from 'app/utils/log'; import { DestinyItemType, DestinyVendorResponse, DestinyVendorsResponse, TierType, } from 'bungie-api-ts/destiny2'; import { createAction } from 'typesafe-actions'; import { getVendorSaleComponents, getVendors } from '../bungie-api/destiny2-api'; export const loadedAll = createAction('vendors/LOADED_ALL')<{ characterId: string; vendorsResponse: DestinyVendorsResponse; }>(); export const loadedVendorComponents = createAction('vendors/LOADED_COMPONENT')<{ characterId: string; vendorResponses: [vendorHash: number, DestinyVendorResponse][]; }>(); export const loadedError = createAction('vendors/LOADED_ERROR')<{ characterId: string; error: Error; }>(); export const setShowUnacquiredOnly = createAction('vendors/SHOW_UNCOLLECTED_ONLY')<boolean>(); export function loadAllVendors( account: DestinyAccount, characterId: string, force = false, ): ThunkResult { return async (dispatch, getState) => { const timeSinceLastLoad = Date.now() - (getState().vendors.vendorsByCharacter[characterId]?.lastLoaded || 0); // Only load "all vendors" at most once per minute. it takes a little while to load all data. if (!force && timeSinceLastLoad < 60 * 1000) { return; } const timings = { salesTime: 0, componentsNeeded: 0, componentsFetched: 0, componentsTime: 0 }; let start = Date.now(); try { // https://github.com/Bungie-net/api/issues/1887 MAY 2024-ish: for perf reasons, // bungie has decided not to send back item components for the "all vendors" endpoint. // this request still lets us know which items are sold, by all available vendors. // enough to make a reasonable-looking vendors page full of shiny icons. const vendorsResponse = await getVendors(account, characterId); timings.salesTime = Date.now() - start; dispatch(loadedAll({ vendorsResponse, characterId })); const defs = manifestSelector(getState()); // we're done if this is d1 if (!defs?.isDestiny2) { return; } // we'll trickle in responses if this is initiated from the vendors page. // otherwise, we'll collect all the itemComponents and inject them as one, // preventing optimization recalc spam const isVendorsPage = window.location.pathname.includes('/vendors'); // hashes of vendors bungie explicitly recommends displaying const topLevelVendors = vendorsResponse.vendorGroups.data?.groups.flatMap((g) => g.vendorHashes) ?? []; // vendors bungie recommends, plus their supporting subscreen vendors. const displayVendors = getSubvendorHashes(topLevelVendors, defs); // after getting all items for sale above, we'll do subsequent API fetches to // fill in item details. but only some vendors, filtered here, need this. // many vendors can be built from just their definition and live item list const vendorsNeedingComponents = filterMap( Object.entries(vendorsResponse.sales.data!), ([vendorHashKey, sales]) => { const vendorHash = Number(vendorHashKey); // if it's not one of the vendors the api says to include on the vendors page, // we don't need its stats. right now this mainly serves to exclude yuna if (!displayVendors.includes(vendorHash)) { return; } // if we find an item that needs components, include this vendor for (const { itemHash } of Object.values(sales.saleItems)) { if (itemNeedsComponents(defs, itemHash)) { return vendorHash; } } }, ); timings.componentsNeeded = vendorsNeedingComponents.length; const { collapsedSections } = settingsSelector(getState()); // decide the order of the single-vendor requests (false is earlier in sort than true) vendorsNeedingComponents.sort( chainComparator( // prioritize vendors in groups (meaning top-level, not click-in sub-vendors) compareBy((h) => !defs.Vendor.getOptional(h)?.groups.length), // deprioritize vendors whose sections are collapsed on the vendors page compareBy((h) => Boolean(collapsedSections[`d2vendor-${h}`])), // sort by their position on the page compareByIndex(displayVendors, (h) => h), ), ); const vendorResponses: [vendorHash: number, DestinyVendorResponse][] = []; const promises: Promise<void>[] = []; for (const vendorHash of vendorsNeedingComponents) { promises.push( (async () => { try { start = Date.now(); const vendorResponse = await getVendorSaleComponents( account, characterId, vendorHash, ); timings.componentsTime += Date.now() - start; timings.componentsFetched++; if (isVendorsPage) { dispatch( loadedVendorComponents({ vendorResponses: [[vendorHash, vendorResponse]], characterId, }), ); } else { vendorResponses.push([vendorHash, vendorResponse]); } } catch { // TO-DO: what to do here if a single vendor component call fails? // not necessarily knock the overall vendors state into error mode. // maybe retry failed single-vendors later? add them to a new list in vendors state? } })(), ); } await Promise.all(promises); if (!isVendorsPage) { dispatch(loadedVendorComponents({ vendorResponses, characterId })); } } catch (e) { // this would catch a failure of getVendors. the single-vendors are caught above const error = convertToError(e); dispatch(loadedError({ characterId, error })); } finally { infoLog( 'Vendors', [ `sales data loaded: ${timings.salesTime / 1000}s`, `component data for ${timings.componentsFetched} vendors loaded: ${timings.componentsTime / 1000}s`, timings.componentsFetched === timings.componentsNeeded ? 0 : `${timings.componentsNeeded - timings.componentsFetched} components not loaded?`, ] .filter(Boolean) .join(' / '), ); } }; } function itemNeedsComponents(defs: D2ManifestDefinitions, itemHash: number) { const item = defs.InventoryItem.getOptional(itemHash); return ( item && // probably just need this for weapon perks and armor stats? (item.itemType === DestinyItemType.Armor || // exotic weapons can be built from defs (item.itemType === DestinyItemType.Weapon && item.inventory!.tierType !== TierType.Exotic)) ); } // a subvendor is a secondary screen with its own "sales", including // - menus for purchasing subclass abilites/fragments // - engram content previews // - engram focusing options // structurally, a subvendor looks like a saleitem of the main vendor, // but the item def refers to a previewVendorHash, a vendor def in its own right /** given vendor hashes, recursively accumulates those plus any subvendor hashes */ function getSubvendorHashes( vendorHashes: number[], defs: D2ManifestDefinitions, accumulator: number[] = Array.from(vendorHashes), ) { for (const vendorHash of vendorHashes) { const newEntries: number[] = []; const itemList = defs.Vendor.getOptional(vendorHash)?.itemList ?? []; for (const i of itemList) { // premature overoptimization: obviously it's not a subvendor if you can buy it if (!i.currencies?.length) { const subvendorHash = defs.InventoryItem.getOptional(i.itemHash)?.preview ?.previewVendorHash; if ( subvendorHash && !newEntries.includes(subvendorHash) && !accumulator.includes(subvendorHash) ) { newEntries.push(subvendorHash); } } } accumulator.push(...newEntries); if (newEntries.length) { getSubvendorHashes(newEntries, defs, accumulator); } } return accumulator; } ================================================ FILE: src/app/vendors/d2-vendors.test.ts ================================================ import { getBuckets } from 'app/destiny2/d2-buckets'; import { getTestDefinitions, getTestProfile, getTestVendors } from 'testing/test-utils'; import { D2VendorGroup, toVendorGroups } from './d2-vendors'; async function getTestVendorGroups() { const defs = await getTestDefinitions(); const profileResponse = getTestProfile(); const vendorsResponse = getTestVendors(); const buckets = getBuckets(defs); const characterId = Object.keys(profileResponse.characters.data!)[0]; return toVendorGroups( { defs, buckets, profileResponse, customStats: [], }, vendorsResponse, characterId, ); } function* allSaleItems(vendorGroups: D2VendorGroup[]) { for (const vendorGroup of vendorGroups) { for (const vendor of vendorGroup.vendors) { for (const saleItem of vendor.items) { yield saleItem; } } } } describe('process vendors', () => { it('can process vendors without errors', async () => { const vendorGroups = await getTestVendorGroups(); // Check that there are any vendors expect(vendorGroups.length).toBeGreaterThan(0); // Check that there's any item that has pattern unlock info - there should be some! let vendorItemPatternFound = false; for (const saleItem of allSaleItems(vendorGroups)) { if (saleItem.item?.patternUnlockRecord) { vendorItemPatternFound = true; break; } } expect(vendorItemPatternFound).toBe(true); }); }); ================================================ FILE: src/app/vendors/d2-vendors.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { ItemCreationContext } from 'app/inventory/store/d2-item-factory'; import { VendorHashes, silverItemHash } from 'app/search/d2-known-values'; import { ItemFilter } from 'app/search/filter-types'; import { compact, filterMap } from 'app/utils/collections'; import { chainComparator, compareBy, compareByIndex } from 'app/utils/comparators'; import { DestinyCollectibleState, DestinyDestinationDefinition, DestinyInventoryItemDefinition, DestinyPlaceDefinition, DestinyVendorComponent, DestinyVendorDefinition, DestinyVendorGroupDefinition, DestinyVendorSaleItemComponent, DestinyVendorsResponse, } from 'bungie-api-ts/destiny2'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import specialVendorStrings from 'data/d2/special-vendors-strings.json'; import { VendorItem, vendorItemForDefinitionItem, vendorItemForSaleItem } from './vendor-item'; export interface D2VendorGroup { def: DestinyVendorGroupDefinition; vendors: D2Vendor[]; } export interface D2Vendor { component?: DestinyVendorComponent; def: DestinyVendorDefinition; destination?: DestinyDestinationDefinition; place?: DestinyPlaceDefinition; items: VendorItem[]; currencies: DestinyInventoryItemDefinition[]; } const vendorOrder = [VendorHashes.AdaTransmog, VendorHashes.Banshee, VendorHashes.Eververse]; export function toVendorGroups( context: ItemCreationContext, vendorsResponse: DestinyVendorsResponse, characterId: string, ): D2VendorGroup[] { if (!vendorsResponse.vendorGroups.data) { return []; } const { defs } = context; return Object.values(vendorsResponse.vendorGroups.data.groups) .map((group) => { const groupDef = defs.VendorGroup.get(group.vendorGroupHash); return { def: groupDef, vendors: filterMap(group.vendorHashes, (vendorHash) => { const vendor = toVendor( // Override the item components from the profile with this vendor's item components { ...context, itemComponents: vendorsResponse.itemComponents?.[vendorHash] }, vendorHash, vendorsResponse.vendors.data?.[vendorHash], characterId, vendorsResponse.sales.data?.[vendorHash]?.saleItems, vendorsResponse, ); return vendor?.items.length ? vendor : undefined; }).sort(compareByIndex(vendorOrder, (v) => v.def.hash)), }; }) .sort(compareBy((g) => g.def.order)); } export function toVendor( context: ItemCreationContext, vendorHash: number, vendor: DestinyVendorComponent | undefined, characterId: string, sales: | { [key: string]: DestinyVendorSaleItemComponent; } | undefined, vendorsResponse: DestinyVendorsResponse | undefined, ): D2Vendor | undefined { const { defs } = context; const vendorDef = defs.Vendor.get(vendorHash); if (!vendorDef) { return undefined; } const vendorItems = getVendorItems( context, vendorDef, characterId, sales, vendor?.nextRefreshDate, ); vendorItems.sort( chainComparator( compareBy( (item) => item.originalCategoryIndex !== undefined && vendorDef.originalCategories[item.originalCategoryIndex]?.sortValue, ), compareBy((item) => item.vendorItemIndex), ), ); const destinationHash = typeof vendor?.vendorLocationIndex === 'number' && vendor.vendorLocationIndex >= 0 ? // Unadvertised nullability: DestinyVendorDefinition.locations (vendorDef.locations?.[vendor.vendorLocationIndex]?.destinationHash ?? 0) : 0; const destinationDef = destinationHash ? defs.Destination.get(destinationHash) : undefined; const placeDef = destinationDef?.placeHash ? defs.Place.get(destinationDef.placeHash) : undefined; const vendorCurrencyHashes = new Set<number>(); gatherVendorCurrencies(defs, vendorDef, vendorsResponse, sales, vendorCurrencyHashes); const currencies = compact( Array.from(vendorCurrencyHashes, (h) => defs.InventoryItem.get(h)).filter( (i) => !i?.itemCategoryHashes?.includes(ItemCategoryHashes.Shaders) && !i?.itemCategoryHashes?.includes(ItemCategoryHashes.ShipModsTransmatEffects), ), ); currencies.sort(compareBy((i) => i.inventory?.tierType)); return { component: vendor, def: vendorDef, destination: destinationDef, place: placeDef, items: vendorItems, currencies, }; } /** * Recursively look at sub-vendors of the current `vendor` to find * all currency hashes needed to purchase the sales, and collect them in `vendorCurrencyHashes`. */ function gatherVendorCurrencies( defs: D2ManifestDefinitions, vendor: DestinyVendorDefinition, vendorsResponse: DestinyVendorsResponse | undefined, sales: | { [key: string]: DestinyVendorSaleItemComponent; } | undefined, vendorCurrencyHashes: Set<number>, // prevent infinite recursion just in case vendors have a cycle in their items' previewvendorHashes seenVendors = new Set<number>(), ) { for (const sale of sales ? Object.values(sales).flatMap((saleItem) => saleItem.costs) : vendor.itemList.flatMap((item) => item.currencies)) { vendorCurrencyHashes.add(sale.itemHash); } for (const item of vendor.itemList) { const itemDef = defs.InventoryItem.get(item.itemHash); if (!itemDef) { continue; } const subVendorHash = defs.InventoryItem.get(item.itemHash)?.preview?.previewVendorHash; if (subVendorHash && !seenVendors.has(subVendorHash)) { seenVendors.add(subVendorHash); const subVendor = defs.Vendor.get(subVendorHash); gatherVendorCurrencies( defs, subVendor, vendorsResponse, vendorsResponse?.sales.data?.[subVendorHash]?.saleItems, vendorCurrencyHashes, seenVendors, ); } } } function getVendorItems( context: ItemCreationContext, vendorDef: DestinyVendorDefinition, characterId: string, sales: | { [key: string]: DestinyVendorSaleItemComponent; } | undefined, nextRefreshDate?: string, ): VendorItem[] { if (sales) { const components = Object.values(sales); return components.map((component) => vendorItemForSaleItem(context, vendorDef, component, characterId, nextRefreshDate), ); } else if (vendorDef.returnWithVendorRequest) { // If the sales should come from the server, don't show anything until we have them return []; } else { return vendorDef.itemList.map((i, index) => vendorItemForDefinitionItem(context, i, characterId, index, nextRefreshDate), ); } } export type VendorFilterFunction = ( item: VendorItem, vendor: D2Vendor, ) => boolean | null | undefined; export function filterVendorGroups( vendorGroups: readonly D2VendorGroup[], predicate: VendorFilterFunction, ) { return vendorGroups .map((group) => ({ ...group, vendors: group.vendors .map((vendor) => ({ ...vendor, items: vendor.items.filter((item) => predicate(item, vendor)), })) .filter((v) => v.items.length), })) .filter((g) => g.vendors.length); } export function filterToUnacquired( ownedItemHashes: Set<number>, defs: D2ManifestDefinitions | undefined, ): VendorFilterFunction { return ({ owned, item, collectibleState, failureStrings }) => item && !owned && !failureStrings.includes( defs?.Vendor.get(specialVendorStrings.alreadyAcquiredFailureString.vendorHash) ?.failureStrings[specialVendorStrings.alreadyAcquiredFailureString.index] || 'FallbackToPreventBadFiltering', ) && (collectibleState !== undefined ? (collectibleState & DestinyCollectibleState.NotAcquired) !== 0 : (item.itemCategoryHashes.includes(ItemCategoryHashes.Mods_Mod) || item.itemCategoryHashes.includes(ItemCategoryHashes.Shaders)) && !ownedItemHashes.has(item.hash)); } export function filterToNoSilver(): VendorFilterFunction { return ({ costs, displayCategoryIndex }, vendor) => { if (costs.some((c) => c.itemHash === silverItemHash && c.quantity > 0)) { return false; } const categoryIdentifier = displayCategoryIndex !== undefined && displayCategoryIndex >= 0 && vendor.def.displayCategories[displayCategoryIndex].identifier; return !( categoryIdentifier && (categoryIdentifier.startsWith('categories.campaigns') || categoryIdentifier.startsWith('categories.featured.carousel')) ); }; } export function filterToSearch(searchQuery: string, filterItems: ItemFilter): VendorFilterFunction { return ({ item }, vendor) => vendor.def.displayProperties.name.toLowerCase().includes(searchQuery.toLowerCase()) || (item && filterItems(item)); } ================================================ FILE: src/app/vendors/focusing-item-outputs.d.ts ================================================ declare module 'data/d2/focusing-item-outputs.json' { const x: { readonly [hash: number]: number | undefined }; export default x; } ================================================ FILE: src/app/vendors/hooks.ts ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import { loadingTracker } from 'app/shell/loading-tracker'; import { refresh$ } from 'app/shell/refresh-events'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; import { useEventBusListener } from 'app/utils/hooks'; import { useCallback, useEffect } from 'react'; import { loadAllVendors } from './actions'; /** * Loads all vendors for the given account+character, and tries refreshing them * when the app refreshes. */ export function useLoadVendors( account: DestinyAccount, storeId: string | undefined, active = true, ) { const dispatch = useThunkDispatch(); useEffect(() => { if (storeId && active) { loadingTracker.addPromise(dispatch(loadAllVendors(account, storeId))); } }, [account, storeId, dispatch, active]); useEventBusListener( refresh$, useCallback( () => () => { if (storeId && active) { loadingTracker.addPromise(dispatch(loadAllVendors(account, storeId, true))); } }, [account, active, dispatch, storeId], ), ); } ================================================ FILE: src/app/vendors/reducer.ts ================================================ import { isEmpty } from 'app/utils/collections'; import { DestinyVendorsResponse } from 'bungie-api-ts/destiny2'; import { produce } from 'immer'; import { Reducer } from 'redux'; import { ActionType, getType } from 'typesafe-actions'; import { setCurrentAccount } from '../accounts/actions'; import type { AccountsAction } from '../accounts/reducer'; import * as actions from './actions'; // TODO: This may really belong in InventoryState // TODO: Save to IDB? export interface VendorsState { vendorsByCharacter: Partial<{ [characterId: string]: { vendorsResponse?: DestinyVendorsResponse; /** ms epoch time */ lastLoaded?: number; error?: Error; }; }>; showUnacquiredOnly: boolean; } export type VendorsAction = ActionType<typeof actions>; const initialState: VendorsState = { vendorsByCharacter: {}, showUnacquiredOnly: false, }; export const vendors: Reducer<VendorsState, VendorsAction | AccountsAction> = ( state: VendorsState = initialState, action: VendorsAction | AccountsAction, ): VendorsState => { switch (action.type) { case getType(actions.loadedAll): { const { characterId, vendorsResponse } = action.payload; // retain old components so that items don't go back to claiming "no perks or stats" // loadedAll being kicked off means that fresh component data is on its way and will replace the old stuff. const oldItemComponents = state.vendorsByCharacter[characterId]?.vendorsResponse?.itemComponents; return produce(state, (draft) => { draft.vendorsByCharacter[characterId] = { vendorsResponse: vendorsResponse, lastLoaded: Date.now(), error: undefined, }; if (oldItemComponents) { draft.vendorsByCharacter[characterId].vendorsResponse!.itemComponents = oldItemComponents; } }); } // augments the stored overall all-vendors response, // by inserting the item components from a single-vendor api response case getType(actions.loadedVendorComponents): { const { characterId, vendorResponses } = action.payload; return produce(state, (draft) => { for (const [vendorHash, vendorResponse] of vendorResponses) { const thisCharVendorState = draft.vendorsByCharacter[characterId]; if ( // nothing about state needs changing if we didn't get back components // (maybe sockets/etc are disabled at bnet right now?) isEmpty(vendorResponse.itemComponents) || // or if there's no main response to inject these components into !thisCharVendorState?.vendorsResponse ) { continue; } thisCharVendorState.vendorsResponse.itemComponents ??= {}; thisCharVendorState.vendorsResponse.itemComponents[vendorHash] = vendorResponse.itemComponents; thisCharVendorState.lastLoaded = Date.now(); } }); } case getType(actions.loadedError): { const { characterId, error } = action.payload; return { ...state, vendorsByCharacter: { ...state.vendorsByCharacter, [characterId]: { ...state.vendorsByCharacter[characterId], error, }, }, }; } case getType(actions.setShowUnacquiredOnly): { return { ...state, showUnacquiredOnly: action.payload }; } case getType(setCurrentAccount): return initialState; default: return state; } }; ================================================ FILE: src/app/vendors/selectors.ts ================================================ import { settingSelector } from 'app/dim-api/selectors'; import { DimItem } from 'app/inventory/item-types'; import { createItemContextSelector, ownedItemsSelector, ownedUncollectiblePlugsSelector, sortedStoresSelector, } from 'app/inventory/selectors'; import { getCurrentStore } from 'app/inventory/stores-helpers'; import { d2ManifestSelector } from 'app/manifest/selectors'; import { searchFilterSelector } from 'app/search/items/item-search-filter'; import { querySelector } from 'app/shell/selectors'; import { RootState } from 'app/store/types'; import { compact } from 'app/utils/collections'; import { emptyArray } from 'app/utils/empty'; import { currySelector } from 'app/utils/selectors'; import { ItemCategoryHashes } from 'data/d2/generated-enums'; import { createSelector } from 'reselect'; import { D2Vendor, D2VendorGroup, VendorFilterFunction, filterToNoSilver, filterToSearch, filterToUnacquired, toVendor, toVendorGroups, } from './d2-vendors'; import { VendorItem } from './vendor-item'; export const vendorsByCharacterSelector = (state: RootState) => state.vendors.vendorsByCharacter; // get character ID from props not state const vendorCharacterIdSelector = (state: RootState, characterId: string | undefined) => characterId || getCurrentStore(sortedStoresSelector(state))?.id; /** * returns a character's vendors and their sale items */ export const vendorGroupsForCharacterSelector = currySelector( createSelector( createItemContextSelector, vendorsByCharacterSelector, vendorCharacterIdSelector, (context, vendors, selectedStoreId) => { if (!context.defs || !context.buckets || !context.profileResponse) { // createItemContextSelector assumes stuff is already loaded, but // the SingleVendorPage still exists and may call this selector prior // to everything being loaded... return emptyArray<D2VendorGroup>(); } const vendorData = selectedStoreId ? vendors[selectedStoreId] : undefined; const vendorsResponse = vendorData?.vendorsResponse; return vendorsResponse && vendorData && selectedStoreId ? toVendorGroups(context, vendorsResponse, selectedStoreId) : emptyArray<D2VendorGroup>(); }, ), ); const subVendorsForCharacterSelector = currySelector( createSelector( createItemContextSelector, vendorsByCharacterSelector, vendorGroupsForCharacterSelector.selector, vendorCharacterIdSelector, (context, vendors, vendorGroups, selectedStoreId) => { const vendorData = selectedStoreId ? vendors[selectedStoreId] : undefined; const vendorsResponse = vendorData?.vendorsResponse; if (!vendorsResponse || !selectedStoreId) { return {}; } const subvendors: { [vendorHash: number]: D2Vendor } = {}; const workList = vendorGroups.flatMap((group) => group.vendors); while (workList.length) { const vendor = workList.pop()!; for (const item of vendor.items) { const vendorHash = item.previewVendorHash; if (vendorHash && !subvendors[vendorHash]) { const vendor = toVendor( { ...context, itemComponents: vendorsResponse.itemComponents?.[vendorHash], }, item.previewVendorHash, vendorsResponse.vendors.data?.[vendorHash], selectedStoreId, vendorsResponse.sales.data?.[vendorHash]?.saleItems, vendorsResponse, ); if (vendor) { subvendors[vendorHash] = vendor; workList.push(vendor); } } } } return subvendors; }, ), ); export const showUnacquiredVendorItemsOnlySelector = (state: RootState) => state.vendors.showUnacquiredOnly; /** * Returns vendor items (for comparison, loadout builder, ...) */ export const characterVendorItemsSelector = createSelector( (_state: RootState, vendorCharacterId?: string) => vendorCharacterId, vendorGroupsForCharacterSelector.selector, subVendorsForCharacterSelector.selector, (vendorCharacterId, vendorGroups, subVendors) => { if (!vendorCharacterId) { return emptyArray<DimItem>(); } return compact( vendorGroups .flatMap((vg) => vg.vendors) .concat(Object.values(subVendors)) .flatMap((vs) => vs.items.filter((vi) => vi.canBeSold).map((vi) => vi.item)) .filter((i) => !i?.itemCategoryHashes.includes(ItemCategoryHashes.Dummies)), ); }, ); export const ownedVendorItemsSelector = currySelector( createSelector( ownedItemsSelector, ownedUncollectiblePlugsSelector, (_: any, storeId?: string) => storeId, (ownedItems, ownedPlugs, storeId) => new Set([ ...ownedItems.accountWideOwned, ...ownedPlugs.accountWideOwned, ...((storeId && ownedItems.storeSpecificOwned[storeId]) || []), ...((storeId && ownedPlugs.storeSpecificOwned[storeId]) || []), ]), ), ); export const vendorItemFilterSelector = currySelector( createSelector( ownedVendorItemsSelector.selector, showUnacquiredVendorItemsOnlySelector, subVendorsForCharacterSelector.selector, querySelector, searchFilterSelector, settingSelector<'vendorsHideSilverItems'>('vendorsHideSilverItems'), d2ManifestSelector, (ownedItemHashes, showUnacquiredOnly, subVendors, query, itemFilter, hideSilver, defs) => { const filters: VendorFilterFunction[] = []; const silverFilter = filterToNoSilver(); if (hideSilver) { filters.push(silverFilter); } if (showUnacquiredOnly) { filters.push(filterToUnacquired(ownedItemHashes, defs)); } if (query.length) { filters.push(filterToSearch(query, itemFilter)); } function filterItem(item: VendorItem, vendor: D2Vendor, seenVendors: number[]): boolean { if (filters.every((f) => f(item, vendor))) { // Our filters match this item or vendor directly return true; } // If this item is a subvendor, check if one of the subvendor's items match filters // But don't allow this if the item itself fails the silver check -- most eververse // bundles cost silver, but their contained items don't, but we still want to hide // the bundle if "hide silver" is on. // Finally, prevent infinite recursion for subvendors because that can happen. const previewVendorHash = item.item?.previewVendor; if ( previewVendorHash && !seenVendors.includes(previewVendorHash) && (!hideSilver || silverFilter(item, vendor)) ) { const subVendorData = subVendors[previewVendorHash]; if (subVendorData) { return subVendorData.items.some((subItem) => filterItem(subItem, subVendorData, [...seenVendors, previewVendorHash]), ); } } return false; } return (item: VendorItem, vendor: D2Vendor) => filterItem(item, vendor, []); }, ), ); ================================================ FILE: src/app/vendors/single-vendor/ArtifactUnlocks.m.scss ================================================ @use '../../variables.scss' as *; .tiers { display: inline-flex; flex-flow: row nowrap; box-shadow: 0 6px 12px 0 #061a1f; } .tier { display: flex; flex-shrink: 1; padding: 6px; flex-flow: column nowrap; gap: var(--item-margin); opacity: 0.6; background-color: hsl(189, 65%, 15%, 0.25); border-right: 1px solid hsl(182, 85%, 29%, 0.5); } .stat { text-transform: uppercase; max-width: 305px; margin: 0 0 8px; color: var(--theme-header-characters-txt); line-height: 11px; font-size: 11px; font-weight: 300; text-shadow: rgb(0, 0, 0, 0.5) 0 0 2px; display: flex; place-content: start space-between; align-items: auto; } .unlockedTier { opacity: 1; background-color: #0d373e; border-right: 1px solid #0af3f3; } .item { background-size: 84%; background-repeat: no-repeat; background-position: center; border-radius: 4px; background-blend-mode: hard-light; border-bottom: 1px solid rgb(0, 0, 0, 0.75); cursor: pointer; @include interactive($hover: true) { border: 1px solid white; } } .unlocked { background-color: #0b878b; border: 1px solid #0b878b; border-top: 1px solid #0af3f3; } .locked { background-color: #061a1f; border: 1px solid #061a1f; border-top: 1px solid #0a1a1e; background-blend-mode: hard-light; } @media screen and (max-width: 540px) { .item { /* smaller since double width tier column margins */ max-width: 15vw; max-height: 15vw; } .stat { max-width: 100%; } } ================================================ FILE: src/app/vendors/single-vendor/ArtifactUnlocks.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'item': string; 'locked': string; 'stat': string; 'tier': string; 'tiers': string; 'unlocked': string; 'unlockedTier': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/single-vendor/ArtifactUnlocks.tsx ================================================ import { BasicItemTrigger, PopupState } from 'app/armory/ItemGrid'; import { bungieBackgroundStyle } from 'app/dim-ui/BungieImage'; import { t } from 'app/i18next-t'; import { DimItem } from 'app/inventory/item-types'; import { createItemContextSelector, profileResponseSelector } from 'app/inventory/selectors'; import { makeFakeItem } from 'app/inventory/store/d2-item-factory'; import ItemPopup from 'app/item-popup/ItemPopup'; import { useD2Definitions } from 'app/manifest/selectors'; import { emptyArray } from 'app/utils/empty'; import { infoLog } from 'app/utils/log'; import clsx from 'clsx'; import { memo, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import * as styles from './ArtifactUnlocks.m.scss'; const ArtifactMod = memo(function ArtifactMod({ ref, showPopup, item, }: { ref: React.Ref<HTMLDivElement>; showPopup: (e: React.MouseEvent) => void; item: { item: DimItem | undefined; isActive: boolean; }; }) { return ( <div ref={ref} onClick={showPopup} title={item.item!.name} style={bungieBackgroundStyle(item.item!.icon)} className={clsx('item', styles.item, { [styles.unlocked]: item.isActive, [styles.locked]: !item.isActive, })} /> ); }); export default function ArtifactUnlocks({ characterId }: { characterId: string }) { const profileResponse = useSelector(profileResponseSelector); const defs = useD2Definitions(); const context = useSelector(createItemContextSelector); const [popup, setPopup] = useState<PopupState | undefined>(); const artifactUnlockData = profileResponse?.characterProgressions.data?.[characterId]?.seasonalArtifact; const tierSource = artifactUnlockData?.tiers ?? emptyArray(); const tiers = useMemo( () => tierSource.map((tier) => ({ ...tier, items: tier.items .filter((i) => i.isVisible) .map((i) => ({ item: makeFakeItem(context, i.itemHash), isActive: i.isActive })), })), [tierSource, context], ); if (!profileResponse || !defs || !artifactUnlockData) { return null; } if (popup) { infoLog('clicked item', popup.item); } const { resetCount = 0, pointsUsed = 0 } = artifactUnlockData; return ( <> <div className={styles.stat}> <div>{t('Progress.PointsUsed', { count: pointsUsed })}</div> <div>{t('Progress.Resets', { count: resetCount })}</div> </div> <div className={styles.tiers}> {tiers.map((tier) => ( <div key={tier.tierHash} className={clsx(styles.tier, { [styles.unlockedTier]: tier.isUnlocked, })} > {tier.items.map( (item) => item.item && ( <BasicItemTrigger key={item.item.index} item={item.item} onShowPopup={setPopup}> {(ref, showPopup) => ( <ArtifactMod ref={ref} showPopup={showPopup} item={item} /> )} </BasicItemTrigger> ), )} </div> ))} </div> {popup && ( <ItemPopup onClose={() => setPopup(undefined)} item={popup.item} element={popup.element} /> )} </> ); } ================================================ FILE: src/app/vendors/single-vendor/SingleVendor.m.scss ================================================ .featuredHeader { margin-bottom: 1em; h1 { margin: 0; min-height: 1lh; } img, svg { position: relative; height: var(--item-size); margin-right: 8px; } } ================================================ FILE: src/app/vendors/single-vendor/SingleVendor.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'featuredHeader': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/single-vendor/SingleVendor.tsx ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import Countdown from 'app/dim-ui/Countdown'; import ShowPageLoading from 'app/dim-ui/ShowPageLoading'; import { useDynamicStringReplacer } from 'app/dim-ui/destiny-symbols/RichDestinyText'; import { t } from 'app/i18next-t'; import { bucketsSelector, createItemContextSelector, profileResponseSelector, } from 'app/inventory/selectors'; import { useLoadStores } from 'app/inventory/store/hooks'; import { useD2Definitions } from 'app/manifest/selectors'; import ErrorPanel from 'app/shell/ErrorPanel'; import { usePageTitle } from 'app/utils/hooks'; import { useSelector } from 'react-redux'; import { VendorLocation } from '../Vendor'; import VendorItems from '../VendorItems'; import { D2Vendor, toVendor } from '../d2-vendors'; import { useLoadVendors } from '../hooks'; import { ownedVendorItemsSelector, vendorItemFilterSelector, vendorsByCharacterSelector, } from '../selectors'; import ArtifactUnlocks from './ArtifactUnlocks'; import * as styles from './SingleVendor.m.scss'; /** * A page that loads its own info for a single vendor, so we can link to a vendor or show engram previews. */ export default function SingleVendor({ account, vendorHash, characterId, updatePageTitle, }: { account: DestinyAccount; vendorHash: number; characterId: string; updatePageTitle?: boolean; }) { const buckets = useSelector(bucketsSelector); const profileResponse = useSelector(profileResponseSelector); const vendors = useSelector(vendorsByCharacterSelector); const defs = useD2Definitions(); const itemCreationContext = useSelector(createItemContextSelector); const ownedItemHashes = useSelector(ownedVendorItemsSelector(characterId)); const vendorData = characterId ? vendors[characterId] : undefined; const vendorResponse = vendorData?.vendorsResponse; const vendorDef = defs?.Vendor.get(vendorHash); const returnWithVendorRequest = vendorDef?.returnWithVendorRequest; useLoadStores(account); useLoadVendors(account, characterId, /* active */ returnWithVendorRequest); const replacer = useDynamicStringReplacer(characterId); usePageTitle( replacer(vendorDef?.displayProperties.name) ?? t('Vendors.Vendors'), updatePageTitle ?? false, ); const itemFilter = useSelector(vendorItemFilterSelector(characterId)); if (!defs || !buckets) { return <ShowPageLoading message={t('Manifest.Load')} />; } if (!vendorDef) { return <ErrorPanel error={new Error(`No known vendor with hash ${vendorHash}`)} />; } if (vendorData?.error) { return <ErrorPanel error={vendorData.error} />; } if (vendorDef.returnWithVendorRequest) { if (!profileResponse) { return <ShowPageLoading message={t('Loading.Profile')} />; } if (!vendorResponse) { return <ShowPageLoading message={t('Loading.Vendors')} />; } } // TODO: // * featured item // * enabled // * filter by character class // * load all classes? const vendor = vendorResponse?.vendors.data?.[vendorHash]; // Unadvertised nullability: DestinyVendorDefinition.locations const destinationHash = vendor?.vendorLocationIndex && vendorDef.locations?.[vendor.vendorLocationIndex].destinationHash; const destinationDef = destinationHash ? defs.Destination.get(destinationHash) : undefined; const placeDef = destinationDef && defs.Place.get(destinationDef.placeHash); const placeString = [destinationDef?.displayProperties.name, placeDef?.displayProperties.name] .filter((n) => n?.length) .join(', '); // TODO: there's a cool background image but I'm not sure how to use it let displayName = vendorDef.displayProperties.name; let displayDesc = vendorDef.displayProperties.description; let isArtifact = false; // if this vendor is the seasonal artifact if (vendorDef.displayCategories.find((c) => c.identifier === 'category_reset')) { // search for the associated item. this is way harder than it should be, but we have what we are given const seasonHash = profileResponse?.profile.data?.currentSeasonHash; const artifactDisplay = Object.values(defs.InventoryItem.getAll()).find( (i) => // belongs to the current season, and looks like an artifact i.seasonHash === seasonHash && /\.artifacts?\./.test(i.inventory!.stackUniqueLabel ?? ''), )?.displayProperties; if (artifactDisplay) { displayName = artifactDisplay.name; displayDesc = artifactDisplay.description; isArtifact = true; } } // The artifact vendor isn't returned by Bungie.net, contains too // many artifact mods, and doesn't allow us to figure out what's unlocked // and what isn't, so we instead use <ArtifactUnlocks />, based on the character // progression. For all the normal vendors we use the vendor items. let d2Vendor: D2Vendor | undefined; let refreshTime: Date | undefined; if (!isArtifact) { d2Vendor = toVendor( { ...itemCreationContext, itemComponents: vendorResponse?.itemComponents?.[vendorHash] }, vendorHash, vendor, characterId, vendorResponse?.sales.data?.[vendorHash]?.saleItems, vendorResponse, ); if (!d2Vendor) { return <ErrorPanel error={new Error(`No known vendor with hash ${vendorHash}`)} />; } d2Vendor = { ...d2Vendor, items: d2Vendor.items.filter((i) => itemFilter(i, d2Vendor!)) }; refreshTime = d2Vendor.component && new Date(d2Vendor.component.nextRefreshDate); if (refreshTime?.getFullYear() === 9999) { refreshTime = undefined; } } return ( <> <div className={styles.featuredHeader}> <h1> {displayName} <VendorLocation>{placeString}</VendorLocation> </h1> <div>{displayDesc}</div> {refreshTime && ( <div> {t('Vendors.RefreshTime')} <Countdown endTime={refreshTime} /> </div> )} </div> {isArtifact ? ( <ArtifactUnlocks characterId={characterId} /> ) : ( <VendorItems vendor={d2Vendor!} ownedItemHashes={ownedItemHashes} currencyLookups={vendorResponse?.currencyLookups.data?.itemQuantities ?? {}} characterId={characterId} /> )} </> ); } ================================================ FILE: src/app/vendors/single-vendor/SingleVendorPage.m.scss ================================================ .page { margin-top: 16px; } ================================================ FILE: src/app/vendors/single-vendor/SingleVendorPage.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'page': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/single-vendor/SingleVendorPage.tsx ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import { getCurrentStore } from 'app/inventory/stores-helpers'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; import { useLocation, useParams } from 'react-router'; import { storesSelector } from '../../inventory/selectors'; import SingleVendor from './SingleVendor'; import * as styles from './SingleVendorPage.m.scss'; /** * A page that loads its own info for a single vendor, so we can link to a vendor or show engram previews. */ export default function SingleVendorPage({ account }: { account: DestinyAccount }) { const { vendorHash: vendorHashString } = useParams(); const vendorHash = parseInt(vendorHashString ?? '', 10); const { search } = useLocation(); const stores = useSelector(storesSelector); // TODO: get for all characters, or let people select a character? This is a hack // we at least need to display that character! const characterId = (search && new URLSearchParams(search).get('characterId')) || (stores.length && getCurrentStore(stores)?.id); if (!characterId) { throw new Error('no characters chosen or found to use for vendor API call'); } return ( <div className={clsx(styles.page, 'dim-page')}> <SingleVendor account={account} vendorHash={vendorHash} characterId={characterId} updatePageTitle /> </div> ); } ================================================ FILE: src/app/vendors/single-vendor/SingleVendorSheet.m.scss ================================================ .vendorSheet { composes: sheet from 'app/armory/ArmorySheet.m.scss'; } .sheetContents { padding: 16px; } ================================================ FILE: src/app/vendors/single-vendor/SingleVendorSheet.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'sheetContents': string; 'vendorSheet': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/vendors/single-vendor/SingleVendorSheet.tsx ================================================ import { DestinyAccount } from 'app/accounts/destiny-account'; import ClickOutsideRoot from 'app/dim-ui/ClickOutsideRoot'; import Sheet from 'app/dim-ui/Sheet'; import SingleVendor from './SingleVendor'; import * as styles from './SingleVendorSheet.m.scss'; export default function SingleVendorSheet({ account, characterId, vendorHash, onClose, }: { account: DestinyAccount; characterId: string; vendorHash: number; onClose: () => void; }) { return ( <Sheet key={vendorHash} onClose={onClose} sheetClassName={styles.vendorSheet} allowClickThrough> <ClickOutsideRoot> <div className={styles.sheetContents}> <SingleVendor account={account} characterId={characterId} vendorHash={vendorHash} /> </div> </ClickOutsideRoot> </Sheet> ); } ================================================ FILE: src/app/vendors/single-vendor/SingleVendorSheetContainer.tsx ================================================ import { currentAccountSelector } from 'app/accounts/selectors'; import { useEventBusListener } from 'app/utils/hooks'; import React, { Suspense, createContext, lazy, useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { SingleVendorState, hideVendorSheet$ } from './single-vendor-sheet'; const SingleVendorSheet = lazy( async () => import(/* webpackChunkName: "vendors" */ 'app/vendors/single-vendor/SingleVendorSheet'), ); export const SingleVendorSheetContext = createContext<React.Dispatch< React.SetStateAction<SingleVendorState> > | null>(null); export default function SingleVendorSheetContainer({ children }: { children: React.ReactNode }) { const account = useSelector(currentAccountSelector); const [currentVendorHash, setCurrentVendorHash] = useState<SingleVendorState>({}); const onClose = useCallback(() => { setCurrentVendorHash({}); }, []); useEventBusListener( hideVendorSheet$, useCallback(() => { setCurrentVendorHash({}); }, []), ); return ( <> <SingleVendorSheetContext value={setCurrentVendorHash}> {children} <Suspense fallback={null}> {account && currentVendorHash.characterId && currentVendorHash.vendorHash !== undefined && ( <SingleVendorSheet account={account} characterId={currentVendorHash.characterId} vendorHash={currentVendorHash.vendorHash} onClose={onClose} /> )} </Suspense> </SingleVendorSheetContext> </> ); } ================================================ FILE: src/app/vendors/single-vendor/single-vendor-sheet.ts ================================================ import { EventBus } from 'app/utils/observable'; export interface SingleVendorState { characterId?: string; vendorHash?: number; } export const hideVendorSheet$ = new EventBus<undefined>(); export function hideVendorSheet() { hideVendorSheet$.next(undefined); } ================================================ FILE: src/app/vendors/specialVendorStrings.d.ts ================================================ declare module 'data/d2/special-vendors-strings.json' { export interface VendorFailureString { vendorHash: number; index: number; } const value: Record<string, VendorFailureString>; export default value; } ================================================ FILE: src/app/vendors/vendor-item.ts ================================================ import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { createCollectibleFinder } from 'app/records/collectible-matching'; import { THE_FORBIDDEN_BUCKET, VendorHashes } from 'app/search/d2-known-values'; import { emptyArray } from 'app/utils/empty'; import { DestinyClass, DestinyCollectibleState, DestinyDisplayPropertiesDefinition, DestinyInventoryItemDefinition, DestinyItemQuantity, DestinyProfileResponse, DestinyVendorDefinition, DestinyVendorItemDefinition, DestinyVendorItemState, DestinyVendorSaleItemComponent, } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { DimItem } from '../inventory/item-types'; import { ItemCreationContext, makeFakeItem } from '../inventory/store/d2-item-factory'; const SYNTH_BOUNTIES_EXHAUSTED = [1073165367, 2363327331, 2376000422]; const SYNTH_BOUNTIES_DUMMY = [171866827, 540971012, 3950721485]; const DestinyClassToSynthTooltipIndex: { [key in DestinyClass]: number } = { [DestinyClass.Titan]: 1, [DestinyClass.Hunter]: 2, [DestinyClass.Warlock]: 0, [DestinyClass.Classified]: -1, [DestinyClass.Unknown]: -1, }; /** * This represents an item inside a vendor. */ export interface VendorItem { readonly item: DimItem | undefined; readonly failureStrings: string[]; /** The index in the vendor definition's saleItems array. Unique to this item within a vendor. */ readonly vendorItemIndex: number; readonly displayProperties: DestinyDisplayPropertiesDefinition; readonly borderless: boolean; readonly displayTile: boolean; /** Indicates that the vendor API marks this item as owned, which is used for upgrades. */ readonly owned: boolean; /** Indicates that the vendor API marks this item as locked, which is used for time-gated upgrades */ readonly locked: boolean; readonly canBeSold: boolean; readonly displayCategoryIndex?: number; readonly originalCategoryIndex?: number; readonly costs: DestinyItemQuantity[]; readonly previewVendorHash?: number; /** The state of this item in the user's D2 Collection */ readonly collectibleState?: DestinyCollectibleState; } /** * Find the state of this item in the user's collections. This takes into account * the selected character. */ function getCollectibleState( defs: D2ManifestDefinitions, inventoryItem: DestinyInventoryItemDefinition, profileResponse: DestinyProfileResponse | undefined, characterId: string, ) { const collectibleFinder = createCollectibleFinder(defs); const collectibleHash = collectibleFinder(inventoryItem)?.hash; let collectibleState: DestinyCollectibleState | undefined; if (collectibleHash) { collectibleState = profileResponse?.profileCollectibles?.data?.collectibles[collectibleHash]?.state ?? (characterId ? profileResponse?.characterCollectibles?.data?.[characterId]?.collectibles[collectibleHash] ?.state : undefined); } return collectibleState; } function makeVendorItem( context: ItemCreationContext, itemHash: number, failureStrings: string[], vendorHash: number, vendorItemDef: DestinyVendorItemDefinition, saleItem: DestinyVendorSaleItemComponent | undefined, // the character to whom this item is being offered characterId: string, // the index in the vendor's items array vendorItemIndex: number, nextRefreshDate?: string, ): VendorItem { const { defs, profileResponse } = context; const inventoryItem = defs.InventoryItem.get(itemHash); let tooltipNotificationIndexes: number[] = []; if (SYNTH_BOUNTIES_EXHAUSTED.includes(itemHash)) { tooltipNotificationIndexes = [0]; } else if (SYNTH_BOUNTIES_DUMMY.includes(itemHash)) { const classType = profileResponse?.characters?.data?.[characterId]?.classType; if (classType !== undefined && DestinyClassToSynthTooltipIndex[classType] > -1) { tooltipNotificationIndexes = [DestinyClassToSynthTooltipIndex[classType]]; } } const vendorItem: VendorItem = { failureStrings, vendorItemIndex, displayProperties: inventoryItem.displayProperties, borderless: Boolean(inventoryItem.uiItemDisplayStyle), displayTile: inventoryItem.uiItemDisplayStyle === 'ui_display_style_set_container', owned: Boolean( (!inventoryItem.inventory || inventoryItem.inventory.bucketTypeHash === THE_FORBIDDEN_BUCKET) && (saleItem?.augments || 0) & DestinyVendorItemState.Owned, ), locked: Boolean((saleItem?.augments || 0) & DestinyVendorItemState.Locked), canBeSold: !saleItem || saleItem.failureIndexes.length === 0, displayCategoryIndex: vendorItemDef?.displayCategoryIndex, originalCategoryIndex: vendorItemDef?.originalCategoryIndex, costs: saleItem?.costs || [], previewVendorHash: inventoryItem.preview?.previewVendorHash, collectibleState: getCollectibleState( context.defs, inventoryItem, profileResponse, characterId, ), item: makeFakeItem(context, itemHash, { // For sale items the item ID needs to be the vendor item index, since that's how we look up item components for perks itemInstanceId: vendorItemIndex.toString(), quantity: vendorItemDef ? vendorItemDef.quantity : 1, // vendor items are wish list enabled! allowWishList: true, itemValueVisibility: saleItem?.itemValueVisibility, tooltipNotificationIndexes, }), }; if (vendorItem.item) { vendorItem.item.hidePercentage = true; // override the DimItem.id for vendor items, so they are each unique enough to identify // (otherwise they'd get their vendor index as an id, which is only unique per-vendor) // Lowercase to match post-parsing filter strings with `id:` vendorItem.item.id = `${vendorHash}-${vendorItem.vendorItemIndex}-${nextRefreshDate?.toLowerCase() ?? '0'}`; vendorItem.item.index = vendorItem.item.id; vendorItem.item.instanced = false; // These would normally be false already, but certain rules like "finishers // are lockable" mess that up, so we set them explicitly here. vendorItem.item.lockable = false; // since this is sold by a vendor, add vendor information vendorItem.item.vendor = { vendorHash, vendorItemIndex, characterId }; if (vendorItem.item.equipment && vendorItem.item.bucket.hash !== BucketHashes.Emblems) { vendorItem.item.comparable = true; } } // only apply for 2255782930, master rahool if (vendorHash === VendorHashes.Rahool && saleItem?.overrideStyleItemHash && vendorItem.item) { const itemDef = defs.InventoryItem.get(saleItem.overrideStyleItemHash); if (itemDef) { const display = itemDef.displayProperties; vendorItem.item.name = display.name; vendorItem.item.icon = display.icon; } } return vendorItem; } /** * creates a VendorItem being sold by a vendor in the API vendors response. * this can include "instanced" stats plugs etc which describe the specifics * of that copy they're selling */ export function vendorItemForSaleItem( context: ItemCreationContext, vendorDef: DestinyVendorDefinition, saleItem: DestinyVendorSaleItemComponent, /** all DIM vendor calls are character-specific. any sale item should have an associated character. */ characterId: string, nextRefreshDate?: string, ): VendorItem { const vendorItemDef = vendorDef.itemList[saleItem.vendorItemIndex]; const failureStrings = saleItem && vendorDef && saleItem.failureIndexes ? saleItem.failureIndexes.map((i) => vendorDef.failureStrings[i]) : emptyArray<string>(); return makeVendorItem( context, saleItem.itemHash, failureStrings, vendorDef.hash, vendorItemDef, saleItem, characterId, saleItem.vendorItemIndex, nextRefreshDate, ); } /** * creates a VendorItem solely according to a vendor's definition. * some vendors are set up so statically, that they have no data in the live Vendors response */ export function vendorItemForDefinitionItem( context: ItemCreationContext, vendorItemDef: DestinyVendorItemDefinition, characterId: string, // the index in the vendor's items array vendorItemIndex: number, nextRefreshDate?: string, ): VendorItem { const item = makeVendorItem( context, vendorItemDef.itemHash, [], 0, vendorItemDef, undefined, characterId, vendorItemIndex, nextRefreshDate, ); return item; } ================================================ FILE: src/app/whats-new/BungieAlerts.m.scss ================================================ @use 'sass:color'; @use '../variables.scss' as *; .bungie-alert { margin: 1em 0; padding: 1em; a { color: var(--theme-text); } h2 { margin: 0 0 8px 0 !important; letter-spacing: normal !important; text-transform: none !important; font-size: 14px; font-weight: bold; } } .error { composes: bungie-alert; background: color.scale($red, $lightness: -90%); border-top: 4px solid $red; } .info { composes: bungie-alert; background: color.scale(#2f96b4, $lightness: -90%); border-top: 4px solid #2f96b4; } .warn { composes: bungie-alert; background: color.scale(#f89406, $lightness: -90%); border-top: 4px solid #f89406; } :global(.bungie-alert-error) { background: $red !important; } :global(.bungie-alert-info) { background: #2f96b4 !important; } :global(.bungie-alert-warn) { background: #f89406 !important; } ================================================ FILE: src/app/whats-new/BungieAlerts.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'bungieAlert': string; 'error': string; 'info': string; 'warn': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/whats-new/BungieAlerts.tsx ================================================ import { t } from 'app/i18next-t'; import { bungieHelpAccount, bungieHelpLink } from 'app/shell/links'; import { bungieAlertsSelector } from 'app/shell/selectors'; import { GlobalAlertLevel } from 'bungie-api-ts/core'; import { useSelector } from 'react-redux'; import ExternalLink from '../dim-ui/ExternalLink'; import * as styles from './BungieAlerts.m.scss'; // http://destinydevs.github.io/BungieNetPlatform/docs/Enums export const GlobalAlertLevelsToToastLevels = [ 'info', // Unknown 'info', // Blue 'warn', // Yellow 'error', // Red ]; const AlertLevelStyles = { [GlobalAlertLevel.Unknown]: styles.info, [GlobalAlertLevel.Blue]: styles.info, [GlobalAlertLevel.Yellow]: styles.warn, [GlobalAlertLevel.Red]: styles.error, }; /** * Displays maintenance alerts from Bungie.net. */ export default function BungieAlerts() { const alerts = useSelector(bungieAlertsSelector); return ( <div> {alerts.map((alert) => ( <div key={alert.AlertKey} className={AlertLevelStyles[alert.AlertLevel]}> <h2>{t('BungieAlert.Title')}</h2> <p dangerouslySetInnerHTML={{ __html: alert.AlertHtml }} /> <div> {t('BungieService.Twitter')}{' '} <ExternalLink href={bungieHelpLink}>{bungieHelpAccount}</ExternalLink> </div> </div> ))} </div> ); } ================================================ FILE: src/app/whats-new/ChangeLog.scss ================================================ @use '../variables.scss' as *; .changelog-date { font-size: 12px; color: #ccc; margin-left: 8px; } code { background-color: #00000060; padding: 0 3px; border-radius: 2px; } ================================================ FILE: src/app/whats-new/ChangeLog.tsx ================================================ import changelog from 'docs/CHANGELOG.md'; import { useEffect } from 'react'; import './ChangeLog.scss'; import { DimVersions } from './versions'; /** * Show the DIM Changelog, with highlights for new changes. */ export default function ChangeLog() { useEffect(() => { DimVersions.changelogWasViewed(); }, []); return ( <> <h1>DIM Changes</h1> <Markdown>{changelog}</Markdown> </> ); } function Markdown({ children }: { children: string }) { return <div dangerouslySetInnerHTML={{ __html: children }} />; } ================================================ FILE: src/app/whats-new/WhatsNew.tsx ================================================ import StaticPage from 'app/dim-ui/StaticPage'; import { t } from 'app/i18next-t'; import { usePageTitle } from 'app/utils/hooks'; import BungieAlerts from './BungieAlerts'; import ChangeLog from './ChangeLog'; /** * What's new in the world of DIM? */ export default function WhatsNew() { usePageTitle(t('Header.WhatsNew')); return ( <StaticPage> <BungieAlerts /> <ChangeLog /> </StaticPage> ); } ================================================ FILE: src/app/whats-new/WhatsNewLink.m.scss ================================================ @use '../variables.scss' as *; .upgrade { color: $upgrade-notification-dot !important; font-size: 18px !important; margin-right: 4px; } .badgeNew { display: inline-block; background-color: $new-notification-dot; border-radius: 50%; height: 8px; width: 8px; margin: 0 4px 0 0 !important; vertical-align: middle; } ================================================ FILE: src/app/whats-new/WhatsNewLink.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'badgeNew': string; 'upgrade': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/whats-new/WhatsNewLink.tsx ================================================ import { t } from 'app/i18next-t'; import { bungieAlertsSelector } from 'app/shell/selectors'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; import { NavLink } from 'react-router'; import { useSubscription } from 'use-subscription'; import { dimNeedsUpdate$, reloadDIM } from '../register-service-worker'; import { AppIcon, updateIcon } from '../shell/icons'; import { GlobalAlertLevelsToToastLevels } from './BungieAlerts'; import * as styles from './WhatsNewLink.m.scss'; import { DimVersions } from './versions'; /** * A link/button to the "What's New" page that highlights the most important action. */ export default function WhatsNewLink({ className, }: { className: (props: { isActive: boolean }) => string; }) { const showChangelog = useSubscription(DimVersions.showChangelog$); const alerts = useSelector(bungieAlertsSelector); const dimNeedsUpdate = useSubscription(dimNeedsUpdate$); // TODO: use presstip/tooltip to help? // TODO: try dots and bottom-borders if (dimNeedsUpdate) { return ( <a className={className({ isActive: false })} onClick={reloadDIM}> <AppIcon className={styles.upgrade} icon={updateIcon} ariaHidden /> {t('Header.UpgradeDIM')} </a> ); } if (alerts.length) { return ( <NavLink to="/whats-new" className={className}> <span className={clsx( styles.badgeNew, `bungie-alert-${GlobalAlertLevelsToToastLevels[alerts[0].AlertLevel]}`, )} />{' '} {t('Header.BungieNetAlert')} </NavLink> ); } if (showChangelog) { return ( <NavLink to="/whats-new" className={className}> <span className={styles.badgeNew} /> {t('Header.WhatsNew')} </NavLink> ); } return ( <NavLink to="/whats-new" className={className}> {t('Header.WhatsNew')} </NavLink> ); } ================================================ FILE: src/app/whats-new/versions.ts ================================================ import { Observable } from 'app/utils/observable'; const localStorageKey = 'dim-changelog-viewed-version'; /** Information about the user's relationship with DIM versions */ class Versions { readonly currentVersion = cleanVersion($DIM_VERSION)!; previousVersion = cleanVersion(localStorage.getItem(localStorageKey)); /** An observable for whether to show the changelog. */ showChangelog$ = new Observable(this.showChangelog); /** * Signify that the changelog page has been viewed. */ changelogWasViewed() { localStorage.setItem(localStorageKey, this.currentVersion); this.previousVersion = this.currentVersion; this.showChangelog$.next(this.showChangelog); } versionIsNew(version: string) { if (version === 'Next') { return false; } if (this.previousVersion) { return compareVersions(version, this.previousVersion) > 0; } else { return false; } } // TODO: It'd be nice to also check whether the changelog has any entries between versions... // TODO: it'd be good to store this in settings, so you sync the last version you've seen private get showChangelog() { // Don't highlight the changelog if this is their first time using DIM. // This also helps with folks who lose their storage. if (this.previousVersion === null) { return false; } return this.currentVersion !== this.previousVersion; } } // Clean out Beta versions to ignore their build number. function cleanVersion(version: string | null) { if (version) { return version.split('.').slice(0, 3).join('.'); } return version; } function splitVersion(version: string): number[] { return version.split('.').map((s) => parseInt(s, 10)); } function compareVersions(version1: string, version2: string) { const v1 = splitVersion(version1); const v2 = splitVersion(version2); for (let i = 0; i < 3; i++) { if ((v1[i] || 0) > (v2[i] || 0)) { return 1; } else if ((v1[i] || 0) < (v2[i] || 0)) { return -1; } } return 0; } export const DimVersions = new Versions(); ================================================ FILE: src/app/wishlists/WishListPerkThumb.m.scss ================================================ .trashlist { color: #d14334; } .thumb { background: #ddd; border-radius: 50%; color: #0b486b; padding: 3px; font-size: 8px !important; letter-spacing: normal; } // For when the thumb should appear above a perk circle .floated { position: absolute; top: -4px; right: -5px; filter: drop-shadow(0 0 2px rgb(0, 0, 0, 0.9)); } ================================================ FILE: src/app/wishlists/WishListPerkThumb.m.scss.d.ts ================================================ // This file is automatically generated. // Please do not change this file! interface CssExports { 'floated': string; 'thumb': string; 'trashlist': string; } export const cssExports: CssExports; export = cssExports; ================================================ FILE: src/app/wishlists/WishListPerkThumb.tsx ================================================ import { t } from 'app/i18next-t'; import { AppIcon, thumbsDownIcon, thumbsUpIcon } from 'app/shell/icons'; import clsx from 'clsx'; import * as styles from './WishListPerkThumb.m.scss'; import { WishListRoll } from './types'; import { InventoryWishListRoll } from './wishlists'; /** * The little thumbs-up (or down) shown on wishlisted perks. */ export default function WishListPerkThumb({ wishListRoll, className, floated, legend, }: { wishListRoll: WishListRoll | InventoryWishListRoll; className?: string; floated?: boolean; legend?: boolean; }) { const perks = wishListRoll && ('recommendedPerks' in wishListRoll ? wishListRoll.recommendedPerks : wishListRoll.wishListPerks); const title = wishListRoll.isUndesirable ? t('WishListRoll.WorstRatedTip', { count: perks.size }) : t('WishListRoll.BestRatedTip', { count: perks.size }); return ( <> <AppIcon className={clsx(styles.thumb, className, { [styles.floated]: floated, [styles.trashlist]: wishListRoll.isUndesirable, })} icon={wishListRoll.isUndesirable ? thumbsDownIcon : thumbsUpIcon} title={title} /> {legend && <> = {title}</>} </> ); } ================================================ FILE: src/app/wishlists/actions.ts ================================================ import { createAction } from 'typesafe-actions'; import { WishListAndInfo } from './types'; export const loadWishLists = createAction('wishlists/LOAD')<{ wishListAndInfo: WishListAndInfo; // Defaults to "now" but can be set if we're loading from IndexedDB lastFetched?: Date; }>(); export const clearWishLists = createAction('wishlists/CLEAR')(); export const touchWishLists = createAction('wishlists/TOUCH')(); ================================================ FILE: src/app/wishlists/observers.ts ================================================ import { set } from 'app/storage/idb-keyval'; import { StoreObserver } from 'app/store/observerMiddleware'; import { WishListsState } from './reducer'; export function createWishlistObserver(): StoreObserver<WishListsState> { return { id: 'wish-list-observer', getObserved: (rootState) => rootState.wishLists, sideEffect: ({ current }) => { if (current.loaded) { set('wishlist', { wishListAndInfo: current.wishListAndInfo, lastFetched: current.lastFetched, }); } }, }; } ================================================ FILE: src/app/wishlists/reducer.ts ================================================ import { Reducer } from 'redux'; import { ActionType, getType } from 'typesafe-actions'; import * as actions from './actions'; import { WishListAndInfo } from './types'; export interface WishListsState { loaded: boolean; wishListAndInfo: WishListAndInfo; lastFetched?: Date; } export type WishListAction = ActionType<typeof actions>; const initialState: WishListsState = { loaded: false, wishListAndInfo: { infos: [], wishListRolls: [] }, lastFetched: undefined, }; export const wishLists: Reducer<WishListsState, WishListAction> = ( state: WishListsState = initialState, action: WishListAction, ): WishListsState => { switch (action.type) { case getType(actions.loadWishLists): return { ...state, wishListAndInfo: { ...initialState.wishListAndInfo, ...action.payload.wishListAndInfo }, loaded: true, lastFetched: action.payload.lastFetched || new Date(), }; case getType(actions.clearWishLists): { return { ...state, wishListAndInfo: { infos: [], wishListRolls: [], source: '', }, lastFetched: undefined, loaded: true, }; } case getType(actions.touchWishLists): { return { ...state, lastFetched: new Date(), }; } default: return state; } }; ================================================ FILE: src/app/wishlists/selectors.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import { RootState } from 'app/store/types'; import { emptyArray } from 'app/utils/empty'; import { createSelector } from 'reselect'; import { getInventoryWishListRoll, InventoryWishListRoll } from './wishlists'; export const wishListsSelector = (state: RootState) => state.wishLists; export const wishListInfosSelector = (state: RootState) => state.wishLists.wishListAndInfo.infos; export const wishListsLastFetchedSelector = (state: RootState) => wishListsSelector(state).lastFetched; export const wishListsByHashSelector = createSelector(wishListsSelector, (wls) => Map.groupBy(wls.wishListAndInfo.wishListRolls?.filter(Boolean), (r) => r.itemHash), ); export const wishListRollsForItemHashSelector = (itemHash: number) => (state: RootState) => wishListsByHashSelector(state).get(itemHash) ?? emptyArray(); export const hasWishListSelector = createSelector( wishListsByHashSelector, (wishlists) => wishlists.size > 0, ); /** Returns a memoized function to look up wishlist by item, which is reset when the wishlist changes. Prefer wishListSelector */ export const wishListFunctionSelector = createSelector( wishListsByHashSelector, (wishlists): ((item: DimItem) => InventoryWishListRoll | undefined) => { // Cache of inventory item id to roll. For this to work, make sure vendor/collections rolls have unique ids. const cache = new Map<string, InventoryWishListRoll | null>(); return (item: DimItem) => { if ( !($featureFlags.wishLists && wishlists && item.wishListEnabled) || !item.sockets || item.sockets.fromDefinitions ) { return undefined; } const cachedRoll = cache.get(item.id); if (cachedRoll !== undefined) { return cachedRoll || undefined; } const roll = getInventoryWishListRoll(item, wishlists); cache.set(item.id, roll ?? null); return roll; }; }, ); /** * A reverse-curried selector that is easier to use in useSelector. This will re-render the component only when * this item's wishlist state changes. * * @example * * useSelector(wishListSelector(item)) */ export const wishListSelector = (item: DimItem) => (state: RootState) => wishListFunctionSelector(state)(item); ================================================ FILE: src/app/wishlists/types.ts ================================================ export const enum DimWishList { WildcardItemId = -69420, // nice } /** * Interface for translating lists of wish list rolls to a format we can use. * Initially, support for translating banshee-44.com -> this has been built, * but this is here so that we can plug in support for anyone else that can * get us this information. */ export interface WishListRoll { /** Item hash for the recommended item, OR an item category hash, OR the special WildcardItemId. */ itemHash: number; /** * All of the perks (perk hashes) that need to be present for an item roll to * be recognized as a wish list roll. * Note that we'll discard some (intrinsics, shaders, masterworks) by default. * Also note that fuzzy matching isn't present, but can be faked by removing * perks that are thought to have marginal bearing on an item. */ recommendedPerks: Set<number>; /** * Is this an expert mode recommendation? * With B-44 rolls, we make sure that most every perk asked for exists * on the item. (It does discard masterwork and some other odds and ends). * With expert rolls, you can be as vague or specific as you want, so we make * sure that at least every perk asked for is there. */ isExpertMode: boolean; /** * Is this an undesirable item/roll? * By default, we expect things in the wish list to be desired rolls, but * it's possible that you might have some items/rolls that you want no part of. * We'll mark undesirable items with a thumbs down instead. */ isUndesirable?: boolean; /** Optional notes from the curator. */ notes?: string; /** * The index into the "infos" list for which wishlist this roll was sourced * from. Not set on some old wishlists from storage. */ sourceWishListIndex?: number; /** Optional title and description from the curator. */ title?: string; description?: string; } export interface WishListAndInfo { /** A merged list of wish list rolls from each of the source wish lists. */ wishListRolls: WishListRoll[]; /** The URL(s) we fetched the wish list(s) from */ source?: string; /** Information about the wishlists themselves that contribute to the rolls. */ infos: WishListInfo[]; } export interface WishListInfo { /** The wish list URL. If undefined, this is a local wish list. */ url: string | undefined; title?: string; description?: string; /** The number of rolls from this wish list that actually made it in (e.g. were valid and unique). */ numRolls: number; /** The number of rolls in this list that were duplicates of other lists. */ dupeRolls: number; } ================================================ FILE: src/app/wishlists/utils.ts ================================================ import { I18nKey, tl } from 'app/i18next-t'; import { filterMap } from 'app/utils/collections'; export const builtInWishlists: { name: I18nKey; url: string }[] = [ { name: tl('WishListRoll.Voltron'), url: 'https://raw.githubusercontent.com/48klocs/dim-wish-list-sources/master/voltron.txt', }, { name: tl('WishListRoll.JustAnotherTeam'), url: 'https://raw.githubusercontent.com/dsf000z/JAT-wishlists-bundler/refs/heads/main/bundles/DIM/just-another-team-mnk.txt', }, ]; // config/content-security-policy.js must be edited alongside this list export const wishListAllowedHosts = ['raw.githubusercontent.com', 'gist.githubusercontent.com']; export function validateWishListURLs(url: string): string[] { return filterMap(url.split('|'), (url) => { url = url.trim(); if (!url) { return undefined; // skip empty strings } try { const parsedUrl = new URL(url); // throws if invalid if (parsedUrl.protocol !== 'https:') { return undefined; } else if (!wishListAllowedHosts.includes(parsedUrl.host)) { // If folks paste the github link, change it to the raw link if (parsedUrl.host === 'github.com') { // e.g. github.com/48klocs/dim-wish-list-sources/blob/master/voltron.txt => https://raw.githubusercontent.com/48klocs/dim-wish-list-sources/refs/heads/master/voltron.txt const match = parsedUrl.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/(.*)/); if (match) { return `https://raw.githubusercontent.com/${match[1]}/${match[2]}/refs/heads/${match[3]}`; } } return undefined; } return parsedUrl.toString(); } catch { return undefined; } }); } ================================================ FILE: src/app/wishlists/wishlist-fetch.ts ================================================ import { toHttpStatusError } from 'app/bungie-api/http-client'; import { settingsSelector } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { showNotification } from 'app/notifications/notifications'; import { setSettingAction } from 'app/settings/actions'; import { settingsReady } from 'app/settings/settings'; import { get } from 'app/storage/idb-keyval'; import { ThunkResult } from 'app/store/types'; import { errorMessage } from 'app/utils/errors'; import { errorLog, infoLog } from 'app/utils/log'; import { once } from 'es-toolkit'; import { clearWishLists, loadWishLists, touchWishLists } from './actions'; import type { WishListsState } from './reducer'; import { wishListsSelector } from './selectors'; import { WishListAndInfo } from './types'; import { validateWishListURLs } from './utils'; import { toWishList } from './wishlist-file'; const TAG = 'wishlist'; function hoursAgo(dateToCheck?: Date): number { if (!dateToCheck) { return 99999; } return (Date.now() - dateToCheck.getTime()) / (1000 * 60 * 60); } /** * this performs both the initial fetch (after setting a new wishlist) (when newWishlistSource exists) * and subsequent fetches (checking for updates) (arg-less) */ export function fetchWishList(newWishlistSource?: string, manualRefresh?: boolean): ThunkResult { return async (dispatch, getState) => { await dispatch(loadWishListAndInfoFromIndexedDB()); await settingsReady; const existingWishListSource = settingsSelector(getState()).wishListSource; // a blank source was submitted, indicating an intention to clear the wishlist if (newWishlistSource === '' && newWishlistSource !== existingWishListSource) { dispatch(clearWishLists()); return; } const wishlistToFetch = newWishlistSource ?? existingWishListSource; // done if there's neither an existing nor new URL if (!wishlistToFetch) { return; } const { lastFetched: wishListLastUpdated, wishListAndInfo: { source: loadedWishListSource, wishListRolls: loadedWishListRolls }, } = wishListsSelector(getState()); const wishListURLsChanged = loadedWishListSource !== undefined && loadedWishListSource !== wishlistToFetch; // Throttle updates if: if ( // this isn't a settings update, and !newWishlistSource && // if the intended fetch target is already the source of the loaded list !wishListURLsChanged && // we already checked the wishlist today hoursAgo(wishListLastUpdated) < 24 ) { return; } // Pipe | separated URLs const wishlistUrlsToFetch = validateWishListURLs(wishlistToFetch); const wishListResults = await Promise.allSettled( wishlistUrlsToFetch.map(async (url) => { const res = await fetch(url); if (res.status < 200 || res.status >= 300) { throw await toHttpStatusError(res); } return res.text(); }), ); const wishLists: [url: string, text: string][] = []; let hasSuccess = false; for (let i = 0; i < wishlistUrlsToFetch.length; i++) { const url = wishlistUrlsToFetch[i]; const result = wishListResults[i]; if (result.status === 'rejected') { showNotification({ type: 'warning', title: t('WishListRoll.Header'), body: t('WishListRoll.ImportError', { url, error: errorMessage(result.reason) }), }); errorLog(TAG, 'Unable to load wish list', url, result.reason); } else if (result.status === 'fulfilled') { hasSuccess = true; wishLists.push([url, result.value]); } } if (!hasSuccess) { // Give up if we couldn't fetch any of the lists return; } // if this is a new wishlist, set the setting now that we know at least one list is fetchable if (newWishlistSource && hasSuccess) { dispatch(setSettingAction('wishListSource', newWishlistSource)); } const wishListAndInfo = toWishList(wishLists); wishListAndInfo.source = wishlistToFetch; // Only update if the length changed. The wish list may actually be different - we don't do a deep check - // but this is good enough to avoid re-doing the work over and over. // If the user manually refreshed, do the work anyway if ( loadedWishListRolls?.length !== wishListAndInfo.wishListRolls.length || wishListURLsChanged || manualRefresh ) { await dispatch(transformAndStoreWishList(wishListAndInfo)); } else { infoLog(TAG, 'Refreshed wishlist, but it matched the one we already have'); dispatch(touchWishLists()); } }; } export function transformAndStoreWishList(wishListAndInfo: WishListAndInfo): ThunkResult { return async (dispatch) => { if (wishListAndInfo.wishListRolls.length > 0) { dispatch(loadWishLists({ wishListAndInfo })); } else { showNotification({ type: 'warning', title: t('WishListRoll.Header'), body: t('WishListRoll.ImportFailed'), }); } }; } function loadWishListAndInfoFromIndexedDB(): ThunkResult { return async (dispatch, getState) => { if (getState().wishLists.loaded) { return; } try { const wishListState = await get<WishListsState>('wishlist'); if (getState().wishLists.loaded) { return; } // Previously we didn't save the URLs together with the source info, // but we want this now. if (wishListState?.wishListAndInfo.source) { const urls = once(() => validateWishListURLs(wishListState.wishListAndInfo.source!)); for (const [idx, entry] of wishListState.wishListAndInfo.infos.entries()) { if (entry.url === undefined) { entry.url = urls()[idx]; } } } if (wishListState?.wishListAndInfo?.wishListRolls?.length) { dispatch(loadWishLists(wishListState)); } } catch (e) { errorLog(TAG, 'unable to load wishlists from IDB', e); } }; } ================================================ FILE: src/app/wishlists/wishlist-file.test.ts ================================================ import { WishListRoll } from './types'; import { toWishList } from './wishlist-file'; const cases: [wishlist: string, result: WishListRoll][] = [ [ 'dimwishlist:item=-69420&perks=2682205016,2402480669#notes:Enh Over, Enh FocuFury', { itemHash: -69420, recommendedPerks: new Set([2682205016, 2402480669]), notes: 'Enh Over, Enh FocuFury', isExpertMode: true, isUndesirable: false, description: undefined, sourceWishListIndex: 0, title: undefined, }, ], [ ` //notes:Example1 with\\nline break working dimwishlist:item=-69420&perks=2682205016,2402480669`, { itemHash: -69420, recommendedPerks: new Set([2682205016, 2402480669]), notes: 'Example1 with\nline break working', isExpertMode: true, isUndesirable: false, description: undefined, sourceWishListIndex: 0, title: undefined, }, ], [ `dimwishlist:item=-69420&perks=2682205016,2402480669#notes:Example2 with\\nline break working`, { itemHash: -69420, recommendedPerks: new Set([2682205016, 2402480669]), notes: 'Example2 with\nline break working', isExpertMode: true, isUndesirable: false, description: undefined, sourceWishListIndex: 0, title: undefined, }, ], ]; test.each(cases)('parse wishlist line: %s', (wishlist, result) => { expect(toWishList([[undefined, wishlist]]).wishListRolls[0]).toStrictEqual(result); }); ================================================ FILE: src/app/wishlists/wishlist-file.ts ================================================ import { emptySet } from 'app/utils/empty'; import { timer, warnLog } from 'app/utils/log'; import { DimWishList, WishListAndInfo, WishListInfo, WishListRoll } from './types'; const TAG = 'wishlist'; /** * The title should follow the following format: * title:This Is My Source File Title. */ const titleLabel = /^@?title:(.+)$/; /** * The description should follow the following format: * description:This Is My Source File Description And Maybe It Is Longer. */ const descriptionLabel = /^@?description:(.+)$/; /** * Notes apply to all rolls until an empty line or comment. */ const notesLabel = '//notes:'; /** * Extracts rolls, title, and description from the meat of * one or more wish list text files, deduplicating within * and between lists. */ export function toWishList(files: [url: string | undefined, contents: string][]): WishListAndInfo { const stopTimer = timer(TAG, 'Parse wish list'); try { const wishList: WishListAndInfo = { wishListRolls: [], infos: [], }; const seen = new Set<string>(); for (const [url, fileText] of files) { const info: WishListInfo = { url, title: undefined, description: undefined, numRolls: 0, dupeRolls: 0, }; let blockNotes: string | undefined = undefined; let title: string | undefined = undefined; let description: string | undefined = undefined; let match: RegExpExecArray | null = null; const lines = fileText.split('\n'); for (const line of lines) { if (line.startsWith(notesLabel)) { blockNotes = parseBlockNoteLine(line); } else if (line.length === 0 || line.startsWith('//')) { // Empty lines and comments reset the block note blockNotes = undefined; } else if ((match = titleLabel.exec(line))) { title = match[1]; if (!info.title) { info.title = title.trim(); } } else if ((match = descriptionLabel.exec(line))) { description = match[1].trim(); if (!info.description) { info.description = description; } } else { const roll = toDimWishListRoll(line, blockNotes) || toBansheeWishListRoll(line, blockNotes) || toDtrWishListRoll(line, blockNotes); if (roll) { const rollHash = `${roll.itemHash};${roll.isExpertMode};${sortedSetToString( roll.recommendedPerks, )}`; if (!seen.has(rollHash)) { seen.add(rollHash); wishList.wishListRolls.push(roll); info.numRolls++; } else { info.dupeRolls++; } roll.sourceWishListIndex = wishList.infos.length; roll.title = title; roll.description = description; } } } if (info.dupeRolls > 0) { warnLog(TAG, 'Discarded', info.dupeRolls, 'duplicate rolls from wish list', url); } wishList.infos.push(info); } return wishList; } finally { stopTimer(); } } function expectedMatchResultsLength(matchResults: RegExpMatchArray): boolean { return matchResults.length === 4; } const blockNoteLineRegex = /^\/\/notes:(?<blockNotes>[^|]*)/; /** Parse out notes from a line */ function parseBlockNoteLine(blockNoteLine: string): string | undefined { const blockMatchResults = blockNoteLineRegex.exec(blockNoteLine); return blockMatchResults?.groups?.blockNotes; } function getPerks(matchResults: RegExpMatchArray): Set<number> { if (matchResults.groups?.itemPerks === undefined) { return emptySet<number>(); } const split = matchResults[2].split(','); const s = new Set<number>(); for (const perkHash of split) { const n = Number(perkHash); if (n > 0) { s.add(n); } } return s; } function getNotes(matchResults: RegExpMatchArray, blockNotes?: string): string | undefined { const notes = matchResults.groups?.wishListNotes && matchResults.groups.wishListNotes.length > 1 ? matchResults.groups.wishListNotes : blockNotes; return notes?.replace(/\\n/g, '\n'); } function getItemHash(matchResults: RegExpMatchArray): number { if (!matchResults.groups) { return 0; } return Number(matchResults.groups.itemHash); } const dtrTextLineRegex = /^https:\/\/destinytracker\.com\/destiny-2\/db\/items\/(?<itemHash>\d+)\D*perks=(?<itemPerks>[\d,]*)(?:#notes:)?(?<wishListNotes>[^|]*)/; function toDtrWishListRoll(dtrTextLine: string, blockNotes?: string): WishListRoll | null { const matchResults = dtrTextLineRegex.exec(dtrTextLine); if (!matchResults || !expectedMatchResultsLength(matchResults)) { return null; } const itemHash = getItemHash(matchResults); const recommendedPerks = getPerks(matchResults); const notes = getNotes(matchResults, blockNotes); return { itemHash, recommendedPerks, isExpertMode: false, notes, }; } const bansheeTextLineRegex = /^https:\/\/banshee-44\.com\/\?weapon=(?<itemHash>\d.+)&socketEntries=(?<itemPerks>[\d,]*)(?:#notes:)?(?<wishListNotes>[^|]*)/; /** Translate a single banshee-44.com URL -> WishListRoll. */ function toBansheeWishListRoll(bansheeTextLine: string, blockNotes?: string): WishListRoll | null { const matchResults = bansheeTextLineRegex.exec(bansheeTextLine); if (!matchResults || !expectedMatchResultsLength(matchResults)) { return null; } const itemHash = getItemHash(matchResults); const recommendedPerks = getPerks(matchResults); const notes = getNotes(matchResults, blockNotes); return { itemHash, recommendedPerks, isExpertMode: false, notes, }; } const textLineRegex = /^dimwishlist:item=(?<itemHash>-?\d+)(?:&perks=)?(?<itemPerks>[\d|,]*)(?:#notes:)?(?<wishListNotes>[^|]*)/; function toDimWishListRoll(textLine: string, blockNotes?: string): WishListRoll | null { const matchResults = textLineRegex.exec(textLine); if (!matchResults || !expectedMatchResultsLength(matchResults)) { return null; } let itemHash = getItemHash(matchResults); const isUndesirable = itemHash < 0 && itemHash !== DimWishList.WildcardItemId; const recommendedPerks = getPerks(matchResults); const notes = getNotes(matchResults, blockNotes); if (isUndesirable && itemHash !== DimWishList.WildcardItemId) { itemHash = Math.abs(itemHash); } return { itemHash, recommendedPerks, isExpertMode: true, notes, isUndesirable, }; } function sortedSetToString(set: Set<number>): string { return [...set].sort((a, b) => a - b).toString(); } ================================================ FILE: src/app/wishlists/wishlists.ts ================================================ import { normalizeToEnhanced, normalizeToUnenhanced } from 'app/utils/perk-utils'; import { BucketHashes, ItemCategoryHashes, PlugCategoryHashes } from 'data/d2/generated-enums'; import { DimItem, DimPlug } from '../inventory/item-types'; import { DimWishList, WishListRoll } from './types'; const targetPCHs = [ PlugCategoryHashes.Frames, PlugCategoryHashes.Bowstrings, PlugCategoryHashes.Batteries, PlugCategoryHashes.Blades, PlugCategoryHashes.Tubes, PlugCategoryHashes.Scopes, PlugCategoryHashes.Hafts, PlugCategoryHashes.Stocks, PlugCategoryHashes.Guards, PlugCategoryHashes.Barrels, PlugCategoryHashes.Arrows, PlugCategoryHashes.Grips, PlugCategoryHashes.Scopes, PlugCategoryHashes.Magazines, PlugCategoryHashes.MagazinesGl, PlugCategoryHashes.Rails, PlugCategoryHashes.Bolts, PlugCategoryHashes.Origins, ]; export const enum UiWishListRoll { Good = 1, Bad, } export function toUiWishListRoll( inventoryWishListRoll?: InventoryWishListRoll, ): UiWishListRoll | undefined { if (!inventoryWishListRoll) { return undefined; } return inventoryWishListRoll.isUndesirable ? UiWishListRoll.Bad : UiWishListRoll.Good; } /** * An inventory wish list roll - for an item instance ID, is the item known to be on the wish list? * If it is on the wish list, what perks are responsible for it being there? */ export interface InventoryWishListRoll { /** What perks did the curator pick for the item? */ wishListPerks: Set<number>; /** What notes (if any) did the curator make for this item + roll? */ notes: string | undefined; /** Is this an undesirable roll? */ isUndesirable?: boolean; } /** * Is this a weapon or armor plug that we'll consider? * This is in place so that we can disregard intrinsics, shaders/cosmetics * and other things (like masterworks) which add more variance than we need. */ function isWeaponOrArmorOrGhostMod(plug: DimPlug): boolean { if (targetPCHs.includes(plug.plugDef.plug.plugCategoryHash)) { return true; } if ( plug.plugDef.itemCategoryHashes?.find( (ich) => ich === ItemCategoryHashes.WeaponModsIntrinsic || ich === ItemCategoryHashes.WeaponModsGameplay || ich === ItemCategoryHashes.ArmorModsGameplay, ) ) { return false; } // if it's an instanced modification, ignore it if ( plug.plugDef.inventory!.bucketTypeHash === BucketHashes.Modifications && plug.plugDef.inventory!.isInstanceItem ) { return false; } return ( plug.plugDef.itemCategoryHashes?.some( (ich) => ich === ItemCategoryHashes.WeaponMods || ich === ItemCategoryHashes.ArmorMods || ich === ItemCategoryHashes.BonusMods || ich === ItemCategoryHashes.GhostModsPerks, ) ?? false ); // weapon, then armor, then bonus (found on armor perks), then ghost mod } /** Is the plug's hash included in the recommended perks from the wish list roll? */ export function isWishListPlug( plug: DimPlug, wishListRoll?: WishListRoll | InventoryWishListRoll, ): boolean { const perks = wishListRoll && ('recommendedPerks' in wishListRoll ? wishListRoll.recommendedPerks : wishListRoll.wishListPerks); return Boolean( perks && // Either the enhanced or unenhanced version of the perk is present (perks.has(normalizeToUnenhanced(plug.plugDef.hash)) || perks.has(normalizeToEnhanced(plug.plugDef.hash))), ); } /** Get all of the plugs for this item that match the wish list roll. */ function getWishListPlugs(item: DimItem, wishListRoll: WishListRoll): Set<number> { const wishListPlugs = new Set<number>(); if (!item.sockets) { return wishListPlugs; } for (const s of item.sockets.allSockets) { if (s.plugged) { for (const dp of s.plugOptions) { if (isWeaponOrArmorOrGhostMod(dp) && isWishListPlug(dp, wishListRoll)) { wishListPlugs.add(dp.plugDef.hash); } } } } return wishListPlugs; } /** * Do all desired perks from the wish list roll exist on this item? * Disregards cosmetics and some other socket types. */ function allDesiredPerksExist(item: DimItem, wishListRoll: WishListRoll): boolean { if (wishListRoll.isExpertMode) { for (const recommendedPerk of wishListRoll.recommendedPerks) { let included = false; // this function serves only getInventoryWishListRoll, // which has already ensured item.sockets exists outer: for (const s of item.sockets!.allSockets) { if (s.plugOptions) { for (const plug of s.plugOptions) { if ( // Either the enhanced or unenhanced version of the perk is present normalizeToUnenhanced(plug.plugDef.hash) === recommendedPerk || normalizeToEnhanced(plug.plugDef.hash) === recommendedPerk ) { included = true; break outer; } } } } if (!included) { return false; } } return true; } return item.sockets!.allSockets.every( (s) => !s.plugged || !isWeaponOrArmorOrGhostMod(s.plugged) || s.plugOptions.some((dp) => isWishListPlug(dp, wishListRoll)), ); } /** Get the InventoryWishListRoll for this item. */ export function getInventoryWishListRoll( item: DimItem, wishListRolls: Map<number, WishListRoll[]>, ): InventoryWishListRoll | undefined { // It could be under the item hash, the wildcard, or any of the item's categories for (const hash of [item.hash, DimWishList.WildcardItemId, ...item.itemCategoryHashes]) { const matchingWishListRoll = wishListRolls .get(hash) ?.find((cr) => allDesiredPerksExist(item, cr)); if (matchingWishListRoll) { return { wishListPerks: getWishListPlugs(item, matchingWishListRoll), notes: matchingWishListRoll.notes, isUndesirable: matchingWishListRoll.isUndesirable, }; } } } ================================================ FILE: src/authReturn.ts ================================================ import { HttpStatusError } from 'app/bungie-api/http-client'; import { errorMessage } from 'app/utils/errors'; import { errorLog } from 'app/utils/log'; import { getAccessTokenFromCode } from './app/bungie-api/oauth'; import { setToken } from './app/bungie-api/oauth-tokens'; import { reportException } from './app/utils/sentry'; async function handleAuthReturn() { const queryParams = new URL(window.location.href).searchParams; const code = queryParams.get('code'); const state = queryParams.get('state'); // Detect when we're in the iOS app's auth popup (but not in the app itself) const iOSApp = state?.startsWith('dimauth-') && !navigator.userAgent.includes('DIM AppStore'); if (iOSApp) { window.location.href = window.location.href.replace('https', 'dimauth'); return; } if (!code?.length) { setError("We expected an authorization code parameter from Bungie.net, but didn't get one."); return; } const authorizationState = localStorage.getItem('authorizationState'); if (state !== authorizationState) { let error = "We expected the state parameter to match what we stored, but it didn't."; if (!authorizationState || authorizationState.length === 0) { error += ' There was no stored state at all - your browser may not support (or may be blocking) localStorage.'; } reportException('authReturn', new Error(error)); setError(error); return; } try { const token = await getAccessTokenFromCode(code); setToken(token); // If we have a stored path from before we logged in (e.g. a loadout or armory link), send them back to that window.location.href = localStorage.getItem('returnPath') ?? $PUBLIC_PATH; } catch (error) { if (error instanceof TypeError || (error instanceof HttpStatusError && error.status === -1)) { setError( 'A content blocker is interfering with either DIM or Bungie.net, or you are not connected to the internet.', ); return; } errorLog('bungie auth', "Couldn't get access token", error); reportException('authReturn', error); setError(errorMessage(error)); } } function setError(error: string) { document.getElementById('error-message')!.textContent = error; document.getElementById('error-display')!.style.display = 'block'; document.getElementById('loading')!.style.display = 'none'; document.getElementById('user-agent')!.textContent = navigator.userAgent; } handleAuthReturn(); ================================================ FILE: src/backup.html ================================================ <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>DIM Data Emergency Backup

Export DIM Data

If DIM cannot load at all, this page may allow you to download a backup of your settings, tags, notes, etc.
It's strongly recommended you try visiting DIM's Settings page, to export your data there.

Exportable data was not found stored in the browser

================================================ FILE: src/backup.ts ================================================ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable no-console */ /* eslint-disable no-alert */ import type { DestinyVersion, ExportResponse } from '@destinyitemmanager/dim-api-types'; import type { DimApiState } from 'app/dim-api/reducer'; let exportData: ExportResponse; function parseProfileKey(profileKey: string): [string, DestinyVersion] { const match = profileKey.match(/(\d+)-d(1|2)/); if (!match) { throw new Error("Profile key didn't match expected format"); } return [match[1], parseInt(match[2], 10) as DestinyVersion]; } const connection = indexedDB.open('keyval-store'); connection.onerror = (event) => { console.log((event.target as any)?.result); alert((event.target as any)?.result); }; connection.onsuccess = (event) => { ((event.target as any).result as IDBDatabase) .transaction('keyval') .objectStore('keyval') .get('dim-api-profile').onsuccess = (event) => { const storedData = (event.target as any)?.result as DimApiState; if (storedData?.settings) { exportData = { settings: storedData.settings, loadouts: [], tags: [], triumphs: [], itemHashTags: [], searches: [], }; if (storedData.profiles) { for (const profileKey in storedData.profiles) { if (Object.prototype.hasOwnProperty.call(storedData.profiles, profileKey)) { const [platformMembershipId, destinyVersion] = parseProfileKey(profileKey); for (const loadout of Object.values(storedData.profiles[profileKey].loadouts)) { exportData.loadouts.push({ loadout, platformMembershipId, destinyVersion, }); } for (const annotation of Object.values(storedData.profiles[profileKey].tags)) { exportData.tags.push({ annotation, platformMembershipId, destinyVersion, }); } exportData.triumphs.push({ platformMembershipId, triumphs: storedData.profiles[profileKey].triumphs, }); } } } if (storedData.itemHashTags) { exportData.itemHashTags = Object.values(storedData.itemHashTags); } if (storedData.searches) { for (const destinyVersionStr in storedData.searches) { const destinyVersion = parseInt(destinyVersionStr, 10) as DestinyVersion; for (const search of storedData.searches[destinyVersion]) { exportData.searches.push({ destinyVersion, search, }); } } } const profileTimestamps = storedData.profiles ? (Object.values(storedData.profiles) .map((p) => p.profileLastLoaded || null) .filter(Boolean) as number[]) : null; const searchTimestamps = exportData.searches .map((s) => s?.search?.lastUsage || null) .filter(Boolean) as number[]; const loadoutTimestamps = exportData.loadouts .map((l) => l?.loadout?.lastUpdatedAt || null) .filter(Boolean) as number[]; document.getElementById('feedback')!.textContent = `Stored DIM data was found in the browser's storage. It looks like it was last downloaded from DIM Sync ${stringifyLatestDate(profileTimestamps)}. It appears to contain: - ${Object.keys(exportData.settings ?? {}).length} settings - ${exportData.loadouts.length} loadouts, last updated ${stringifyLatestDate(loadoutTimestamps)} - ${exportData.tags.length} specific item tags/notes - ${exportData.triumphs.length} tracked triumphs - ${exportData.itemHashTags.length} item type tags/notes - ${exportData.searches.length} searches, last used ${stringifyLatestDate(searchTimestamps)}`; } }; }; function stringifyLatestDate(datestamps: number[] | null) { return datestamps?.length ? new Date(Math.max(...datestamps)).toLocaleString() : '[unknown date]'; } function downloadBackup() { const stringified = encodeURIComponent(JSON.stringify(exportData)); const a = document.createElement('a'); a.setAttribute('href', `data:application/json;charset=utf-8,${stringified}`); a.setAttribute('download', 'dim-data-emergency-backup.json'); document.body.appendChild(a); a.click(); } document.getElementById('download')?.addEventListener('click', downloadBackup); ================================================ FILE: src/browsercheck-utils.js ================================================ export const supportedLanguages = [ 'en', 'de', 'es', 'es-mx', 'fr', 'it', 'ja', 'ko', 'pl', 'pt-br', 'ru', 'zh-chs', 'zh-cht', ]; export const unsupported = { en: 'The DIM team does not support using this browser. Some or all DIM features may not work.', de: 'Das DIM-Team unterstützt diesen Browser nicht. Einige oder alle DIM-Funktionen funktionieren möglicherweise nicht.', es: 'El equipo de DIM no soporta usar este navegador. Algunas o todas las características de DIM podrían no funcionar.', 'es-mx': 'El equipo de DIM no soporta usar este navegador. Algunas o todas las características de DIM podrían no funcionar.', fr: "L'équipe DIM ne prend pas en charge ce navigateur. Certaines ou toutes les fonctionnalités de DIM peuvent ne pas fonctionner.", it: "Il team di DIM non supporta l'utilizzo di questo browser. Alcune o tutte le funzioni di DIM potrebbero non funzionare.", ja: 'DIM チームはこのブラウザの利用をサポートしていません。DIM の一部または全ての機能が動作しない可能性があります。', ko: 'DIM 팀은 이 브라우저를 지원하지 않습니다. DIM의 일부나 전체 기능이 동작하지 않을 수 있습니다.', pl: 'Zespół DIM nie obsługuje tej przeglądarki. Niektóre lub wszystkie funkcje DIM mogą nie działać.', 'pt-br': 'A equipe do DIM não oferece suporte a este navegador. Alguns recursos do DIM podem não funcionar.', ru: 'Команда DIM не поддерживает использование этого браузера. Некоторые или все функции DIM могут не работать.', 'zh-chs': 'DIM 团队未对当前浏览器作出支持。DIM 可能部分或完全不可用。', 'zh-cht': 'DIM團隊不支持使用此瀏覽器。 DIM可能部分或完全不可用。', }; export const steamBrowser = { en: 'The Steam overlay browser is very old and some or all DIM features may not work. We cannot provide support for it.', de: 'Der Steam-Overlay-Browser ist sehr alt und einige oder alle DIM-Funktionen funktionieren möglicherweise nicht. Wir können dies nicht unterstützen.', es: 'El navegador de la interfaz de Steam es muy antiguo y podría hacer que algunas o todas las características de DIM no funcionen. No podemos proveer de soporte para ello.', 'es-mx': 'El navegador de Steam es muy antiguo y podría hacer que algunas o todas las características de DIM no funcionen. No podemos proveer de soporte para ello.', fr: "Le navigateur de l'overlay Steam est très vieux et certaines des fonctionnalitées de DIM peuvent ne pas fonctionner. Nous ne pouvons pas fournir de support pour ce navigateur.", it: "Il browser dell'overlay di Steam è molto vecchio e alcune o tutte le funzionalità di DIM potrebbero non funzionare. Non possiamo fornire supporto.", ja: 'Steam オーバーレイ ブラウザは非常に古いため、DIMの一部の機能もしくは全て動作しない可能性があることからサポート対象外とします。', ko: '스팀 오버레이의 브라우저는 매우 오래되어 DIM의 일부 혹은 모든 기능이 동작하지 않을 수 있습니다. 이에 대한 지원은 제공되지 않습니다.', pl: 'Przeglądarka nakładki Steam jest bardzo stara i część funkcji lub wszystkie funkcje DIM mogą nie działać. Nie możemy zapewnić wsparcia w tym zakresie.', 'pt-br': 'O navegador do Painel Steam é muito antigo e alguns recursos do DIM podem não funcionar. Não podemos oferecer suporte para isso.', ru: 'Браузер оверлея Steam очень старый, и некоторые или все функции DIM могут не работать. Мы не можем предоставить для него поддержку.', 'zh-chs': 'Steam 游戏内叠加浏览器版本较老,DIM 的部分或全部功能可能无法正常工作,且我们无法为其提供支持。', 'zh-cht': 'Steam覆蓋層的瀏覽器已經非常老了,可能無法使用一些DIM的功能,我們無法提供相關支援。', }; export const samsungInternet = { en: 'Samsung Internet can make sites look too dark when dark mode is on. Enable Settings > Labs > Use website dark theme or switch to another browser.', de: 'Samsung Internet kann Websites zu dunkel erscheinen lassen, wenn der dunkle Modus eingeschaltet ist. Einstellungen > Labs > Webseite dunkles Theme verwenden oder wechsle zu einem anderen Browser.', es: 'Internet de Samsung puede hacer que los sitios parezcan demasiado oscuros cuando el modo oscuro está activo. Habilita Opciones > Laboratorios > Usar tema oscuro o cambia a otro navegador.', 'es-mx': 'El Internet de Samsung puede hacer que los sitios parezcan demasiado oscuros cuando el modo oscuro está activo. Activa Opciones > Laboratorios > Usar tema oscuro o cambia a otro navegador.', fr: 'Samsung Internet peut rendre les sites trop sombre quand le mode sombre est activé. Activez Paramètres > Labs > Utiliser le mode sombre du site web ou utilisez un autre navigateur.', it: 'Samsung Internet può rendere i siti troppo scuri quando la modalità scura è attiva. Abilita in Impostazioni > Labs > Utilizza tema scuro oppure passa a un altro browser.', ja: 'Samsungブラウザでは、ダーク モードがオンになっているとサイトが暗くなりすぎることがあります。ブラウザから 設定 > ラボ > Webサイトのダークテーマを使用 を有効にするか、別のブラウザに切り替えてください。', ko: '삼성 인터넷은 다크 모드가 활성화된 경우 사이트를 너무 어둡게 만들 수 있습니다. 설정 > 실험실 > 웹사이트의 다크 테마를 사용하거나 다른 브라우저를 사용하세요.', pl: 'Aplikacja Samsung Internet może spowodować, że strony będą wyglądały zbyt ciemno, gdy włączony jest tryb ciemny. Wejdź w i włącz Ustawienia > Labs > Użyj ciemnego motywu witr. WWW lub przełącz się na inną przeglądarkę.', 'pt-br': 'O Samsung Internet pode fazer com que alguns sites pareçam muito escuros quando o modo escuro está ativado. Ative Configurações > Labs > Usar tema escuro do site da web ou mude para outro navegador.', ru: 'В Samsung Internet сайты могут выглядеть слишком тёмными, когда включен тёмный режим. Включите "Настройки" > "Labs" > "Использовать темную тему сайта" или переключитесь на другой браузер.', 'zh-chs': '启用深色模式时,三星浏览器可能会导致网页显示过于黑暗。在设置 > 通用 > 使用网页深色主题或使用其他浏览器来解决此问题。', 'zh-cht': 'Samsung Internet can make sites look too dark when dark mode is on. Enable Settings > Labs > Use website dark theme or switch to another browser.', }; ================================================ FILE: src/browsercheck.js ================================================ /* eslint prefer-template: 0 */ import parser from 'ua-parser-js'; import { samsungInternet, steamBrowser, supportedLanguages, unsupported, } from './browsercheck-utils.js'; // Adapted from 'is-browser-supported' npm package. Separate from index.js so it'll run even if that fails. // This is also intentionally written in es5 and not TypeScript because it should not use any new features. /** * @param {parser.IResult} agent */ function getBrowserName(agent) { if (agent.browser.name === 'Chrome' && agent.os.name === 'Android') { return 'and_chr'; } else if (agent.browser.name === 'Firefox' && agent.os.name === 'Android') { return 'and_ff'; } else if (agent.browser.name === 'Mobile Safari' || agent.os.name === 'iOS') { return 'ios_saf'; } else if (agent.browser.name === 'Chromium') { return 'chrome'; } else if (agent.browser.name === 'Opera') { return 'opera'; } return agent.browser.name; } /** * @returns {import('app/i18n.js').DimLanguage} */ function getUserLocale() { var lang = window.navigator.language.toLowerCase() || 'en'; if (lang.startsWith('zh-') && lang.length === 5) { lang = lang === 'zh-cn' ? 'zh-chs' : 'zh-cht'; } if (!supportedLanguages.includes(lang)) { lang = lang.split('-', 1)[0]; } if (!supportedLanguages.includes(lang)) { // fallback to 'en' if unsupported language after removing dialect lang = 'en'; } return lang; } /** * @param {parser.IResult} agent */ function getBrowserVersionFromUserAgent(agent) { var browserName = getBrowserName(agent).toLowerCase(); var version = ( browserName === 'ios_saf' ? agent.os.version : agent.browser.version || agent.os.version || '' ).split('.'); while (version.length > 0) { try { return browserName + ' ' + version.join('.'); } catch { // Ignore unknown browser query error } version.pop(); } return 'unknown'; } /** * @param {string[]} browsersSupported * @param {string} userAgent */ export function isSupported(browsersSupported, userAgent) { if (navigator.standalone) { // Assume support if we're installed as an iOS PWA. return true; } var agent = parser(userAgent); // Build a map from browser version to minimum supported version /** @type {Record} */ var minBrowserVersions = {}; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (var i = 0; i < browsersSupported.length; i++) { // ios_saf 11.0-11.2 => [ios_saf, 11.0, 11.2] var supportedBrowserVersion = browsersSupported[i].split(/[- ]/); minBrowserVersions[supportedBrowserVersion[0]] = Math.min( minBrowserVersions[supportedBrowserVersion[0]] || 999999, parseFloat(supportedBrowserVersion[1]), ); } /** @param {string} browser */ function isBrowserSupported(browser) { var nameAndVersion = browser.split(' '); return ( minBrowserVersions[nameAndVersion[0]] && minBrowserVersions[nameAndVersion[0]] <= parseFloat(nameAndVersion[1]) ); } var browser = getBrowserVersionFromUserAgent(agent); var supported = isBrowserSupported(browser); if (!supported) { // Detect anything based on chrome as if it were chrome var chromeMatch = /Chrome\/(\d+)/.exec(agent.ua); if (chromeMatch) { browser = 'chrome ' + chromeMatch[1]; supported = isBrowserSupported(browser); } } if (!supported) { // eslint-disable-next-line no-console console.warn( 'Browser ' + browser + ' is not supported by DIM. Supported browsers:', browsersSupported, ); } return supported; } var lang = getUserLocale(); if ($BROWSERS.length && lang) { var supported = isSupported($BROWSERS, navigator.userAgent); if (!supported) { // t(`Browsercheck.Unsupported`) document.getElementById('browser-warning').textContent = unsupported[lang]; document.getElementById('browser-warning').style.display = 'block'; } // Steam is never supported if (navigator.userAgent.includes('Steam')) { // https://guide.dim.gg/Figuring-out-why-DIM-doesn't-work-in-Steam // t(`Browsercheck.Steam`) document.getElementById('browser-warning').textContent = steamBrowser[lang]; document.getElementById('browser-warning').style.display = 'block'; } // Samsung Internet is not supported because of its weird forced dark mode if ( navigator.userAgent.includes('SamsungBrowser') && // When the "Labs" setting to respect websites' dark mode capabilities is // enabled, Samsung Internet will actually set prefers-color-scheme to dark. // Otherwise, it's always "light". This *could* be a user who actually // prefers a light theme - there's no way to tell. window.matchMedia('(prefers-color-scheme: light)').matches ) { // t(`Browsercheck.Samsung`) document.getElementById('browser-warning').textContent = samsungInternet[lang]; document.getElementById('browser-warning').style.display = 'block'; } } ================================================ FILE: src/browsercheck.test.ts ================================================ import { isSupported } from './browsercheck'; // A snapshot of our support list so these tests will always work const browsersSupported = [ 'and_chr 78', 'and_ff 68', 'chrome 78', 'chrome 77', 'edge 18', 'edge 17', 'firefox 71', 'firefox 70', 'firefox 68', 'ios_saf 13.2', 'ios_saf 13.0-13.1', 'ios_saf 12.2-12.4', 'ios_saf 12.0-12.1', 'ios_saf 11.3-11.4', 'ios_saf 11.0-11.2', 'opera 64', 'opera 63', 'safari 13', 'safari 12.1', ]; test.each([ [ 'Firefox 72', 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0', true, ], [ 'Chrome 79', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36', true, ], [ 'iOS 12', 'Mozilla/5.0 (iPod; CPU iPhone OS 12_0 like macOS) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/12.0 Mobile/14A5335d Safari/602.1.50', true, ], [ 'Edge 18', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362', true, ], [ 'Vivaldi', 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36 Vivaldi/2.10', true, ], [ 'Opera', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36 OPR/65.0.3467.78', true, ], [ 'Old Chrome', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36', false, ], // This isn't actually checked in isSupported anymore, but it's nice to have the example user agent here. It's up to Chrome 85 now though... // [ // 'Steam Overlay', // 'Mozilla/5.0 (Windows; U; Windows NT 10.0; en-US; Valve Steam GameOverlay/1608507519; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', // false, // ], ])('%s: User agent %s, supported: %s', (_name, userAgent, shouldBeSupported) => { expect(isSupported(browsersSupported, userAgent)).toStrictEqual(shouldBeSupported); }); ================================================ FILE: src/browserconfig.xml ================================================ #434444 ================================================ FILE: src/build-browsercheck-utils.js ================================================ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import fs from 'node:fs'; import de from './locale/de.json' with { type: 'json' }; import en from './locale/en.json' with { type: 'json' }; import es from './locale/es.json' with { type: 'json' }; import esMX from './locale/esMX.json' with { type: 'json' }; import fr from './locale/fr.json' with { type: 'json' }; import it from './locale/it.json' with { type: 'json' }; import ja from './locale/ja.json' with { type: 'json' }; import ko from './locale/ko.json' with { type: 'json' }; import pl from './locale/pl.json' with { type: 'json' }; import ptBR from './locale/ptBR.json' with { type: 'json' }; import ru from './locale/ru.json' with { type: 'json' }; import zhCHS from './locale/zhCHS.json' with { type: 'json' }; import zhCHT from './locale/zhCHT.json' with { type: 'json' }; /** * @param {string} key */ function getI18nKey(key) { let key1 = key.split('.')[0]; let key2 = key.split('.')[1]; let getStringFor = (lang) => { const str = lang[key1]?.[key2] ?? en[key1][key2]; return JSON.stringify(str); }; return ` en: ${getStringFor(en)}, de: ${getStringFor(de)}, es: ${getStringFor(es)}, 'es-mx': ${getStringFor(esMX)}, fr: ${getStringFor(fr)}, it: ${getStringFor(it)}, ja: ${getStringFor(ja)}, ko: ${getStringFor(ko)}, pl: ${getStringFor(pl)}, 'pt-br': ${getStringFor(ptBR)}, ru: ${getStringFor(ru)}, 'zh-chs': ${getStringFor(zhCHS)}, 'zh-cht': ${getStringFor(zhCHT)},\n};`; } var browserCheckUtils = `export const supportedLanguages = [ 'en', 'de', 'es', 'es-mx', 'fr', 'it', 'ja', 'ko', 'pl', 'pt-br', 'ru', 'zh-chs', 'zh-cht', ]; export const unsupported = { ${getI18nKey('Browsercheck.Unsupported')} export const steamBrowser = { ${getI18nKey('Browsercheck.Steam')} export const samsungInternet = { ${getI18nKey('Browsercheck.Samsung')}`; fs.writeFileSync('src/browsercheck-utils.js', browserCheckUtils); ================================================ FILE: src/bungie-api-ts.d.ts ================================================ import 'bungie-api-ts/destiny2'; // Extensions/customizations to the generated Bungie.net API types declare module 'bungie-api-ts/destiny2' { const enum DestinyClass { /* * The class cannot be known because the item is classified. * DestinyClass.Unknown really means "not specific to a class", so we invent this * value to represent classified items. */ Classified = -1, } } ================================================ FILE: src/data/d1/manifests/d1-manifest-de.json ================================================ [File too large to display: 47.7 MB] ================================================ FILE: src/data/d1/manifests/d1-manifest-en.json ================================================ [File too large to display: 47.2 MB] ================================================ FILE: src/data/d1/manifests/d1-manifest-es.json ================================================ [File too large to display: 47.7 MB] ================================================ FILE: src/data/d1/manifests/d1-manifest-fr.json ================================================ [File too large to display: 48.1 MB] ================================================ FILE: src/data/d1/manifests/d1-manifest-it.json ================================================ [File too large to display: 47.6 MB] ================================================ FILE: src/data/d1/manifests/d1-manifest-ja.json ================================================ [File too large to display: 48.5 MB] ================================================ FILE: src/data/d1/manifests/d1-manifest-pt-br.json ================================================ [File too large to display: 47.7 MB] ================================================ FILE: src/data/d1/missing_sources.json ================================================ { "488703545": [1374970038], "4063984059": [1374970038], "1954673444": [1374970038], "4274025725": [1374970038], "4203554251": [1374970038], "3653048978": [1374970038], "3758132323": [1374970038], "3566605998": [1374970038], "2360689228": [1374970038], "4274025724": [1374970038], "4203554250": [1374970038], "3653048989": [1374970038], "488703544": [1374970038], "4063984058": [1374970038], "1954673451": [1374970038], "3758132332": [1374970038], "3566605999": [1374970038], "2360689229": [1374970038], "1954673450": [1374970038], "3653048988": [1374970038], "3758132333": [1374970038], "3776902104": [1374970038], "3776902105": [1374970038], "3776902106": [1374970038], "3441383075": [1374970038], "3441383074": [1374970038], "3441383073": [1374970038], "48423572": [2650556703], "48423573": [2650556703], "3490124916": [2650556703], "3490124917": [2650556703], "341708370": [2650556703], "341708371": [2650556703], "2748310062": [2650556703], "2748310063": [2650556703], "1768925824": [2650556703], "149649367": [2650556703], "1768925825": [2650556703], "2472321204": [2650556703], "1550781863": [2650556703], "46721743": [2650556703], "3195303140": [2650556703], "1550781862": [2650556703], "1283021732": [2650556703], "1283021733": [2650556703], "120524975": [2650556703], "2846692979": [2650556703], "2106863376": [2650556703], "120524974": [2650556703], "1173766591": [2650556703], "1173766590": [2650556703], "2959003546": [2650556703], "3342415802": [2650556703], "10673376": [2650556703], "728377844": [2650556703], "1705510184": [2650556703], "1811367950": [2650556703], "616180714": [2650556703], "3366907656": [2650556703], "3366907657": [2650556703], "320170738": [2650556703], "320170739": [2650556703], "1398798172": [2650556703], "1398798173": [2650556703], "1232070660": [2650556703], "2904362064": [2650556703], "2968802931": [2650556703], "2469233045": [2650556703], "1604125378": [2650556703], "1305525274": [2650556703], "1505957929": [2650556703], "2321310309": [2650556703], "1149751883": [2650556703], "811367950": [2650556703], "2129462487": [2650556703], "2602145129": [2650556703], "3407607557": [2650556703], "1278603962": [2650556703], "4184289357": [2650556703], "3515978627": [2650556703], "1566345525": [2650556703], "2339777344": [2650556703], "1811367951": [2650556703], "1705510185": [2650556703], "1566345524": [2650556703], "728377845": [2650556703], "10673377": [2650556703], "4184289356": [2650556703], "3515978626": [2650556703], "3342415803": [2650556703], "2959003547": [2650556703], "664737061": [2650556703], "952225322": [2650556703], "1429062181": [2650556703], "4111350117": [2650556703], "4111350118": [2650556703], "3458902100": [2650556703], "486639600": [1234918199], "4228322808": [1234918199], "3909166969": [1234918199], "3909166968": [1234918199], "3325871682": [1234918199], "3137749768": [1234918199], "3043255431": [1234918199], "281163670": [1234918199], "2292033576": [1234918199], "1944434611": [1234918199], "1944434610": [1234918199], "1427602698": [1234918199], "1227758151": [1234918199], "1184900753": [1234918199], "1184900752": [1234918199], "4125370907": [1234918199], "4125370906": [1234918199], "2476051997": [1234918199], "711737843": [1234918199], "711737842": [1234918199], "347985610": [1234918199], "2364921277": [1234918199], "2364921276": [1234918199], "1576112564": [1234918199], "669643075": [1234918199], "4223010274": [1234918199], "294399990": [1234918199], "2828837278": [1234918199], "2555395778": [1234918199], "2379135058": [1234918199], "2170625070": [1234918199], "2150667283": [1234918199], "2072689472": [1234918199], "1706217754": [1234918199], "1266297712": [1234918199], "1001251654": [1234918199], "4223010277": [1234918199], "3764987313": [1234918199], "2828837273": [1234918199], "2170625065": [1234918199], "2150667284": [1234918199], "1266297719": [1234918199], "1001251649": [1234918199], "947033339": [1682133942], "869771221": [1682133942], "849545090": [1682133942], "839919863": [1682133942], "785760358": [1682133942], "738109042": [1682133942], "687119879": [1682133942], "594033570": [1682133942], "599590573": [1682133942], "582029658": [1682133942], "488162542": [1682133942], "457764795": [1682133942], "4228079567": [1682133942], "4204877210": [1682133942], "3999537132": [1682133942], "3996986648": [1682133942], "3923782257": [1682133942], "3920950905": [1682133942], "3852109822": [1682133942], "3598716818": [1682133942], "3501164583": [1682133942], "349409405": [1682133942], "3356538611": [1682133942], "3356336516": [1682133942], "3300523568": [1682133942], "3300361771": [1682133942], "3217318818": [1682133942], "3153834917": [1682133942], "3106741559": [1682133942], "3098456198": [1682133942], "3092730773": [1682133942], "2935267644": [1682133942], "2777724579": [1682133942], "2604951151": [1682133942], "2599638040": [1682133942], "2564278177": [1682133942], "255184294": [1682133942], "2540333768": [1682133942], "2534979482": [1682133942], "2432311021": [1682133942], "2226144345": [1682133942], "2194008686": [1682133942], "2180255678": [1682133942], "2136148892": [1682133942], "2112416087": [1682133942], "2088244310": [1682133942], "1812756619": [1682133942], "1737853468": [1682133942], "165822953": [1682133942], "1632312788": [1682133942], "1629229483": [1682133942], "1483483181": [1682133942], "1482417412": [1682133942], "1422254016": [1682133942], "1252256284": [1682133942], "1230611383": [1682133942], "1139076902": [1682133942], "1103854799": [1682133942], "1012256480": [1682133942], "1623420384": [2964550958], "1170904292": [2964550958], "2604291457": [2964550958, 3131490494], "2604291456": [2964550958, 3131490494], "2575095884": [2964550958], "2575095885": [2964550958], "2575095886": [2964550958], "2575095887": [2964550958], "1826822442": [2964550958], "2634463554": [2964550958], "2672107536": [2964550958], "2672107537": [2964550958], "2672107538": [2964550958], "2672107539": [2964550958], "2672107540": [2964550958], "2672107541": [2964550958], "2672107542": [2964550958], "2672107551": [2964550958], "2833532021": [3131490494], "2970247930": [3131490494], "446517955": [3131490494], "1943318157": [3131490494], "2752699458": [3131490494], "498794675": [3131490494], "501878012": [3131490494], "773948710": [3131490494], "1516397455": [3131490494], "3241409756": [3131490494], "3677095941": [3131490494], "1270491518": [3131490494], "1475011170": [3131490494], "2170724558": [3131490494], "240078628": [3131490494], "1285649692": [3131490494], "1564235349": [3131490494], "2996043921": [3131490494], "2515984674": [3131490494], "2564277895": [3131490494], "3018515720": [3131490494], "510876942": [3131490494], "3597491335": [3131490494], "3664122880": [3131490494], "1061345955": [3131490494], "1230196177": [3131490494], "483952790": [3131490494], "4083935130": [3131490494], "4083935131": [3131490494], "4083935128": [3131490494], "4083935129": [3131490494], "4083935134": [3131490494], "4083935135": [3131490494], "4083935132": [3131490494], "4083935133": [3131490494], "4083935123": [3131490494], "3453851953": [3131490494], "1350048973": [3131490494], "1350048972": [3131490494], "1649308085": [3131490494], "1649308084": [3131490494], "2496068859": [3131490494], "2496068858": [3131490494], "3846961187": [3131490494], "3846961186": [3131490494], "3664120108": [3131490494], "3664120109": [3131490494], "3705287266": [3131490494], "3705287267": [3131490494], "3705287264": [3131490494], "3705287265": [3131490494], "894761026": [3131490494], "894761027": [3131490494], "894761024": [3131490494], "3143956507": [3131490494], "3143956504": [3131490494], "3143956505": [3131490494], "3143956510": [3131490494], "3143956511": [3131490494], "3143956508": [3131490494], "3143956509": [3131490494], "3143956498": [3131490494], "3143956499": [3131490494], "452104881": [3131490494], "452104880": [3131490494], "2810867465": [2585003248, 440710167, 1662673928, 4160622434], "3974974481": [2585003248, 440710167, 1662673928, 4160622434, 3068521220], "1846613521": [2585003248], "2038632607": [2585003248], "2038632606": [2585003248], "2038632601": [2585003248], "656520733": [2585003248], "656520731": [2585003248], "656520730": [2585003248], "418165612": [2585003248], "418165611": [2585003248], "418165610": [2585003248], "2150006565": [2585003248], "2239590644": [2585003248], "1723945892": [2585003248], "2143732224": [2585003248], "1126257023": [2585003248], "169290560": [2585003248], "297997397": [2585003248], "1771861810": [2585003248], "136219292": [2585003248], "3654591822": [2585003248], "23451592": [2585003248], "1221552698": [2585003248], "925424697": [2585003248], "47507137": [2585003248], "3288685443": [2585003248], "4025684900": [2585003248], "504330683": [2585003248], "1090313784": [2585003248], "3227022822": [2585003248], "3048102613": [2585003248], "440236681": [2585003248], "3971460238": [2585003248], "278511964": [2585003248], "3912672489": [2585003248], "3563445476": [2585003248], "3139712529": [2585003248], "4186638508": [2585003248], "3284317805": [2585003248], "2727182629": [2585003248], "2727182628": [2585003248], "1050291911": [2585003248], "124508677": [440710167], "905185492": [440710167], "1696520940": [440710167], "1151347422": [440710167], "100312097": [440710167], "2696808257": [440710167], "3534727768": [440710167], "72887664": [440710167], "1569892242": [440710167], "3253433517": [440710167], "1913300516": [440710167], "1477614333": [440710167], "4208473987": [440710167], "3801766431": [440710167], "2399369391": [440710167], "3107297018": [440710167], "1456315384": [440710167], "1370460719": [440710167], "306958364": [440710167], "3008445140": [440710167], "1444401177": [440710167], "3895707031": [440710167], "167916720": [440710167], "1458926430": [440710167], "4113238754": [440710167], "2038632600": [440710167], "2038632602": [440710167], "2038632603": [440710167], "656520735": [440710167], "656520732": [440710167], "656520734": [440710167], "418165613": [440710167], "418165614": [440710167], "418165615": [440710167], "3912672488": [440710167], "2512322824": [440710167], "1288233773": [440710167], "3307602312": [440710167], "1620583065": [440710167], "17215187": [440710167], "17215186": [440710167], "1050291910": [440710167], "191736999": [1662673928], "3500664182": [1662673928], "4219383232": [1662673928], "1014231576": [1662673928], "706920989": [1662673928], "1973841835": [1662673928], "1979402994": [1662673928], "299972636": [1662673928], "1714356932": [1662673928], "1761960529": [1662673928], "4027080268": [1662673928], "384778813": [1662673928], "3870751653": [1662673928], "4065554759": [1662673928], "319614456": [1662673928], "2744847861": [1662673928], "856822016": [1662673928], "3242690679": [1662673928], "974535320": [1662673928], "1565163446": [1662673928], "457366421": [1662673928], "1631737897": [1662673928], "189063458": [1662673928], "3752225442": [1662673928], "2038632604": [1662673928], "2038632594": [1662673928], "2038632605": [1662673928], "656520726": [1662673928], "656520729": [1662673928], "656520728": [1662673928], "418165608": [1662673928], "418165609": [1662673928], "418165607": [1662673928], "2227954477": [1662673928], "1344441638": [1662673928], "690024325": [1662673928], "162993472": [1662673928], "547597837": [1662673928], "1050291904": [1662673928], "1050291905": [4160622434], "46152726": [4160622434], "1665465325": [4160622434], "4274184111": [4160622434], "2706628142": [4160622434], "1772680569": [3068521220], "1128836046": [3068521220], "1075289601": [3068521220], "987198024": [3068521220], "639304757": [3068521220], "446254464": [3068521220], "351563194": [3068521220], "3819042234": [3068521220], "3037726217": [3068521220], "2588937859": [3068521220], "2154039487": [3068521220], "1190560887": [3068521220], "431256617": [3068521220], "3536376556": [3068521220], "1298850675": [3068521220], "1573495022": [3068521220], "1554221763": [3068521220], "2185888362": [3068521220], "2185888363": [3068521220], "2191224114": [3068521220], "2191224115": [3068521220], "1050291908": [3068521220], "1050291909": [3068521220], "1536084164": [3068521220], "265504045": [3068521220], "653965742": [3068521220], "1856274185": [3068521220], "3979504232": [3068521220], "2180254632": [353834582] } ================================================ FILE: src/data/d2/README.md ================================================ greetings, DIM contributor ♥ if you are in the DIM repository, files in this directory are cleared out then regenerated by the d2ai build process don't change or add anything here manually! ================================================ FILE: src/data/d2/artifact-breaker-weapon-types.json ================================================ { "485622768": [ 6, 10 ], "2611060930": [ 7, 3954685534 ], "3178805705": [ 3317538576, 54 ] } ================================================ FILE: src/data/d2/bad-vendors.json ================================================ [] ================================================ FILE: src/data/d2/bright-engram-bonus.json ================================================ [ 2152404392 ] ================================================ FILE: src/data/d2/bright-engrams.json ================================================ { "1": 3215463713, "2": 1276327882, "3": 3565864927, "4": 2378129814, "5": 3101758893, "6": 2801591300, "7": 476782113, "8": 591441816, "9": 827183327, "10": 1679203998, "11": 3767535285, "12": 1968811824, "13": 1968811824, "14": 1968811824, "15": 1968811824, "16": 1968811824, "17": 1968811824, "18": 1968811824, "19": 1968811824, "20": 1968811824, "21": 1968811824, "22": 1968811824, "23": 1968811824, "24": 1968811824, "25": 1968811824, "26": 1968811824, "27": 1968811824, "28": 1968811824 } ================================================ FILE: src/data/d2/catalyst-triumph-icons.json ================================================ { "15917031": "/common/destiny2_content/icons/a1fddbd287f5c9259914e4082417726e.jpg", "173502661": "/common/destiny2_content/icons/b760b737519af909e26f21009d6a1487.jpg", "185032465": "/common/destiny2_content/icons/89900d1b6646f1caf9498c9f99ad5eb6.jpg", "206322164": "/common/destiny2_content/icons/1bd8b5b8a2287f37ba549f09f46bd905.jpg", "206322165": "/common/destiny2_content/icons/3b4c1694f52b85f9fc1c57cb963dc36c.jpg", "207103968": "/common/destiny2_content/icons/eed762e88c5e164784ebf8b3c25ff1ec.jpg", "209320411": "/common/destiny2_content/icons/7dcb41a8e98004a1af64df680784b24d.jpg", "250211794": "/common/destiny2_content/icons/10eb188aae222806131864097a012b57.jpg", "252263460": "/common/destiny2_content/icons/5d835716e5298a974f93098bfc66ab1d.jpg", "263158944": "/common/destiny2_content/icons/efbc967e9cb7074cbad50cfee1998207.jpg", "299659704": "/common/destiny2_content/icons/b22e5ede50882e552212533397f2443b.jpg", "310266584": "/common/destiny2_content/icons/4aed2e5da8425b24df7a4db16e035c54.jpg", "373671280": "/common/destiny2_content/icons/92577b2999d98670c8a6a98a9646eff8.jpg", "436556889": "/common/destiny2_content/icons/b2b684b8ef316b5b16df9a2a0b3b7c66.jpg", "478443982": "/common/destiny2_content/icons/3b6a501e7a3d40580e8d9f86e701aef1.jpg", "494981303": "/common/destiny2_content/icons/78f8cdd72e124dc1faaf7801085e4d5b.png", "503301272": "/common/destiny2_content/icons/0d23330a42b682cdec7ec72c1d024900.jpg", "507778024": "/common/destiny2_content/icons/750539326d20c65f2c61ded7b552b07b.jpg", "571025162": "/common/destiny2_content/icons/5026805f6270ef152db2234caf242a09.jpg", "591600693": "/common/destiny2_content/icons/c013e41cdb32779bc2322337614ea06b.jpg", "639604165": "/common/destiny2_content/icons/d399beabd830286ba09e5bfba32f63db.jpg", "663317912": "/common/destiny2_content/icons/0e4aaccfad36ae9179375de5477bfd13.jpg", "748675128": "/common/destiny2_content/icons/379a2872be1dc01faf4cae20508fe8bf.jpg", "773758208": "/common/destiny2_content/icons/ce71024d7d432d5b0d912eca78a863d0.jpg", "788492661": "/common/destiny2_content/icons/f81462c7d5aac3dfa37c0757de8e0bac.jpg", "812366033": "/common/destiny2_content/icons/e2a8400513cffd7275b205ba198310f7.jpg", "828471459": "/common/destiny2_content/icons/8c97cfcc1a6837141d63f267de15180e.jpg", "836516980": "/common/destiny2_content/icons/8e54c8921ee2947dc378088725197f22.jpg", "1048178161": "/common/destiny2_content/icons/973a1e50742d98492c142ffa205df246.jpg", "1060652297": "/common/destiny2_content/icons/0fe10511fa234ae3432f51d608a6abd2.jpg", "1068904235": "/common/destiny2_content/icons/e9d0d921a8428bfbf0e0b4951c59569e.jpg", "1071947311": "/common/destiny2_content/icons/2164d8411d065abce0442fe597b06a21.jpg", "1094735064": "/common/destiny2_content/icons/44236d5ef42fc6f0789dc5cfe3d5eb2a.jpg", "1107018752": "/common/destiny2_content/icons/d011f6e099dc412ca11a7c8c12c08a3c.jpg", "1107121513": "/common/destiny2_content/icons/690b61c9ddb068f4e2641e0ceca16fdb.jpg", "1144463862": "/common/destiny2_content/icons/81f875075fd0a90202a0acb885d93736.jpg", "1163649614": "/common/destiny2_content/icons/44652f6aba6ec54e0782480679fe043b.jpg", "1169793114": "/common/destiny2_content/icons/182310630a2dfa3236c9e2d74f795e93.jpg", "1182982189": "/common/destiny2_content/icons/3e864b06fb9266cac7b4ab89835d2e6f.jpg", "1226048594": "/common/destiny2_content/icons/775f67a65132fc728618f8a18df1f905.jpg", "1233471745": "/common/destiny2_content/icons/d9373b83e799a2d4035d82526ac362f3.jpg", "1345348453": "/common/destiny2_content/icons/66fbe54a24d5a2b9978c909eb15ab62d.jpg", "1385469960": "/common/destiny2_content/icons/562b80b58bccce105b7cdd81847f665c.jpg", "1390549867": "/common/destiny2_content/icons/27fdad9f07de0dc4b97bf8574e6e5242.jpg", "1439993428": "/common/destiny2_content/icons/f3c921857b1539c916692ae84ed925e2.jpg", "1450844310": "/common/destiny2_content/icons/69144416733c3dc21fcadfdbb9fcb0a2.jpg", "1461305554": "/common/destiny2_content/icons/c5efe44dfb505ff905c5cac4797c76fc.jpg", "1514331782": "/common/destiny2_content/icons/39eb2cda13c467b5b5391bb36d462517.jpg", "1547246830": "/common/destiny2_content/icons/013403d109a9563ee881830044177193.jpg", "1586771061": "/common/destiny2_content/icons/6144b4eb582f8a6bdb3b011d8e9d9e4e.jpg", "1605993075": "/common/destiny2_content/icons/0aa56f2363515a307fb6c7b04910930a.jpg", "1629497825": "/common/destiny2_content/icons/b2290e03e87daaa6a8e86f9490c379f3.jpg", "1648672968": "/common/destiny2_content/icons/405051879d60ba211b1514a9bb5c5fe5.jpg", "1833569807": "/common/destiny2_content/icons/68961477ce421de2dbd8cefbc59a6edf.jpg", "1855685192": "/common/destiny2_content/icons/cb7b28517f147274efe68112a637bc94.jpg", "1932245887": "/common/destiny2_content/icons/efbabef1e4d381ae4ada30a92be33924.jpg", "1940645774": "/common/destiny2_content/icons/374c3ebc08dd55d77a634360b3ec8603.jpg", "2135780490": "/common/destiny2_content/icons/596b7579fcc98656493dc3783e6a963e.jpg", "2249784217": "/common/destiny2_content/icons/4675323df325c33a0c759ac66fbd7f78.jpg", "2254190310": "/common/destiny2_content/icons/15acb140a9b88315b4539f3d98d9c435.jpg", "2329638530": "/common/destiny2_content/icons/f655ec9343e18cb811f69930d9cb426a.jpg", "2371798338": "/common/destiny2_content/icons/6a69af7476c5cec54db55a7b507dff6c.jpg", "2383994221": "/common/destiny2_content/icons/1d38c8d82f1bd1d22bf76314fe8764f9.jpg", "2387948227": "/common/destiny2_content/icons/a0ca8e1fddf00d1e2fa16357cabbd7f4.jpg", "2395817019": "/common/destiny2_content/icons/3a3dff881138741dc701c44b9043f552.jpg", "2479567609": "/common/destiny2_content/icons/544fe035ccdbaeceea70b1278e8285a0.jpg", "2524364954": "/common/destiny2_content/icons/45d19f57bcbb22fa70cedca91a3f987e.jpg", "2538406461": "/common/destiny2_content/icons/ed12186dd3abfe66b6b2bcb557319082.jpg", "2571854597": "/common/destiny2_content/icons/e18f1c00497de70e2a53d5dcd5c79cab.jpg", "2618920720": "/common/destiny2_content/icons/1bf12f1807f0926f6375fb494b7bf580.jpg", "2629249101": "/common/destiny2_content/icons/c9cd0f9705851f79a46508934d352451.jpg", "2708727662": "/common/destiny2_content/icons/95a9a1af9280ef24c6c0d821f589d399.jpg", "2744473468": "/common/destiny2_content/icons/2398efcf2c81d69f03fd5fab272046ad.jpg", "2744508352": "/common/destiny2_content/icons/0bada21df8674cde30fcaac3e599ff6e.jpg", "2761319400": "/common/destiny2_content/icons/19e7c9a3d838e3801e7147975d173cb5.jpg", "2856496392": "/common/destiny2_content/icons/28355e0adda864289165854e376f86ee.jpg", "2880917375": "/common/destiny2_content/icons/c4eadc82554420d33ecfb517416a9d1e.jpg", "2940589008": "/common/destiny2_content/icons/4a37bd1fc1756b09f509396557937832.jpg", "2960086668": "/common/destiny2_content/icons/23f4656a3dd3f4ee0e42d140ba251974.jpg", "3022631571": "/common/destiny2_content/icons/372ab21c37bd69c70e88860e4f6a8b7f.jpg", "3025950752": "/common/destiny2_content/icons/9db4e36e1e64749a01ce81c570b17f0f.jpg", "3095531901": "/common/destiny2_content/icons/b86db68bce5846e4d8767b519c0bcead.jpg", "3318895306": "/common/destiny2_content/icons/f709442adefbc5430ba5d99d50366366.jpg", "3368860448": "/common/destiny2_content/icons/e444e97b9d2c368006272fc90eb1e022.jpg", "3371896300": "/common/destiny2_content/icons/e68a5dba35f4b96f7230e88666e416e6.jpg", "3381077691": "/common/destiny2_content/icons/d3fe6666df5dc374448a600e8b9744d6.jpg", "3393121279": "/common/destiny2_content/icons/f2838bd9c7a5f45ce8df9ddb82eca8b9.jpg", "3396973108": "/common/destiny2_content/icons/6bc8ba6e234d5f80b17ecbecd1268702.jpg", "3465971755": "/common/destiny2_content/icons/2305fe129af716dae7af521e1537748c.jpg", "3468043157": "/common/destiny2_content/icons/773f96cb2f9f2149ec31874cafec7528.jpg", "3518287681": "/common/destiny2_content/icons/3b8175fb92ee774d15d25fed58dca619.jpg", "3531533350": "/common/destiny2_content/icons/e4053a126b622e640de2ad0499086b8a.jpg", "3567687058": "/common/destiny2_content/icons/2f0bb806a25c1b2a4d694ac1c4df9fdc.jpg", "3574136388": "/common/destiny2_content/icons/53bd2d1f0a0b34c9764d0cc949ac2594.jpg", "3589295049": "/common/destiny2_content/icons/a6d9a19ac7f14078210b91b59d3955bb.jpg", "3628100770": "/common/destiny2_content/icons/8e39a7150ffdb139deeb7eaaf42747f9.jpg", "3644944303": "/common/destiny2_content/icons/51c18c347866e7370597e0e155de4fb1.jpg", "3663964046": "/common/destiny2_content/icons/f1144dc4e0d95d87d63ecedc4d31c84c.jpg", "3740374319": "/common/destiny2_content/icons/bd42c14527a59e7638b25ca50caddd62.jpg", "3746741161": "/common/destiny2_content/icons/fc8734628c47ba0add073aef57f8e860.jpg", "3784038415": "/common/destiny2_content/icons/d3b1aeb1757f2011bc062befe58ab3dd.jpg", "3787307395": "/common/destiny2_content/icons/6bb8eac7386df477d7b6194438da8a33.jpg", "3802151748": "/common/destiny2_content/icons/46ebd791f692b682691416beea7ac4c5.jpg", "3835718947": "/common/destiny2_content/icons/d69bff533e6e2bb69f0d5df8692de1dd.jpg", "3891168900": "/common/destiny2_content/icons/bd6993b1382825614f2c542c2a1d097b.jpg", "3968841949": "/common/destiny2_content/icons/5106f4624da0a5ff26b97702547fc282.jpg", "4137195476": "/common/destiny2_content/icons/060d820c7307f8cc9bc303965092214e.jpg", "4147054663": "/common/destiny2_content/icons/5bcea1f9a52fb82286a26dca952c9259.jpg", "4178028503": "/common/destiny2_content/icons/68df199792e5e6ab67de1befba7e0595.jpg", "4235016462": "/common/destiny2_content/icons/76eeaac2611baa6f621654b394e3ce36.jpg" } ================================================ FILE: src/data/d2/craftable-hashes.json ================================================ [ 14194600, 45643573, 46125926, 46524085, 70083888, 92459755, 105306149, 120706239, 128782990, 135029084, 147444292, 172461430, 231031173, 232928045, 254636484, 268260372, 268260373, 291092617, 297296830, 318443586, 337578911, 342514437, 392008588, 407511664, 412251536, 424291879, 431721920, 445197843, 484515708, 495442100, 496728945, 501329015, 502356570, 522366885, 535198113, 542203595, 548958835, 578105049, 613334176, 648595258, 694500607, 768621510, 786352912, 820890091, 833898322, 851296754, 859869931, 892183998, 927567426, 963574173, 999767358, 1039915310, 1047932517, 1058098236, 1081724548, 1098171824, 1168625549, 1184309824, 1184692845, 1197771438, 1239700299, 1248372789, 1258168956, 1289796511, 1298815317, 1311684613, 1321506184, 1366394399, 1392919471, 1399109800, 1432682459, 1447836603, 1466006054, 1471212226, 1473821207, 1478986057, 1481594633, 1491665733, 1501688142, 1509167284, 1526296434, 1572896086, 1679868061, 1720503118, 1731355324, 1751893422, 1753923263, 1769847435, 1770490683, 1801007332, 1851777734, 1875512595, 1886840007, 1911060537, 1937552980, 1941816543, 1959650777, 1983149589, 1986287028, 1992309064, 1994645182, 2001697739, 2034215657, 2045811635, 2097055732, 2119346509, 2126178511, 2145441168, 2149683300, 2188764214, 2194955522, 2198166292, 2218569744, 2221264583, 2241507890, 2263839058, 2265407516, 2272041093, 2302346155, 2323544076, 2334480463, 2350330520, 2362652544, 2490988246, 2508948099, 2531963421, 2534546147, 2535142413, 2558925366, 2563668388, 2595497736, 2607304614, 2708806099, 2720651699, 2721157927, 2778013407, 2779821308, 2817683783, 2821677368, 2827764482, 2828278545, 2856514843, 2883484461, 2884596447, 2886339027, 2890082420, 2910326942, 2922749929, 2943293195, 2972949637, 2978226043, 2990047042, 2993554824, 3016891299, 3055790362, 3103325054, 3107853529, 3118061005, 3123651616, 3163900678, 3175851496, 3228096719, 3232203524, 3248429089, 3257091166, 3257091167, 3281285075, 3347946548, 3366545721, 3371413056, 3371413057, 3371413059, 3388655311, 3407395594, 3428521585, 3444688218, 3489657138, 3493494807, 3503019618, 3569407878, 3591141932, 3605603507, 3621336854, 3623686757, 3635821806, 3654744298, 3685470415, 3698448090, 3710082365, 3782662983, 3794274730, 3824673936, 3844610113, 3849444474, 3849810018, 3865728990, 3867373351, 3885259140, 3886416794, 3890055324, 3904516037, 3920310144, 3926103986, 3947966653, 3951511045, 3969066556, 4038592169, 4066778670, 4067556514, 4096943616, 4132072834, 4153087276, 4184168210, 4195186942, 4207120603, 4219826183, 4225322581, 4230965989, 4248569242 ] ================================================ FILE: src/data/d2/crafting-enhanced-intrinsics.ts ================================================ const enhancedIntrinsics = new Set([ 451371216, // Adaptive Burst 1063261332, // Adaptive Burst 1655963600, // Adaptive Burst 3049431773, // Adaptive Burst 4269485650, // Adaptive Burst 31057037, // Adaptive Burst 1795925907, // Adaptive Burst 1938288214, // Adaptive Burst 2503963990, // Adaptive Burst 3387402995, // Adaptive Burst 3414419157, // Adaptive Burst 3576895600, // Adaptive Burst 4193948962, // Adaptive Burst 4200880287, // Adaptive Burst 141515518, // Adaptive Frame 489506521, // Adaptive Frame 1900989425, // Adaptive Frame 2178772051, // Adaptive Frame 2490530085, // Adaptive Frame 3133077199, // Adaptive Frame 3355499393, // Adaptive Frame 3435239180, // Adaptive Frame 4249817697, // Adaptive Frame 1793150018, // Adaptive Frame 378204240, // Adaptive Glaive 1338909520, // Adaptive Glaive 1447716563, // Adaptive Glaive 2696719570, // Adaptive Glaive 581875391, // Aggressive Burst 1020196213, // Aggressive Burst 1068043187, // Aggressive Burst 2419864979, // Aggressive Burst 757651572, // Aggressive Frame 2662827496, // Aggressive Frame 3615521782, // Aggressive Frame 4074888076, // Aggressive Frame 1069611115, // Aggressive Frame 1361856293, // Aggressive Frame 1739861752, // Aggressive Frame 1748364716, // Aggressive Frame 1831499663, // Aggressive Frame 2059481925, // Aggressive Frame 2097693203, // Aggressive Frame 2101490074, // Aggressive Frame 2912509910, // Aggressive Frame 3794558792, // Aggressive Frame 4036525828, // Aggressive Frame 4198833635, // Aggressive Frame 4203236451, // Aggressive Frame 1749118639, // Aggressive Frame 2159352803, // Aggressive Frame 2552875793, // Aggressive Frame 3320257055, // Aggressive Frame 16021165, // Aggressive Frame 176072987, // Aggressive Frame 1827557644, // Aggressive Frame 3026990487, // Aggressive Frame 3884051579, // Aggressive Frame 189818679, // Aggressive Frame 389945760, // Aggressive Frame 1487938731, // Aggressive Frame 1632897927, // Aggressive Frame 1737914521, // Aggressive Frame 167794220, // Aggressive Frame 16445399, // Aggressive Glaive 738339367, // Aggressive Glaive 2040155611, // Aggressive Glaive 3228668394, // Aggressive Glaive 244760020, // Area Denial Frame 1208953762, // Area Denial Frame 1782704870, // Area Denial Frame 2012023082, // Area Denial Frame 3035281791, // Caster Frame 210578077, // Compressed Wave Frame 667908513, // Compressed Wave Frame 2264456959, // Compressed Wave Frame 3142073525, // Compressed Wave Frame 583593420, // Double Fire 650063316, // Double Fire 983290038, // Double Fire 3755958904, // Double Fire 570373697, // Heavy Burst 749936529, // Heavy Burst 1370384437, // Heavy Burst 1817649409, // Heavy Burst 2170254329, // Heavy Burst 2213429699, // Heavy Burst 2444870733, // Heavy Burst 2732540911, // Heavy Burst 3099083217, // Heavy Burst 3291480605, // Heavy Burst 3456918063, // Heavy Burst 4175870265, // Heavy Burst 302702765, // High-Impact Frame 1451602450, // High-Impact Frame 1472963920, // High-Impact Frame 1875323682, // High-Impact Frame 1946568256, // High-Impact Frame 2516075140, // High-Impact Frame 2617324347, // High-Impact Frame 3769337248, // High-Impact Frame 4231246084, // High-Impact Frame 916649862, // Legacy PR-55 Frame 2155015844, // Legacy PR-55 Frame 3332480988, // Legacy PR-55 Frame 3766386008, // Legacy PR-55 Frame 118439741, // Lightweight Frame 132189785, // Lightweight Frame 885620491, // Lightweight Frame 1665848857, // Lightweight Frame 395096174, // Lightweight Frame 2012877834, // Lightweight Frame 3048420653, // Lightweight Frame 3472640090, // Lightweight Frame 3728676938, // Lightweight Frame 308595185, // Lightweight Frame 539564471, // Lightweight Frame 956390015, // Lightweight Frame 1671927875, // Lightweight Frame 3272152575, // Lightweight Frame 3377988521, // Lightweight Frame 3725333007, // Lightweight Frame 4008973208, // Lightweight Frame 4080055066, // Lightweight Frame 2105054824, // Lightweight Frame 1057935015, // MIDA Synergy 1891876363, // MIDA Synergy 2470575005, // MIDA Synergy 2670025099, // MIDA Synergy 368110299, // Micro-Missile Frame 651554065, // Micro-Missile Frame 687584589, // Micro-Missile Frame 1225770249, // Micro-Missile Frame 2100231191, // Micro-Missile Frame 3178379945, // Micro-Missile Frame 540070330, // Micro-Missile Frame 745725302, // Micro-Missile Frame 1278053348, // Micro-Missile Frame 4024789138, // Micro-Missile Frame 878620401, // Pinpoint Slug Frame 2410819063, // Pinpoint Slug Frame 2426674025, // Pinpoint Slug Frame 2897981193, // Pinpoint Slug Frame 1827389998, // Precision Frame 3114731754, // Precision Frame 3252839262, // Precision Frame 3665558569, // Precision Frame 4000302358, // Precision Frame 2305599261, // Precision Frame 2458294492, // Precision Frame 2563509458, // Precision Frame 2725422375, // Precision Frame 2743098132, // Precision Frame 3046673757, // Precision Frame 3094629643, // Precision Frame 3274444880, // Precision Frame 3356299403, // Precision Frame 3489809232, // Precision Frame 445823153, // Precision Frame 668357349, // Precision Frame 2094305299, // Precision Frame 3192296481, // Precision Frame 26177576, // Precision Frame 797798924, // Precision Frame 1206996986, // Precision Frame 3646909656, // Precision Frame 433469519, // Precision Frame 886865893, // Precision Frame 1034287523, // Precision Frame 4113841443, // Precision Frame 482158780, // Precision Frame 738967614, // Precision Frame 762204274, // Precision Frame 2409208302, // Precision Frame 2927971896, // Precision Frame 2986718682, // Precision Frame 3573764622, // Precision Frame 3774850330, // Precision Frame 224485255, // Precision Frame 364418505, // Precision Frame 384272571, // Precision Frame 453527127, // Precision Frame 743857847, // Precision Frame 811588234, // Precision Frame 1045795938, // Precision Frame 1052350088, // Precision Frame 3000852559, // Precision Frame 3226552705, // Precision Frame 3399947696, // Precision Frame 3419227006, // Precision Frame 3913106382, // Precision Frame 680193725, // Rapid-Fire Frame 762801111, // Rapid-Fire Frame 872207875, // Rapid-Fire Frame 1483339932, // Rapid-Fire Frame 1564126489, // Rapid-Fire Frame 2164888232, // Rapid-Fire Frame 2806361224, // Rapid-Fire Frame 2984571381, // Rapid-Fire Frame 3095041770, // Rapid-Fire Frame 3249407402, // Rapid-Fire Frame 3478030936, // Rapid-Fire Frame 3841661468, // Rapid-Fire Frame 4188742693, // Rapid-Fire Frame 137876701, // Rapid-Fire Frame 802623077, // Rapid-Fire Frame 1027896051, // Rapid-Fire Frame 1497440861, // Rapid-Fire Frame 1765356367, // Rapid-Fire Frame 1787083609, // Rapid-Fire Frame 1886418605, // Rapid-Fire Frame 2003022817, // Rapid-Fire Frame 2260949877, // Rapid-Fire Frame 2263539715, // Rapid-Fire Frame 2986029425, // Rapid-Fire Frame 4116588173, // Rapid-Fire Frame 1576423267, // Rapid-Fire Frame 1707990417, // Rapid-Fire Frame 1894749743, // Rapid-Fire Frame 3688301727, // Rapid-Fire Frame 2593449, // Rapid-Fire Glaive 217816625, // Rapid-Fire Glaive 694879972, // Rapid-Fire Glaive 2479787361, // Rapid-Fire Glaive 156358224, // Support Frame 840061228, // Support Frame 1535085486, // Support Frame 3161047604, // Support Frame 785441979, // Support Frame 1926141333, // Support Frame 2491949917, // Support Frame 2707250581, // Support Frame 1241894699, // Together Forever 1300107783, // Together Forever 2826720951, // Together Forever 3513901081, // Together Forever 40492375, // Vortex Frame 621911507, // Wave Frame 1220310607, // Wave Frame 1713394949, // Wave Frame 3030812579, // Wave Frame 1978306813, // Wave Sword Frame ]); export default enhancedIntrinsics; ================================================ FILE: src/data/d2/crafting-mementos.json ================================================ { "games": [18179099, 1429874803, 3617209212], "monolithic": [461727014, 1745945065, 3100848450], "vaultofglass": [574898816, 3179652816, 3752630563], "gardenofsalvation": [619391783, 1750913471, 2430770464], "solstice": [1257952519, 3481131423, 4160988416], "lost": [1261107326, 1971919313, 3686736741], "nightfall": [1425794805, 3254175265, 4240221838], "ironbanner": [1483496162, 1950948038, 3977496457], "trials": [2012491044, 3408716127, 3831031876], "gambit": [2140049750, 3060237106, 3088135833], "champion": [2519247511, 3346875528, 4049753919], "dawning": [2676329044, 3809278479, 3921485172] } ================================================ FILE: src/data/d2/d2-event-info-v2.ts ================================================ export const enum D2EventEnum { DAWNING = 1, CRIMSON_DAYS, SOLSTICE_OF_HEROES, FESTIVAL_OF_THE_LOST, REVELRY, GUARDIAN_GAMES, } export const D2EventInfo = { [D2EventEnum.DAWNING]: { name: 'The Dawning', shortname: 'dawning', sources: [464727567, 547767158, 629617846, 2364515524, 3092212681, 3952847349, 4054646289], engram: [1170720694, 3151770741], }, [D2EventEnum.CRIMSON_DAYS]: { name: 'Crimson Days', shortname: 'crimsondays', sources: [2502262376], engram: [191363032, 3373123597], }, [D2EventEnum.SOLSTICE_OF_HEROES]: { name: 'Solstice', shortname: 'solstice', sources: [151416041, 641018908, 1666677522, 2050870152, 3724111213], engram: [821844118], }, [D2EventEnum.FESTIVAL_OF_THE_LOST]: { name: 'Festival of the Lost', shortname: 'fotl', sources: [1054169368, 1677921161, 1919933822, 3190938946, 3482766024, 3693722471, 4041583267], engram: [ 1123009796, 1130434762, 1130434763, 1130434764, 1130434765, 1130434767, 1451959506, 1876344833, 1876344834, 1876344835, 1876344836, 1876344837, 2113144106, 2113144107, 2113144108, 2113144109, 2113144111, 3021397983, 3485785096, 3485785098, 3485785099, 3485785100, 3485785101, 3768874632, 3768874633, 3768874637, 3768874638, 3768874639, ], }, [D2EventEnum.REVELRY]: { name: 'The Revelry', shortname: 'revelry', sources: [2187511136], engram: [1974821348, 2570200927], }, [D2EventEnum.GUARDIAN_GAMES]: { name: 'Guardian Games', shortname: 'games', sources: [611838069, 1568732528, 2006303146, 2011810450, 2473294025, 3095773956, 3388021959], engram: [], }, }; ================================================ FILE: src/data/d2/d2-event-info.ts ================================================ export const enum D2EventEnum { DAWNING = 1, CRIMSON_DAYS, SOLSTICE_OF_HEROES, FESTIVAL_OF_THE_LOST, REVELRY, GUARDIAN_GAMES, } export const D2EventInfo = { 1: { name: 'The Dawning', shortname: 'dawning', sources: [464727567, 547767158, 629617846, 2364515524, 3092212681, 3952847349, 4054646289], engram: [1170720694, 3151770741], }, 2: { name: 'Crimson Days', shortname: 'crimsondays', sources: [2502262376], engram: [191363032, 3373123597], }, 3: { name: 'Solstice', shortname: 'solstice', sources: [151416041, 641018908, 1666677522, 2050870152, 3724111213], engram: [821844118], }, 4: { name: 'Festival of the Lost', shortname: 'fotl', sources: [1054169368, 1677921161, 1919933822, 3190938946, 3482766024, 3693722471, 4041583267], engram: [ 1123009796, 1130434762, 1130434763, 1130434764, 1130434765, 1130434767, 1451959506, 1876344833, 1876344834, 1876344835, 1876344836, 1876344837, 2113144106, 2113144107, 2113144108, 2113144109, 2113144111, 3021397983, 3485785096, 3485785098, 3485785099, 3485785100, 3485785101, 3768874632, 3768874633, 3768874637, 3768874638, 3768874639, ], }, 5: { name: 'The Revelry', shortname: 'revelry', sources: [2187511136], engram: [1974821348, 2570200927], }, 6: { name: 'Guardian Games', shortname: 'games', sources: [611838069, 1568732528, 2006303146, 2011810450, 2473294025, 3095773956, 3388021959], engram: [], }, }; export type D2EventIndex = keyof typeof D2EventInfo; export const D2EventPredicateLookup = { dawning: D2EventEnum.DAWNING, crimsondays: D2EventEnum.CRIMSON_DAYS, solstice: D2EventEnum.SOLSTICE_OF_HEROES, fotl: D2EventEnum.FESTIVAL_OF_THE_LOST, revelry: D2EventEnum.REVELRY, games: D2EventEnum.GUARDIAN_GAMES, }; export const D2SourcesToEvent = { 464727567: D2EventEnum.DAWNING, 547767158: D2EventEnum.DAWNING, 629617846: D2EventEnum.DAWNING, 2364515524: D2EventEnum.DAWNING, 3092212681: D2EventEnum.DAWNING, 3952847349: D2EventEnum.DAWNING, 4054646289: D2EventEnum.DAWNING, 2502262376: D2EventEnum.CRIMSON_DAYS, 151416041: D2EventEnum.SOLSTICE_OF_HEROES, 641018908: D2EventEnum.SOLSTICE_OF_HEROES, 1666677522: D2EventEnum.SOLSTICE_OF_HEROES, 2050870152: D2EventEnum.SOLSTICE_OF_HEROES, 3724111213: D2EventEnum.SOLSTICE_OF_HEROES, 1054169368: D2EventEnum.FESTIVAL_OF_THE_LOST, 1677921161: D2EventEnum.FESTIVAL_OF_THE_LOST, 1919933822: D2EventEnum.FESTIVAL_OF_THE_LOST, 3190938946: D2EventEnum.FESTIVAL_OF_THE_LOST, 3482766024: D2EventEnum.FESTIVAL_OF_THE_LOST, 3693722471: D2EventEnum.FESTIVAL_OF_THE_LOST, 4041583267: D2EventEnum.FESTIVAL_OF_THE_LOST, 2187511136: D2EventEnum.REVELRY, 611838069: D2EventEnum.GUARDIAN_GAMES, 1568732528: D2EventEnum.GUARDIAN_GAMES, 2006303146: D2EventEnum.GUARDIAN_GAMES, 2011810450: D2EventEnum.GUARDIAN_GAMES, 2473294025: D2EventEnum.GUARDIAN_GAMES, 3095773956: D2EventEnum.GUARDIAN_GAMES, 3388021959: D2EventEnum.GUARDIAN_GAMES, }; ================================================ FILE: src/data/d2/d2-season-info.ts ================================================ export const D2SeasonInfo: Record< number, { DLCName: string; seasonName: string; seasonTag: string; season: number; maxLevel: number; powerFloor: number; softCap: number; powerfulCap: number; pinnacleCap: number; releaseDate: string; resetTime: string; numWeeks: number; episode?: number; } > = { 1: { DLCName: 'Red War', seasonName: 'Red War', seasonTag: 'redwar', season: 1, maxLevel: 20, powerFloor: 0, softCap: 285, powerfulCap: 300, pinnacleCap: 300, releaseDate: '2017-09-06', resetTime: '09:00:00Z', numWeeks: 13, }, 2: { DLCName: 'Curse of Osiris', seasonName: 'Curse of Osiris', seasonTag: 'osiris', season: 2, maxLevel: 25, powerFloor: 0, softCap: 320, powerfulCap: 330, pinnacleCap: 330, releaseDate: '2017-12-05', resetTime: '17:00:00Z', numWeeks: 22, }, 3: { DLCName: 'Warmind', seasonName: 'Resurgence', seasonTag: 'warmind', season: 3, maxLevel: 30, powerFloor: 0, softCap: 340, powerfulCap: 380, pinnacleCap: 380, releaseDate: '2018-05-08', resetTime: '18:00:00Z', numWeeks: 17, }, 4: { DLCName: 'Forsaken', seasonName: 'Season of the Outlaw', seasonTag: 'outlaw', season: 4, maxLevel: 50, powerFloor: 0, softCap: 500, powerfulCap: 600, pinnacleCap: 600, releaseDate: '2018-09-04', resetTime: '17:00:00Z', numWeeks: 12, }, 5: { DLCName: 'Black Armory', seasonName: 'Season of the Forge', seasonTag: 'forge', season: 5, maxLevel: 50, powerFloor: 0, softCap: 500, powerfulCap: 650, pinnacleCap: 650, releaseDate: '2018-11-27', resetTime: '17:00:00Z', numWeeks: 14, }, 6: { DLCName: "Joker's Wild", seasonName: 'Season of the Drifter', seasonTag: 'drifter', season: 6, maxLevel: 50, powerFloor: 0, softCap: 500, powerfulCap: 700, pinnacleCap: 700, releaseDate: '2019-03-05', resetTime: '17:00:00Z', numWeeks: 13, }, 7: { DLCName: 'Penumbra', seasonName: 'Season of Opulence', seasonTag: 'opulence', season: 7, maxLevel: 50, powerFloor: 0, softCap: 500, powerfulCap: 750, pinnacleCap: 750, releaseDate: '2019-06-04', resetTime: '17:00:00Z', numWeeks: 17, }, 8: { DLCName: 'Shadowkeep', seasonName: 'Season of the Undying', seasonTag: 'undying', season: 8, maxLevel: 50, powerFloor: 750, softCap: 900, powerfulCap: 950, pinnacleCap: 960, releaseDate: '2019-10-01', resetTime: '17:00:00Z', numWeeks: 10, }, 9: { DLCName: '', seasonName: 'Season of Dawn', seasonTag: 'dawn', season: 9, maxLevel: 50, powerFloor: 750, softCap: 900, powerfulCap: 960, pinnacleCap: 970, releaseDate: '2019-12-10', resetTime: '17:00:00Z', numWeeks: 13, }, 10: { DLCName: '', seasonName: 'Season of the Worthy', seasonTag: 'worthy', season: 10, maxLevel: 50, powerFloor: 750, softCap: 950, powerfulCap: 1000, pinnacleCap: 1010, releaseDate: '2020-03-10', resetTime: '17:00:00Z', numWeeks: 13, }, 11: { DLCName: '', seasonName: 'Season of Arrivals', seasonTag: 'arrivals', season: 11, maxLevel: 50, powerFloor: 750, softCap: 1000, powerfulCap: 1050, pinnacleCap: 1060, releaseDate: '2020-06-09', resetTime: '17:00:00Z', numWeeks: 22, }, 12: { DLCName: 'Beyond Light', seasonName: 'Season of the Hunt', seasonTag: 'hunt', season: 12, maxLevel: 50, powerFloor: 1050, softCap: 1200, powerfulCap: 1250, pinnacleCap: 1260, releaseDate: '2020-11-10', resetTime: '17:00:00Z', numWeeks: 13, }, 13: { DLCName: '', seasonName: 'Season of the Chosen', seasonTag: 'chosen', season: 13, maxLevel: 50, powerFloor: 1100, softCap: 1250, powerfulCap: 1300, pinnacleCap: 1310, releaseDate: '2021-02-09', resetTime: '17:00:00Z', numWeeks: 13, }, 14: { DLCName: '', seasonName: 'Season of the Splicer', seasonTag: 'splicer', season: 14, maxLevel: 50, powerFloor: 1100, softCap: 1250, powerfulCap: 1310, pinnacleCap: 1320, releaseDate: '2021-05-11', resetTime: '17:00:00Z', numWeeks: 15, }, 15: { DLCName: '', seasonName: 'Season of the Lost', seasonTag: 'lost', season: 15, maxLevel: 50, powerFloor: 1100, softCap: 1250, powerfulCap: 1320, pinnacleCap: 1330, releaseDate: '2021-08-24', resetTime: '17:00:00Z', numWeeks: 26, }, 16: { DLCName: 'Witch Queen', seasonName: 'Season of the Risen', seasonTag: 'risen', season: 16, maxLevel: 50, powerFloor: 1350, softCap: 1500, powerfulCap: 1550, pinnacleCap: 1560, releaseDate: '2022-02-22', resetTime: '17:00:00Z', numWeeks: 13, }, 17: { DLCName: '', seasonName: 'Season of the Haunted', seasonTag: 'haunted', season: 17, maxLevel: 50, powerFloor: 1350, softCap: 1510, powerfulCap: 1560, pinnacleCap: 1570, releaseDate: '2022-05-24', resetTime: '17:00:00Z', numWeeks: 13, }, 18: { DLCName: '', seasonName: 'Season of Plunder', seasonTag: 'plunder', season: 18, maxLevel: 50, powerFloor: 1350, softCap: 1520, powerfulCap: 1570, pinnacleCap: 1580, releaseDate: '2022-08-23', resetTime: '17:00:00Z', numWeeks: 15, }, 19: { DLCName: '', seasonName: 'Season of the Seraph', seasonTag: 'seraph', season: 19, maxLevel: 50, powerFloor: 1350, softCap: 1530, powerfulCap: 1580, pinnacleCap: 1590, releaseDate: '2022-12-06', resetTime: '17:00:00Z', numWeeks: 12, }, 20: { DLCName: 'Lightfall', seasonName: 'Season of Defiance', seasonTag: 'defiance', season: 20, maxLevel: 50, powerFloor: 1600, softCap: 1750, powerfulCap: 1800, pinnacleCap: 1810, releaseDate: '2023-02-28', resetTime: '17:00:00Z', numWeeks: 12, }, 21: { DLCName: '', seasonName: 'Season of the Deep', seasonTag: 'deep', season: 21, maxLevel: 50, powerFloor: 1600, softCap: 1750, powerfulCap: 1800, pinnacleCap: 1810, releaseDate: '2023-05-23', resetTime: '17:00:00Z', numWeeks: 13, }, 22: { DLCName: '', seasonName: 'Season of the Witch', seasonTag: 'witch', season: 22, maxLevel: 50, powerFloor: 1600, softCap: 1750, powerfulCap: 1800, pinnacleCap: 1810, releaseDate: '2023-08-22', resetTime: '17:00:00Z', numWeeks: 14, }, 23: { DLCName: '', seasonName: 'Season of the Wish', seasonTag: 'wish', season: 23, maxLevel: 50, powerFloor: 1600, softCap: 1750, powerfulCap: 1800, pinnacleCap: 1810, releaseDate: '2023-11-28', resetTime: '17:00:00Z', numWeeks: 27, }, 24: { DLCName: 'The Final Shape', seasonName: 'Episode: Echoes', seasonTag: 'echoes', season: 24, maxLevel: 50, powerFloor: 1900, softCap: 1940, powerfulCap: 1990, pinnacleCap: 2000, releaseDate: '2024-06-04', resetTime: '17:00:00Z', numWeeks: 18, episode: 1, }, 25: { DLCName: 'Revenant', seasonName: 'Episode: Revenant', seasonTag: 'revenant', season: 25, maxLevel: 50, powerFloor: 1900, softCap: 1950, powerfulCap: 2000, pinnacleCap: 2010, releaseDate: '2024-10-08', resetTime: '17:00:00Z', numWeeks: 17, episode: 2, }, 26: { DLCName: 'Heresy', seasonName: 'Episode: Heresy', seasonTag: 'heresy', season: 26, maxLevel: 50, powerFloor: 1900, softCap: 1960, powerfulCap: 2010, pinnacleCap: 2020, releaseDate: '2025-02-04', resetTime: '17:00:00Z', numWeeks: 23, episode: 3, }, 27: { DLCName: 'Edge of Fate', seasonName: 'Reclamation', seasonTag: 'reclamation', season: 27, maxLevel: 50, powerFloor: 10, softCap: 200, powerfulCap: 500, pinnacleCap: 550, releaseDate: '2025-07-15', resetTime: '17:00:00Z', numWeeks: 20, }, 28: { DLCName: 'Renegades', seasonName: 'Lawless', seasonTag: 'lawless', season: 28, maxLevel: 50, powerFloor: 10, softCap: 200, powerfulCap: 500, pinnacleCap: 550, releaseDate: '2025-12-02', resetTime: '17:00:00Z', numWeeks: 33, }, }; export const D2CalculatedSeason = 28; export const D2SeasonPassActiveList = 0; ================================================ FILE: src/data/d2/d2-trials-objectives.json ================================================ { "passages": [ 46532100, 583402086, 1766769942 ], "objectives": { "250385543": "Trials Multiplier", "1275600285": "Ticket win streak", "1586211619": "Wins", "2552133478": "Flawless win streak", "3682362563": "Active win streak", "3930064064": "Longest streak rewarded" } } ================================================ FILE: src/data/d2/deprecated-mods.json ================================================ [] ================================================ FILE: src/data/d2/dummy-catalyst-mapping.json ================================================ { "354293076": 1824496861, "354293077": 1824496860, "390807531": 1864989992, "544137184": 1249968539, "544137185": 1249968538, "680163197": 658317088, "800074992": 3466057365, "854868710": 769440797, "1340292993": 2732814938, "1620506138": 615063267, "1620506139": 615063266, "1637046321": 2674202880, "1678902463": 2409031770, "1772382457": 1496699324, "1891148055": 920755188, "2101754671": 3459475454, "2142466730": 136852797, "2282260620": 2085058763, "2282260621": 2085058762, "2408641879": 4067652714, "2626423393": 1733620422, "2790377728": 1783582993, "2858348496": 924149235, "2858348497": 924149234, "3384861888": 1758592809, "3804992459": 1149703256, "3815768596": 484491717, "3867277431": 1684153732, "4233905576": 456628589, "4233905577": 456628588 } ================================================ FILE: src/data/d2/empty-plug-hashes.ts ================================================ export const emptyPlugHashes = new Set([ 235531041, // Activity Mod Socket (Activity Ghost Mod, enhancements.ghosts_activity) 3545404847, // Activity Mod Socket (Activity Ghost Mod, enhancements.ghosts_activity_fake) 1892982956, // Default Combat Flair (Combat Flair, weapon_tiering_kill_vfx) 1390587439, // Default Effect (ship.spawnfx) 1608119540, // Default Emblem (Emblem, emblem.variant) 702981643, // Default Ornament (Restore Defaults, armor_skins_empty) 1959648454, // Default Ornament (exotic_all_skins) 2931483505, // Default Ornament (exotic_all_skins) 3854296178, // Default Ornament (weapon_tiering_tier5_skins) 2325217837, // Default Shader (Restore Defaults, shader) 4248210736, // Default Shader (Restore Defaults, shader) 2794014115, // Default Weapon Effects (v420.plugs.weapons.masterworks.toggle.vfx) 1675508353, // Economic Mod Socket (Economic Ghost Mod, enhancements.ghosts_economic) 791435474, // Empty Activity Mod Socket (Deprecated Armor Mod, enhancements.activity) 3074755706, // Empty Arrows Socket (crafting.recipes.empty_socket) 2802541735, // Empty Aspect Socket (hunter.arc.aspects) 518663192, // Empty Aspect Socket (hunter.prism.aspects) 1715180370, // Empty Aspect Socket (hunter.shared.aspects) 3875863236, // Empty Aspect Socket (hunter.solar.aspects) 4037640975, // Empty Aspect Socket (hunter.strand.aspects) 2801436041, // Empty Aspect Socket (hunter.void.aspects) 2789698445, // Empty Aspect Socket (titan.arc.aspects) 3635963100, // Empty Aspect Socket (titan.prism.aspects) 321296654, // Empty Aspect Socket (titan.shared.aspects) 3416473448, // Empty Aspect Socket (titan.solar.aspects) 3207138885, // Empty Aspect Socket (titan.strand.aspects) 662916127, // Empty Aspect Socket (titan.void.aspects) 3472368310, // Empty Aspect Socket (warlock.arc.aspects) 1080004479, // Empty Aspect Socket (warlock.prism.aspects) 3819991001, // Empty Aspect Socket (warlock.shared.aspects) 2352766955, // Empty Aspect Socket (warlock.solar.aspects) 2164407902, // Empty Aspect Socket (warlock.strand.aspects) 3834374608, // Empty Aspect Socket (warlock.void.aspects) 1007199041, // Empty Barrels Socket (crafting.recipes.empty_socket) 1527687869, // Empty Batteries Socket (crafting.recipes.empty_socket) 2836298415, // Empty Blades Socket (crafting.recipes.empty_socket) 3471922734, // Empty Bowstrings Socket (crafting.recipes.empty_socket) 1498917124, // Empty Catalyst Socket (v400.empty.exotic.masterwork) 1649663920, // Empty Catalyst Socket (v400.empty.exotic.masterwork) 1961918267, // Empty Deepsight Socket (crafting.plugs.weapons.mods.extractors) 253922071, // Empty Enhancement Socket (crafting.plugs.weapons.mods.enhancers) 1826298670, // Empty Fragment Socket (shared.arc.fragments) 3363787531, // Empty Fragment Socket (shared.arc.fragments) 3251563851, // Empty Fragment Socket (shared.fragments) 2808665197, // Empty Fragment Socket (shared.prism.fragments) 3720092164, // Empty Fragment Socket (shared.prism.fragments) 424005861, // Empty Fragment Socket (shared.solar.fragments) 4205702044, // Empty Fragment Socket (shared.solar.fragments) 330751742, // Empty Fragment Socket (shared.stasis.trinkets) 1618645595, // Empty Fragment Socket (shared.strand.fragments) 2111549310, // Empty Fragment Socket (shared.strand.fragments) 770211541, // Empty Fragment Socket (shared.void.fragments) 1372656116, // Empty Fragment Socket (shared.void.fragments) 1372656117, // Empty Fragment Socket (shared.void.fragments) 1219897208, // Empty Frames Socket (crafting.recipes.empty_socket) 366474809, // Empty Grips Socket (crafting.recipes.empty_socket) 1779961758, // Empty Guards Socket (crafting.recipes.empty_socket) 1232390730, // Empty Hafts Socket (crafting.recipes.empty_socket) 3057124503, // Empty Magazines Socket (crafting.recipes.empty_socket) 3803329707, // Empty Magazines Socket (crafting.recipes.empty_socket) 2909846572, // Empty Memento Socket (crafting.recipes.empty_socket) 666522389, // Empty Mod Socket (General Armor Mod, core.gear_systems.event_gear.item_sets.selectors) 481675395, // Empty Mod Socket (General Armor Mod, deprecated) 4173924323, // Empty Mod Socket (Artifice Armor Mod, enhancements.artifice) 4055462131, // Empty Mod Socket (Deep Stone Crypt Raid Mod, enhancements.raid_descent) 706611068, // Empty Mod Socket (Garden of Salvation Raid Mod, enhancements.raid_garden) 3738398030, // Empty Mod Socket (Vault of Glass Armor Mod, enhancements.raid_v520) 2447143568, // Empty Mod Socket (Vow of the Disciple Raid Mod, enhancements.raid_v600) 1728096240, // Empty Mod Socket (King's Fall Mod, enhancements.raid_v620) 4144354978, // Empty Mod Socket (Root of Nightmares Armor Mod, enhancements.raid_v700) 717667840, // Empty Mod Socket (Crota's End Mod, enhancements.raid_v720) 4059283783, // Empty Mod Socket (Salvation's Edge Armor Mod, enhancements.raid_v800) 720857, // Empty Mod Socket (Legacy Armor Mod, enhancements.season_forge) 1180997867, // Empty Mod Socket (Nightmare Mod, enhancements.season_maverick) 2620967748, // Empty Mod Socket (Legacy Armor Mod, enhancements.season_maverick) 4106547009, // Empty Mod Socket (Legacy Armor Mod, enhancements.season_opulence) 1679876242, // Empty Mod Socket (Last Wish Raid Mod, enhancements.season_outlaw) 3625698764, // Empty Mod Socket (Legacy Armor Mod, enhancements.season_outlaw) 1182150429, // Empty Mod Socket (Armor Mod, enhancements.universal) 1835369552, // Empty Mod Socket (enhancements.universal) 2600899007, // Empty Mod Socket (Armor Mod, enhancements.universal) 3851138800, // Empty Mod Socket (Armor Mod, enhancements.universal) 1285086138, // Empty Mod Socket (Arms Artifact Mod, enhancements.v2_arms) 1844045567, // Empty Mod Socket (Arms Armor Mod, enhancements.v2_arms) 3820147479, // Empty Mod Socket (Arms Armor Mod, enhancements.v2_arms) 1659393211, // Empty Mod Socket (Chest Armor Mod, enhancements.v2_chest) 1803434835, // Empty Mod Socket (Chest Armor Mod, enhancements.v2_chest) 3965359154, // Empty Mod Socket (Chest Artifact Mod, enhancements.v2_chest) 1137289077, // Empty Mod Socket (Class Item Armor Mod, enhancements.v2_class_item) 3200810407, // Empty Mod Socket (Class Item Armor Mod, enhancements.v2_class_item) 4059708161, // Empty Mod Socket (Class Item Artifact Mod, enhancements.v2_class_item) 1980618587, // Empty Mod Socket (General Armor Mod, enhancements.v2_general) 787139317, // Empty Mod Socket (Helmet Artifact Mod, enhancements.v2_head) 807186981, // Empty Mod Socket (Helmet Armor Mod, enhancements.v2_head) 1078080765, // Empty Mod Socket (Helmet Armor Mod, enhancements.v2_head) 79843948, // Empty Mod Socket (Leg Artifact Mod, enhancements.v2_legs) 573150099, // Empty Mod Socket (Leg Armor Mod, enhancements.v2_legs) 2269836811, // Empty Mod Socket (Leg Armor Mod, enhancements.v2_legs) 144338558, // Empty Mod Socket (Weapon Mod, v400.weapon.mod_empty) 2323986101, // Empty Mod Socket (Weapon Mod, v400.weapon.mod_empty) 350414343, // Empty Mod Socket (Armor Mod, v404.armor.fotl.masks.abyss.perks) 2287797791, // Empty Mod Socket (Power Core, v950.new.sword0.perk_upgrades) 3224071925, // Empty Mod Socket (Blade Focus, v950.new.sword0.stat_upgrades) 51925409, // Empty Scopes Socket (crafting.recipes.empty_socket) 1134447515, // Empty Stocks Socket (crafting.recipes.empty_socket) 469511105, // Empty Traits Socket (crafting.recipes.empty_socket) 2503665585, // Empty Traits Socket (crafting.recipes.empty_socket) 819232495, // Empty Tubes Socket (crafting.recipes.empty_socket) 2121121504, // Empty Tuning Mod Socket (General Armor Mod, core.gear_systems.armor_tiering.plugs.tuning.mods) 4043342755, // Empty Weapon Level Boost Socket (crafting.plugs.weapons.mods.transfusers.level) 4216349042, // Experience Mod Socket (Experience Ghost Mod, enhancements.ghosts_experience) 2085536058, // Flickering Blessing Mod Socket (Flickering Blessing Destination Mod, schism_boons.destination_mods.efficiency) 1656746282, // Locked Artifice Socket (Artifice Armor Mod, enhancements.artifice.exotic) 2426387438, // No Projection (Restore Defaults, hologram) 3725942064, // Pale Blessing Mod Socket (Pale Blessing Destination Mod, schism_boons.destination_mods.playstyle) 1692473496, // Protocol Socket (v700.weapons.mods.mission_avalon) 760030801, // Tracking Mod Socket (Tracking Ghost Mod, enhancements.ghosts_tracking) ]); ================================================ FILE: src/data/d2/energy-mods-change.json ================================================ { "1": [ 4048086880, 3437002246, 3437002247, 3437002240, 3437002241, 3437002242, 3437002243, 3437002252, 3437002253, 3231365398 ], "2": [ 3020065861, 838280176, 838280177, 838280182, 838280183, 838280180, 838280181, 838280186, 838280187, 2601359289 ], "3": [ 4197017647, 3652771136, 3652771137, 3652771142, 3652771143, 3652771140, 3652771141, 3652771146, 3652771147, 2022178838 ] } ================================================ FILE: src/data/d2/energy-mods.json ================================================ { "1": [ null, 4048086883, 4048086882, 4048086885, 4048086884, 4048086887, 4048086886, 4048086889, 4048086888, 902052880 ], "2": [ null, 3020065862, 3020065863, 3020065856, 3020065857, 3020065858, 3020065859, 3020065868, 3020065869, 2768425135 ], "3": [ null, 4197017644, 4197017645, 4197017642, 4197017643, 4197017640, 4197017641, 4197017638, 4197017639, 4264493517 ] } ================================================ FILE: src/data/d2/engram-rarity-icons.json ================================================ { "Exotic": "/common/destiny2_content/icons/3e6a698e1a8a5fb446fdcbf1e63c5269.png", "Legendary": "/common/destiny2_content/icons/f846f489c2a97afb289b357e431ecf8d.png" } ================================================ FILE: src/data/d2/events.json ================================================ { "10656656": 6, "16638392": 3, "18179099": 6, "53761356": 5, "60802325": 3, "63024229": 1, "171748061": 4, "187034864": 1, "213458862": 6, "215596672": 1, "221164697": 1, "242730894": 3, "263371512": 2, "263371513": 2, "263371514": 2, "263371515": 2, "263371516": 2, "263371517": 2, "263371519": 2, "270671209": 1, "281718534": 5, "281718535": 5, "286098604": 5, "286098607": 5, "302488496": 6, "316740353": 1, "317465074": 1, "317465075": 1, "403465735": 3, "437421244": 1, "444302065": 4, "444302066": 4, "444302067": 4, "450844637": 3, "463166592": 3, "494187468": 4, "494187469": 4, "500083158": 5, "518566750": 2, "537036424": 3, "537036425": 3, "537036427": 3, "558870048": 3, "574694085": 3, "577345565": 3, "596833222": 2, "611329864": 1, "621810011": 4, "654307116": 4, "671247011": 1, "779962716": 1, "796633253": 4, "811724212": 3, "834178986": 3, "839740147": 3, "855351524": 1, "855351525": 1, "870019568": 5, "870527944": 4, "873770815": 3, "889413643": 1, "918791342": 4, "937162783": 4, "951413738": 1, "961496619": 3, "971728596": 5, "971728597": 5, "980898608": 3, "980898609": 3, "980898610": 3, "980898611": 3, "980898614": 3, "980898615": 3, "1005594230": 4, "1005594231": 4, "1028757552": 1, "1028757553": 1, "1028757554": 1, "1028757555": 1, "1028757556": 1, "1028757557": 1, "1028757558": 1, "1028757559": 1, "1028757567": 1, "1032136201": 4, "1052553862": 2, "1052553863": 2, "1067975722": 4, "1067975723": 4, "1112535733": 4, "1124054883": 1, "1131177620": 4, "1138577658": 4, "1138577659": 4, "1154659864": 1, "1165027035": 5, "1187045864": 6, "1187078090": 5, "1201782502": 4, "1201782503": 4, "1205148160": 6, "1257952519": 3, "1259278657": 3, "1261107326": 4, "1280755883": 1, "1288683596": 3, "1327083312": 1, "1334842411": 2, "1339405989": 3, "1356657785": 5, "1362642485": 5, "1363029408": 3, "1363029409": 3, "1423305584": 1, "1423305585": 1, "1423305586": 1, "1423305587": 1, "1423305588": 1, "1423305589": 1, "1423305590": 1, "1423305591": 1, "1423305598": 1, "1423305599": 1, "1429874803": 6, "1454610995": 3, "1473368760": 4, "1473368761": 4, "1473368764": 4, "1473368765": 4, "1473368766": 4, "1473368767": 4, "1482931023": 3, "1502135233": 1, "1502135240": 1, "1502135241": 1, "1502135242": 1, "1502135243": 1, "1502135244": 1, "1502135246": 1, "1502135247": 1, "1510405477": 3, "1547512994": 4, "1575548701": 4, "1584837156": 1, "1605596084": 2, "1623653768": 2, "1623653769": 2, "1623653771": 2, "1671745951": 1, "1690059054": 4, "1695429263": 1, "1702504372": 3, "1721185806": 4, "1721185807": 4, "1727527805": 4, "1750365155": 3, "1767312242": 1, "1771869770": 4, "1771869771": 4, "1775707016": 3, "1792195381": 1, "1821343040": 5, "1833569876": 4, "1833569877": 4, "1833569879": 4, "1836636207": 4, "1844904392": 4, "1844904393": 4, "1849697741": 3, "1858216083": 3, "1862324869": 3, "1870273657": 6, "1873273984": 1, "1914989540": 5, "1914989541": 5, "1952523260": 4, "1971240964": 4, "1971240965": 4, "1971919313": 4, "1987263977": 1, "2024188850": 4, "2039333456": 3, "2073576287": 1, "2105109359": 1, "2127474099": 3, "2156817213": 3, "2168550540": 1, "2179603792": 5, "2179603793": 5, "2179603794": 5, "2179603795": 5, "2179603798": 5, "2179603799": 5, "2201628119": 6, "2251060291": 3, "2253976433": 4, "2277536120": 4, "2277536122": 4, "2277536123": 4, "2291082292": 3, "2298896088": 3, "2298896090": 3, "2298896091": 3, "2298896092": 3, "2298896093": 3, "2298896094": 3, "2298896095": 3, "2300270673": 6, "2311506225": 1, "2314454779": 4, "2325283036": 3, "2325283037": 3, "2325283038": 3, "2325283039": 3, "2337290000": 3, "2367025562": 6, "2396888157": 3, "2419910641": 3, "2422643144": 5, "2433900295": 2, "2449203932": 3, "2459768632": 5, "2526736328": 4, "2538439951": 1, "2546370410": 3, "2549404869": 1, "2552954151": 3, "2556098840": 1, "2574262860": 4, "2574262861": 4, "2578820926": 3, "2593080269": 1, "2607476204": 1, "2607476205": 1, "2607476206": 1, "2607476207": 1, "2632141946": 4, "2661272173": 1, "2676329044": 1, "2699000684": 3, "2702372534": 1, "2774782768": 1, "2774782769": 1, "2799886702": 1, "2800372416": 4, "2805101184": 3, "2808451697": 4, "2873996295": 5, "2877046370": 3, "2883258882": 4, "2891490654": 1, "2912265353": 3, "2919938481": 3, "2949664689": 3, "2957044930": 3, "2957044931": 3, "2967148056": 5, "3012249670": 1, "3012249671": 1, "3023230941": 1, "3031612900": 3, "3035129091": 5, "3037536592": 4, "3054638345": 3, "3089075939": 5, "3105953706": 1, "3105953707": 1, "3105972940": 5, "3105972941": 5, "3105972942": 5, "3105972943": 5, "3117902614": 4, "3140833553": 1, "3140833554": 1, "3140833555": 1, "3140833556": 1, "3140833557": 1, "3140833558": 1, "3140833559": 1, "3154516913": 1, "3159052337": 3, "3166802179": 1, "3171649853": 2, "3222576960": 4, "3222576961": 4, "3222576962": 4, "3222576963": 4, "3222576964": 4, "3222576965": 4, "3222576966": 4, "3222576967": 4, "3222576972": 4, "3222576973": 4, "3244569127": 3, "3252358296": 5, "3252358297": 5, "3252358300": 5, "3252358301": 5, "3252358302": 5, "3252358303": 5, "3287805174": 1, "3287805175": 1, "3287805176": 1, "3287805177": 1, "3287805178": 1, "3287805179": 1, "3287805180": 1, "3287805181": 1, "3287805183": 1, "3299164941": 1, "3301429924": 1, "3306267716": 1, "3309556272": 1, "3326837142": 4, "3326837143": 4, "3328375332": 4, "3328375333": 4, "3328839466": 1, "3328839467": 1, "3344732822": 3, "3352962401": 4, "3367964921": 3, "3368317463": 1, "3373357626": 2, "3403933998": 1, "3425898631": 5, "3448772677": 4, "3475074928": 3, "3481131423": 3, "3503026437": 5, "3507818312": 3, "3510138048": 1, "3539322898": 1, "3570942494": 3, "3587167050": 2, "3617209212": 6, "3624435060": 6, "3631905978": 3, "3631905979": 3, "3661044024": 4, "3661044025": 4, "3665594271": 4, "3677746972": 4, "3677746973": 4, "3677746975": 4, "3682383633": 5, "3686736741": 4, "3692806198": 3, "3717471208": 3, "3717471209": 3, "3727205096": 5, "3727346032": 4, "3727346033": 4, "3734501342": 1, "3735037521": 3, "3750865260": 1, "3755201983": 4, "3766145631": 4, "3785074629": 4, "3800267412": 1, "3800267413": 1, "3800267414": 1, "3800267415": 1, "3807544519": 3, "3809278479": 1, "3809722908": 2, "3835954362": 3, "3859483818": 3, "3859483819": 3, "3909352274": 1, "3921485172": 1, "3932439362": 1, "3946927545": 5, "3956230125": 1, "3980154334": 1, "3980259370": 4, "3980259371": 4, "3987442049": 3, "4031340274": 1, "4031340275": 1, "4036178150": 1, "4044559530": 4, "4050120691": 4, "4051153950": 4, "4079552711": 5, "4087530286": 4, "4087530287": 4, "4089988225": 3, "4096637925": 3, "4097227155": 2, "4114677048": 3, "4128095381": 5, "4143534670": 3, "4160988416": 3, "4173467416": 3, "4173467417": 3, "4187872788": 4, "4199694578": 5, "4213271810": 3, "4213271811": 3, "4224972854": 4, "4224972855": 4, "4236468733": 3, "4264904777": 1, "4283023978": 4, "4283023979": 4, "4283023980": 4, "4283023981": 4, "4283023982": 4, "4283023983": 4 } ================================================ FILE: src/data/d2/exotic-synergy.json ================================================ { "50291571": { "damageType": [ 3 ] }, "89619507": { "damageType": [ 3 ] }, "106575079": { "damageType": [ 4 ], "subclass": [ 4260353952, 4260353953 ] }, "121305948": { "damageType": [ 2 ], "subclass": [ 1081893461 ] }, "192896783": { "damageType": [ 7 ] }, "193869523": { "damageType": [ 4 ], "subclass": [ 2722573681, 2722573683 ] }, "235591051": { "damageType": [ 3 ], "subclass": [ 2274196886 ] }, "241462141": { "damageType": [ 4 ], "subclass": [ 4260353952 ] }, "300502917": { "damageType": [ 4 ] }, "308767728": { "damageType": [ 2 ] }, "322173891": { "damageType": [ 6 ] }, "327385975": { "damageType": [ 3 ] }, "368733543": { "damageType": [ 2 ], "subclass": [ 3769507633 ] }, "370930766": { "damageType": [ 3 ] }, "405059572": { "damageType": [ 3 ] }, "461841403": { "damageType": [ 4 ] }, "475652357": { "damageType": [ 3 ] }, "508515818": { "damageType": [ 2 ] }, "511888814": { "damageType": [ 4 ] }, "599049453": { "damageType": [ 3 ] }, "691578979": { "damageType": [ 3 ], "subclass": [ 375052471 ] }, "728668916": { "damageType": [ 4 ] }, "730200061": { "damageType": [ 4 ] }, "768145972": { "damageType": [ 4 ] }, "863908533": { "damageType": [ 2 ], "subclass": [ 3769507633 ] }, "864224653": { "damageType": [ 3 ], "subclass": [ 2747500760 ] }, "903984858": { "damageType": [ 2 ] }, "925466716": { "damageType": [ 4 ] }, "925466718": { "damageType": [ 2 ], "subclass": [ 1081893460 ] }, "943320520": { "damageType": [ 4 ], "subclass": [ 4260353952, 4260353953 ] }, "950745251": { "damageType": [ 3 ] }, "978537162": { "damageType": [ 3 ] }, "1003391927": { "damageType": [ 7 ], "subclass": [ 1885339915 ] }, "1008364980": { "damageType": [ 7 ] }, "1030017949": { "damageType": [ 4 ], "subclass": [ 1656118681, 1656118682 ] }, "1053737370": { "damageType": [ 2 ] }, "1168370783": { "damageType": [ 4 ], "subclass": [ 2722573682 ] }, "1179169099": { "damageType": [ 3 ] }, "1188437342": { "damageType": [ 6 ] }, "1192890598": { "damageType": [ 3 ] }, "1203306857": { "damageType": [ 3 ] }, "1321354573": { "damageType": [ 3 ], "subclass": [ 375052468, 375052469 ] }, "1322544481": { "damageType": [ 6 ] }, "1407595207": { "damageType": [ 2 ] }, "1443166262": { "damageType": [ 4 ] }, "1446374842": { "damageType": [ 2 ] }, "1453120846": { "damageType": [ 7 ] }, "1467044898": { "damageType": [ 6 ] }, "1474735276": { "damageType": [ 4 ], "subclass": [ 2722573682 ] }, "1479082657": { "damageType": [ 7 ] }, "1537074069": { "damageType": [ 3 ] }, "1611241895": { "damageType": [ 2 ] }, "1619425569": { "damageType": [ 2 ] }, "1627691271": { "damageType": [ 2 ] }, "1703551922": { "damageType": [ 2 ], "subclass": [ 3769507633 ] }, "1703598057": { "damageType": [ 2 ] }, "1725917554": { "damageType": [ 2 ] }, "1734674241": { "damageType": [ 6 ] }, "1734844650": { "damageType": [ 3 ] }, "1747063685": { "damageType": [ 2 ], "subclass": [ 1081893460 ] }, "1803778317": { "damageType": [ 2 ] }, "1848640623": { "damageType": [ 4 ], "subclass": [ 4260353952 ] }, "1849149215": { "damageType": [ 2 ] }, "1849901681": { "damageType": [ 6 ] }, "1906093346": { "damageType": [ 4 ] }, "1935198785": { "damageType": [ 4 ] }, "1955548646": { "damageType": [ 7 ], "subclass": [ 1885339915 ] }, "1996008488": { "damageType": [ 2 ], "subclass": [ 1081893460 ] }, "2002759682": { "damageType": [ 4 ], "subclass": [ 4260353952 ] }, "2046130360": { "damageType": [ 4 ], "subclass": [ 4260353952 ] }, "2066430310": { "damageType": [ 3 ], "subclass": [ 2747500760 ] }, "2082483156": { "damageType": [ 3 ] }, "2094927311": { "damageType": [ 3 ] }, "2100136540": { "damageType": [ 7 ] }, "2109877638": { "damageType": [ 6 ], "subclass": [ 3683904166 ] }, "2169905051": { "damageType": [ 6 ] }, "2255796155": { "damageType": [ 2, 3, 4 ] }, "2268523867": { "damageType": [ 2 ], "subclass": [ 3769507632 ] }, "2316914168": { "damageType": [ 3 ], "subclass": [ 2274196886 ] }, "2321120637": { "damageType": [ 2 ], "subclass": [ 119041298 ] }, "2405271938": { "damageType": [ 4 ], "subclass": [ 2722573681, 2722573683 ] }, "2415768376": { "damageType": [ 3 ] }, "2439379860": { "damageType": [ 3 ] }, "2447096246": { "damageType": [ 4 ] }, "2463947681": { "damageType": [ 7 ] }, "2555349547": { "damageType": [ 6 ] }, "2563444729": { "damageType": [ 2 ] }, "2647299044": { "damageType": [ 7 ] }, "2749457084": { "damageType": [ 2 ] }, "2757274117": { "damageType": [ 4 ] }, "2766109872": { "damageType": [ 2 ], "subclass": [ 3769507633 ] }, "2767482390": { "damageType": [ 2 ], "subclass": [ 3769507632 ] }, "2822465023": { "damageType": [ 4 ] }, "2851385306": { "damageType": [ 6 ] }, "2915174768": { "damageType": [ 6 ] }, "3050017626": { "damageType": [ 4 ], "subclass": [ 1656118681, 1656118682 ] }, "3084282676": { "damageType": [ 2 ] }, "3139221735": { "damageType": [ 2 ] }, "3216110440": { "damageType": [ 2 ] }, "3234692237": { "damageType": [ 4 ] }, "3259193988": { "damageType": [ 6 ] }, "3261527845": { "damageType": [ 4 ] }, "3267996858": { "damageType": [ 4 ] }, "3316517958": { "damageType": [ 3 ] }, "3381022969": { "damageType": [ 2 ], "subclass": [ 1081893460 ] }, "3381022971": { "damageType": [ 4 ] }, "3450721096": { "damageType": [ 2, 3, 4 ] }, "3453042252": { "damageType": [ 3 ] }, "3481695565": { "damageType": [ 3 ] }, "3545094010": { "damageType": [ 3 ], "subclass": [ 2274196887 ] }, "3574051505": { "damageType": [ 6 ] }, "3608857511": { "damageType": [ 2 ], "subclass": [ 1081893461 ] }, "3617849232": { "damageType": [ 3 ] }, "3637722482": { "damageType": [ 7 ] }, "3642110725": { "damageType": [ 3 ] }, "3649264718": { "damageType": [ 2 ] }, "3717431477": { "damageType": [ 7 ] }, "3767088557": { "damageType": [ 3 ], "subclass": [ 2274196886 ] }, "3781549120": { "damageType": [ 2 ] }, "3787517196": { "damageType": [ 3 ] }, "3793245444": { "damageType": [ 6 ] }, "3831935023": { "damageType": [ 6 ], "subclass": [ 3683904166 ] }, "3849355625": { "damageType": [ 7 ] }, "3864952146": { "damageType": [ 3 ], "subclass": [ 2274196886 ] }, "3883286570": { "damageType": [ 3 ], "subclass": [ 375052471 ] }, "3883866764": { "damageType": [ 4 ] }, "3948284065": { "damageType": [ 4 ], "subclass": [ 1656118680 ] }, "3960926756": { "damageType": [ 3 ], "subclass": [ 375052468, 375052469 ] }, "3982932616": { "damageType": [ 4 ] }, "4002939008": { "damageType": [ 4 ] }, "4029987901": { "damageType": [ 4 ] }, "4035625646": { "damageType": [ 4 ], "subclass": [ 1656118680 ] }, "4057299719": { "damageType": [ 3 ], "subclass": [ 2274196887 ] }, "4057875770": { "damageType": [ 2 ], "subclass": [ 119041298 ] }, "4060352315": { "damageType": [ 4 ] }, "4119704973": { "damageType": [ 7 ] }, "4157369212": { "damageType": [ 7 ] }, "4165710258": { "damageType": [ 6 ] }, "4165919945": { "damageType": [ 2 ] }, "4177778015": { "damageType": [ 2 ] }, "4255001288": { "damageType": [ 4 ] }, "4258262391": { "damageType": [ 3 ] }, "4268030601": { "damageType": [ 4 ] } } ================================================ FILE: src/data/d2/exotic-to-catalyst-record.json ================================================ { "17096506": 3465971755, "19024058": 1182982189, "46524085": 494981303, "204878059": 2538406461, "219145368": 1144463862, "331231237": 3396973108, "347366834": 836516980, "370712896": 3381077691, "374573733": 3318895306, "400096939": 591600693, "417164956": 1060652297, "427899681": 2960086668, "603721696": 250211794, "648595258": 4235016462, "776191470": 1586771061, "814876685": 436556889, "940371471": 2479567609, "1034055198": 503301272, "1047932517": 207103968, "1111334348": 828471459, "1234150730": 3022631571, "1331482397": 1071947311, "1345867570": 1385469960, "1345867571": 3802151748, "1363238943": 3567687058, "1363886209": 1855685192, "1441805468": 1169793114, "1473821207": 1439993428, "1481594633": 2371798338, "1508896098": 2387948227, "1541131350": 2761319400, "1594120904": 2708727662, "1665952087": 1940645774, "1681583613": 3025950752, "1685137410": 1932245887, "1736135946": 1648672968, "1753923263": 2249784217, "1763584999": 263158944, "1802135586": 773758208, "1833195496": 478443982, "1853180924": 571025162, "1864563948": 3468043157, "1891561814": 173502661, "1912669214": 1048178161, "2044500762": 2880917375, "2084878005": 1514331782, "2130065553": 3835718947, "2179048386": 3574136388, "2208405142": 3518287681, "2232171099": 2254190310, "2286143274": 2856496392, "2350354266": 2744508352, "2357297366": 2744473468, "2362471601": 209320411, "2581676735": 2629249101, "2591746970": 3740374319, "2603483885": 1226048594, "2694576561": 310266584, "2812324401": 1163649614, "2816212794": 748675128, "2856683562": 4137195476, "2907129556": 2524364954, "2907129557": 2940589008, "2910326942": 2395817019, "2973900274": 1068904235, "3089417789": 373671280, "3118061005": 3787307395, "3121540812": 1450844310, "3141979346": 3589295049, "3141979347": 663317912, "3211806999": 1233471745, "3260753130": 1833569807, "3325463374": 2571854597, "3413074534": 1390549867, "3413860063": 3531533350, "3437746471": 1345348453, "3460576091": 2135780490, "3512014804": 3663964046, "3524313097": 1107018752, "3549153978": 3968841949, "3549153979": 3371896300, "3561203890": 3095531901, "3580904580": 206322165, "3580904581": 206322164, "3588934839": 3628100770, "3628991658": 4147054663, "3628991659": 2383994221, "3654674561": 15917031, "3659414143": 788492661, "3664831848": 1629497825, "3698448090": 812366033, "3725585710": 3393121279, "3761898871": 2329638530, "3766045777": 252263460, "3821409356": 1605993075, "3844694310": 3368860448, "3856705927": 1107121513, "3886719505": 3891168900, "3899270607": 299659704, "3973202132": 1461305554, "4017959782": 3644944303, "4036115577": 1547246830, "4068264807": 639604165, "4124984448": 3784038415, "4174431791": 185032465, "4190156464": 4178028503, "4255268456": 3746741161, "4284533075": 1094735064, "4289226715": 507778024, "4293613902": 2618920720 } ================================================ FILE: src/data/d2/exotics-with-catalysts.ts ================================================ const exoticWeaponHashesWithCatalyst = new Set([ 17096506, // Dragon's Breath 19024058, // Prometheus Lens 46524085, // Osteo Striga 204878059, // Malfeasance 219145368, // The Manticore 331231237, // Finality's Auger 347366834, // Ace of Spades 370712896, // Salvation's Grip 374573733, // Delicate Tomb 400096939, // Outbreak Perfected 417164956, // Jötunn 427899681, // Red Death Reformed 603721696, // Cryosthesia 77K 648595258, // Graviton Spike 776191470, // Tommy's Matchbook 814876685, // Trinity Ghoul 940371471, // Wicked Implement 1034055198, // Necrochasm 1047932517, // Slayer's Fang 1111334348, // Ice Breaker 1234150730, // Trespasser 1331482397, // MIDA Multi-Tool 1345867570, // Sweet Business 1345867571, // Coldheart 1363238943, // Ruinous Effigy 1363886209, // Gjallarhorn 1441805468, // The Navigator 1473821207, // Revision Zero 1481594633, // Barrow-Dyad 1508896098, // The Wardcliff Coil 1541131350, // Cerberus+1 1594120904, // No Time to Explain 1665952087, // The Fourth Horseman 1681583613, // Ergo Sum 1685137410, // Heirloom 1736135946, // New Malpais 1753923263, // Wolfsbane 1763584999, // Grand Overture 1802135586, // Touch of Malice 1833195496, // Ager's Scepter 1853180924, // Traveler's Chosen 1864563948, // Worldline Zero 1891561814, // Whisper of the Worm 1912669214, // Centrifuse 2044500762, // The Queenbreaker 2084878005, // Heir Apparent 2130065553, // Arbalest 2179048386, // Forerunner 2208405142, // Telesto 2232171099, // Deathbringer 2286143274, // The Huckleberry 2350354266, // Alethonym 2357297366, // Witherhoard 2362471601, // Rat King 2581676735, // New Land Beyond 2591746970, // Leviathan's Breath 2603483885, // Cloudstrike 2694576561, // Two-Tailed Fox 2812324401, // Dead Messenger 2816212794, // Bad Juju 2856683562, // SUROS Regime 2907129556, // Sturm 2907129557, // Sunshot 2910326942, // Wish-Keeper 2973900274, // Third Iteration 3089417789, // Riskrunner 3118061005, // Vexcalibur 3121540812, // Final Warning 3141979346, // D.A.R.C.I. 3141979347, // Borealis 3211806999, // Izanagi's Burden 3260753130, // Ticuu's Divination 3325463374, // Thunderlord 3413074534, // Polaris Lance 3413860063, // Lord of Wolves 3437746471, // Crimson 3460576091, // Duality 3512014804, // Lumina 3524313097, // Eriana's Vow 3549153978, // Fighting Lion 3549153979, // The Prospector 3561203890, // Tessellation 3580904580, // Legend of Acrius 3580904581, // Tractor Cannon 3588934839, // Le Monarque 3628991658, // Graviton Lance 3628991659, // Vigilance Wing 3654674561, // Dead Man's Tale 3659414143, // Verglas Curve 3664831848, // Heartshadow 3698448090, // Choir of One 3725585710, // Lodestar 3761898871, // Lorentz Driver 3766045777, // Black Talon 3821409356, // Ex Diris 3844694310, // The Jade Rabbit 3856705927, // Hawkmoon 3886719505, // Buried Bloodline 3899270607, // The Colony 3973202132, // Thorn 4017959782, // Symmetry 4036115577, // Sleeper Simulant 4068264807, // Monte Carlo 4124984448, // Hard Light 4174431791, // Hierarchy of Needs 4190156464, // Merciless 4255268456, // Skyburner's Oath 4284533075, // Service of Luzaku 4289226715, // Vex Mythoclast 4293613902, // Quicksilver Storm ]); export default exoticWeaponHashesWithCatalyst; ================================================ FILE: src/data/d2/extended-breaker.json ================================================ { "17096506": 3178805705, "89619507": 3178805705, "449318888": 485622768, "503025963": 485622768, "511888814": 2611060930, "1003391927": 485622768, "1047932517": 2611060930, "1203306857": 3178805705, "1351987111": 3178805705, "1443166262": 485622768, "1685137410": 3178805705, "1734844650": 3178805705, "1955548646": 485622768, "2110100341": 3178805705, "2415768376": 3178805705, "2474293653": 485622768, "2687811401": 2611060930, "2694576561": 2611060930, "3118061004": 2611060930, "3487253372": 485622768, "3598649636": 485622768, "4029987901": 2611060930, "4268030601": 485622768 } ================================================ FILE: src/data/d2/extended-foundry.json ================================================ { "2307365": "suros", "4425887": "fotc", "20025671": "suros", "52790208": "suros", "74733286": "cassoid", "105164264": "nadir", "135971347": "cassoid", "137879537": "fotc", "153979396": "fotc", "153979397": "fotc", "153979399": "fotc", "157601190": "nadir", "177568179": "nadir", "190747610": "cassoid", "191996029": "fotc", "192784503": "cassoid", "195440257": "fotc", "205225492": "omolon", "253196586": "fotc", "276918162": "fotc", "303107619": "cassoid", "337893613": "cassoid", "339163900": "fotc", "358788212": "cassoid", "408440598": "fotc", "413901114": "nadir", "417100299": "nadir", "417474225": "cassoid", "425681240": "nadir", "471764396": "cassoid", "496556698": "cassoid", "499245245": "cassoid", "501345268": "nadir", "566740455": "suros", "580961571": "fotc", "586671776": "nadir", "614140575": "hakke", "627188188": "cassoid", "632597698": "hakke", "705774642": "cassoid", "717150101": "cassoid", "751880654": "cassoid", "781498181": "nadir", "792755504": "fotc", "806021398": "fotc", "807192446": "fotc", "819358961": "fotc", "834081972": "fotc", "888872889": "cassoid", "930590127": "fotc", "990416096": "fotc", "1048266744": "fotc", "1050806815": "veist", "1099433612": "fotc", "1120843239": "fotc", "1136510727": "omolon", "1137768695": "fotc", "1161276682": "fotc", "1161561386": "nadir", "1172884782": "hakke", "1177293325": "omolon", "1177293326": "omolon", "1177293327": "omolon", "1189790632": "fotc", "1200414607": "fotc", "1211155739": "suros", "1281822856": "fotc", "1288422452": "cassoid", "1289324202": "hakke", "1292594730": "cassoid", "1293340523": "veist", "1296429091": "nadir", "1321626661": "cassoid", "1325579289": "fotc", "1377069894": "tex-mechanica", "1386601612": "suros", "1393021134": "cassoid", "1393021135": "cassoid", "1396813375": "fotc", "1401300690": "cassoid", "1443049976": "cassoid", "1453235079": "omolon", "1457979868": "field-forged", "1489452902": "fotc", "1529450902": "cassoid", "1531295694": "cassoid", "1547760589": "hakke", "1612781792": "nadir", "1619016919": "suros", "1644162710": "fotc", "1650442173": "cassoid", "1650626966": "cassoid", "1669771782": "cassoid", "1674742470": "fotc", "1678957657": "cassoid", "1719687748": "cassoid", "1723380073": "fotc", "1757129747": "hakke", "1760543913": "cassoid", "1773600468": "fotc", "1775804198": "suros", "1820994983": "cassoid", "1821724780": "suros", "1890332078": "cassoid", "1948035256": "cassoid", "1960218487": "fotc", "1974641289": "nadir", "1975125963": "cassoid", "1988218406": "cassoid", "1999754402": "nadir", "2009106091": "veist", "2014642399": "fotc", "2022294213": "nadir", "2039776723": "nadir", "2059255495": "cassoid", "2063217087": "cassoid", "2168486467": "fotc", "2171006181": "fotc", "2185327324": "suros", "2213848860": "cassoid", "2213848863": "cassoid", "2257180473": "cassoid", "2278995296": "fotc", "2287240026": "veist", "2290863050": "fotc", "2299191415": "tex-mechanica", "2351747819": "cassoid", "2422664927": "hakke", "2478247171": "cassoid", "2502422774": "cassoid", "2510526114": "nadir", "2581162758": "fotc", "2605790033": "cassoid", "2605790034": "omolon", "2611861926": "fotc", "2621637518": "fotc", "2658740569": "hakke", "2660862359": "fotc", "2693941407": "cassoid", "2694044461": "omolon", "2742838700": "fotc", "2759590322": "suros", "2763843899": "fotc", "2767393525": "cassoid", "2800870005": "nadir", "2812672356": "fotc", "2817798849": "suros", "2824241403": "hakke", "2826850739": "nadir", "2848549302": "cassoid", "2860172150": "cassoid", "2875763009": "cassoid", "2876244791": "cassoid", "2888266564": "hakke", "2891672170": "cassoid", "2898525497": "fotc", "2932922810": "cassoid", "2957542878": "fotc", "2961807684": "fotc", "3005879472": "fotc", "3009199534": "cassoid", "3040742682": "fotc", "3098328572": "veist", "3110698812": "tex-mechanica", "3163061743": "veist", "3185293912": "cassoid", "3190698551": "fotc", "3228993066": "hakke", "3246523831": "cassoid", "3337727085": "cassoid", "3356526253": "fotc", "3361694400": "cassoid", "3361694403": "omolon", "3383958219": "fotc", "3409645497": "cassoid", "3435238842": "cassoid", "3435238843": "cassoid", "3441197112": "cassoid", "3441197113": "cassoid", "3445437901": "fotc", "3461377698": "hakke", "3465233192": "cassoid", "3493948734": "fotc", "3505958430": "suros", "3529780349": "fotc", "3556730800": "cassoid", "3582424018": "fotc", "3583275737": "cassoid", "3637669759": "nadir", "3649173192": "fotc", "3662200188": "cassoid", "3662200189": "cassoid", "3751622019": "suros", "3757612024": "fotc", "3826803617": "fotc", "3889907763": "cassoid", "3890960908": "fotc", "3906357377": "cassoid", "3929685100": "fotc", "3998080529": "nadir", "4024037919": "fotc", "4041111172": "fotc", "4077588826": "cassoid", "4083045006": "fotc", "4105447486": "cassoid", "4118334987": "cassoid", "4138415949": "cassoid", "4138415950": "cassoid", "4148143418": "hakke", "4152016199": "nadir", "4157959956": "omolon", "4157959958": "omolon", "4157959959": "omolon", "4169225313": "nadir", "4174481098": "suros", "4176551594": "cassoid", "4176633581": "suros", "4186079026": "omolon", "4193877020": "fotc", "4200654067": "veist", "4209476803": "suros", "4230993599": "suros", "4238497225": "fotc", "4272442416": "fotc" } ================================================ FILE: src/data/d2/extended-ich.json ================================================ { "199733460": 55, "239189018": 55, "2213504923": 55, "2352138838": 55, "2390807586": 55, "2462335932": 55, "2545426109": 55, "3224066584": 55, "4095816113": 55 } ================================================ FILE: src/data/d2/focusing-item-outputs.json ================================================ { "944883": 3667553455, "2363653": 491078457, "9788213": 2376585692, "11400735": 2673925403, "17306485": 3849355625, "29482590": 2999866952, "33903049": 3440927677, "38855301": 4019668921, "40290223": 2641704939, "46196714": 1051949956, "52492571": 3074679271, "52790208": 2759590322, "55944745": 1725332125, "66527815": 791521235, "83965148": 2073794990, "85648155": 144007143, "90822968": 53292762, "91766129": 2821430069, "101824386": 423343404, "102374420": 2809120022, "104525723": 1606497639, "117057140": 3997086838, "122152503": 2883045518, "143601688": 3201140055, "152550357": 1959672649, "154412394": 409551876, "173152368": 3221722018, "175003048": 1987616650, "176225958": 690855456, "179758314": 2509918852, "183582177": 197672677, "189348890": 53126580, "199445735": 332170995, "201798442": 1719687748, "202522666": 2674680132, "203769430": 502173648, "205680223": 2349907931, "207548065": 1314666277, "218214716": 1661393678, "220051784": 3408834730, "220944791": 2233545123, "224319343": 2555349547, "234708547": 3813221631, "253950230": 469333264, "264580769": 3261527845, "264944861": 310708513, "279354191": 417100299, "279578169": 297009581, "284985776": 2009277538, "285956507": 3473290087, "293795567": 3207116971, "294557224": 1246793994, "296152704": 1896287986, "296378791": 108286515, "308335642": 1008364980, "316472157": 1702245537, "320408326": 2551549824, "324978278": 532168544, "325108472": 241291034, "325881855": 3110377595, "332192961": 4161591237, "332625175": 2248987683, "338446411": 18990920, "349137930": 900910756, "350442704": 125833536, "356432935": 2826850739, "362902214": 3730575552, "366078621": 3984883553, "366437642": 597039524, "366482347": 2876244791, "374248557": 456484913, "378242631": 1959285715, "380543246": 2766527864, "384236947": 2094927311, "384806842": 1265513556, "390263398": 3583853408, "394983260": 297586990, "397729467": 2580822279, "400987770": 3014775444, "405311520": 1254421266, "405672802": 523456396, "415227049": 4091317789, "420543798": 3146160048, "425782652": 978215630, "426311013": 3884544409, "428574585": 215768941, "434356905": 124944413, "447839375": 3791868619, "448147429": 274659609, "449889866": 4277137508, "476791533": 2320491441, "479072281": 1141927949, "480579364": 1807196134, "483006436": 3926811686, "487137411": 1740966079, "489274433": 39339589, "503025963": 1003391927, "508533863": 1144014195, "517186571": 876115223, "529088743": 2618417907, "547853538": 3150799884, "585603777": 3731378629, "588963177": 2513965917, "589585329": 2767393525, "593699562": 2889501828, "605025041": 341034325, "606510166": 2454114768, "609243450": 4146702548, "617836335": 4148887147, "618261657": 4119704973, "622764187": 2217519207, "629859204": 512777670, "638850307": 2935526975, "642983430": 4002939008, "646222889": 4238378141, "652077411": 768769183, "654848542": 3211624072, "666992026": 768145972, "667818643": 3737404623, "672369541": 273457849, "683374858": 480368036, "683632999": 143821939, "691707870": 4255001288, "704898505": 3829990714, "709164353": 1394633029, "716883942": 417977312, "717710567": 3352415987, "720153764": 3419310182, "733560210": 2749457084, "736550308": 2448259942, "737419447": 3118064579, "737428704": 1916287826, "740417776": 3568377122, "742707936": 2697143634, "755662092": 469005214, "756971831": 896081219, "759005343": 2016308379, "760706311": 2731689491, "764975752": 2891672170, "766089081": 1952223085, "767948183": 3971188131, "780496764": 2313726158, "781313686": 3684978064, "782991879": 2298039571, "787111830": 3299416208, "789415529": 1528655453, "790411442": 2100136540, "791810866": 2199554524, "792320746": 886273156, "801895599": 3824918251, "809709120": 3489657138, "812524306": 3115740538, "813239853": 1849901681, "814527257": 1803778317, "816003836": 2448907086, "819773640": 2870974250, "827599943": 2039776723, "837053731": 1215952756, "839115995": 3867373351, "850834881": 2022294213, "852885104": 1304266146, "854926798": 2498588344, "859789177": 2177224557, "865222334": 1392054568, "872190708": 557165046, "878104499": 2639921391, "879692448": 1299272338, "891829968": 162269058, "892020293": 1032041977, "894210265": 3481695565, "898459512": 2117889178, "917501872": 3721047650, "917820210": 891996636, "918317154": 4233375372, "930044848": 671504994, "940822787": 3406291173, "941282909": 1005315489, "949449009": 1709794165, "953703634": 3738678140, "959456403": 4224804453, "960363826": 1331824604, "962058923": 1586231351, "966848994": 138566412, "968289293": 3863492689, "986956153": 685165933, "987454021": 994038265, "1005621537": 1456017061, "1009072908": 38792606, "1009346008": 1346827258, "1011791524": 2522706952, "1012826548": 791799769, "1016565787": 657606375, "1017948510": 4114929480, "1024892034": 991314988, "1034058285": 1601177201, "1037962076": 4038429998, "1038737566": 3770087944, "1055088541": 3441081953, "1055356060": 435216110, "1059479825": 3651424085, "1059520422": 2132672800, "1062181600": 3864952146, "1064943697": 693067797, "1069478322": 792171868, "1079930198": 3187320016, "1087404526": 1788903768, "1096864740": 2615220262, "1100231689": 1354727549, "1103701935": 62937067, "1104664699": 2381515079, "1106028100": 119859462, "1120180351": 1765728763, "1125545953": 87665893, "1127009385": 925466717, "1137434578": 2772262012, "1138336561": 1537128821, "1140943142": 694275488, "1149271199": 438165659, "1165790188": 3591512190, "1166254911": 3507791931, "1169027282": 1028124540, "1192207416": 2851385306, "1193316520": 2653909130, "1195988317": 1479082657, "1200919036": 2195587150, "1205337961": 1412970845, "1217178015": 1429817243, "1220605797": 1734674241, "1221110111": 4207120603, "1224060546": 1001798188, "1232437646": 619556600, "1234612272": 3832743906, "1240056416": 1322042322, "1242518736": 2405271938, "1242556131": 2009892127, "1243425594": 1630402260, "1246618526": 882778888, "1249762845": 3426527713, "1256736028": 1266712686, "1256834041": 599049453, "1258973771": 2252757719, "1271294424": 4143415290, "1282098120": 3883286570, "1290536011": 852551895, "1294158516": 1183405878, "1294553754": 2651216180, "1317166018": 3552091116, "1343183965": 418363297, "1351987111": 89619507, "1356965425": 2800870005, "1366750484": 2993008662, "1370185563": 900910759, "1373116678": 1851521408, "1391595268": 960442438, "1393735405": 959037361, "1401276266": 372697604, "1408322960": 3428407746, "1413646678": 1207531728, "1416458672": 3768376418, "1418030349": 3437370193, "1428449496": 2492669178, "1436391764": 407148246, "1444163150": 1019000888, "1447074729": 435339366, "1455241180": 4035625646, "1458342358": 1586932304, "1459919588": 2867399974, "1462508868": 3959549446, "1469993244": 2395939950, "1470208358": 2099894368, "1470817006": 671423576, "1488099192": 2671639706, "1515561791": 3125454907, "1516075651": 4230993599, "1517474839": 959484195, "1520180304": 2002759682, "1527328167": 3179623987, "1529835198": 1046651176, "1538369423": 1006498763, "1540293887": 3545094011, "1549471512": 1524444346, "1551464092": 1893967086, "1552702991": 3565520715, "1562141193": 1854753405, "1564178759": 1613581523, "1568976227": 3922217119, "1569427840": 3932952562, "1578915183": 1796949035, "1585868599": 3341893443, "1588913887": 2655279451, "1593841906": 3611754012, "1599155324": 267960270, "1613585882": 1492522228, "1616416988": 1890332078, "1624161432": 4057875770, "1625916736": 132486706, "1629007932": 938618741, "1643656410": 2616071821, "1657208257": 2353371845, "1669660339": 1310207471, "1693103724": 4122447870, "1695992504": 3050017626, "1696802966": 2395788176, "1700920860": 1021381486, "1709963256": 1248530160, "1715740932": 1968711238, "1726065449": 3671337107, "1726944726": 796577360, "1730148720": 2510526114, "1732065014": 3166250992, "1734076444": 2385676654, "1740905008": 3601794530, "1747014693": 858758105, "1757424586": 1801007332, "1762083721": 1698660093, "1765702368": 3355385170, "1771461428": 3754408118, "1781607626": 2647299044, "1786438829": 3400283633, "1809794768": 845422466, "1816364190": 2300143112, "1828328977": 4225322581, "1838197241": 1727248109, "1842623742": 105164264, "1844990602": 3960926756, "1869654860": 4006721630, "1872688848": 1512386434, "1874604139": 327385975, "1874827456": 682107058, "1882570974": 943320520, "1891772651": 2713820407, "1891927373": 3998080529, "1892687578": 405059572, "1896335568": 1267361154, "1896884883": 2335208655, "1900451399": 2969535443, "1900794881": 1747063685, "1901196910": 2509940440, "1904726257": 53126581, "1911399760": 1821747970, "1912252717": 996946801, "1912716657": 777526069, "1913013889": 2914474757, "1923654025": 946526461, "1926701814": 308767728, "1929900447": 2857770907, "1937611370": 3232203524, "1937794711": 950745251, "1940028766": 3450721096, "1942251393": 3686538757, "1947463881": 499245245, "1951308312": 1446374842, "1954300473": 3767088557, "1955453921": 3063765221, "1960898552": 2237216282, "1961759511": 1788603939, "1965867309": 496728945, "1976108766": 2351038408, "1981520313": 2576944173, "1992194968": 1482105402, "1998829404": 676270382, "2000707632": 3987871714, "2001962619": 3991622471, "2002770452": 1852587798, "2003638177": 3292795429, "2015733300": 2447096246, "2016003826": 1737050140, "2016440717": 2078005457, "2020782603": 2938035991, "2028217633": 2496875173, "2029585884": 378498222, "2042885489": 1812185909, "2047507132": 2725889934, "2054874830": 132452792, "2055524983": 2151744259, "2057542406": 1233862528, "2059601679": 3275117874, "2060830238": 2680535688, "2062287346": 124786972, "2064212142": 3106439832, "2069829085": 1139897377, "2073348400": 222606050, "2077601806": 1260563064, "2077874558": 3104459624, "2085074569": 730200061, "2086016266": 1026828708, "2086437031": 1465405235, "2091225008": 714372706, "2094168185": 2368311917, "2110100341": 1203306857, "2111030142": 173787496, "2134085073": 1479465109, "2144437236": 4100775158, "2145353828": 157601190, "2150208209": 671664021, "2153298319": 1106635211, "2154456873": 2988121501, "2157486560": 3150852690, "2159167152": 4266736482, "2164131193": 4255586669, "2165423482": 1767106452, "2168728704": 81398002, "2175841482": 51129316, "2182452253": 4076604385, "2183966710": 2290416, "2187992932": 1107446438, "2197274028": 2627533758, "2204419101": 708549601, "2210194577": 3337759189, "2212837316": 2109877638, "2233602319": 1615052875, "2262455308": 2347145374, "2267066698": 155308388, "2271192476": 164221422, "2271876370": 2931145020, "2276310682": 2741247284, "2277184009": 546003581, "2279050881": 3642110725, "2281936901": 1911060537, "2292621312": 1394979698, "2293707032": 230314682, "2300822101": 1724366537, "2307892413": 2943943681, "2310598311": 3250247987, "2311186393": 2188974669, "2312764638": 860168648, "2328599478": 2966741808, "2341107225": 23061005, "2349487312": 1567525250, "2354898110": 3465233192, "2358069650": 2999584444, "2362430352": 266021826, "2373970243": 216983039, "2381507358": 511773064, "2387444228": 2348198214, "2397722856": 1066605642, "2398130369": 2591257541, "2398693243": 3849754183, "2400813307": 1407595207, "2410737925": 2399134521, "2413746134": 1076810832, "2417743858": 1740888860, "2423294139": 1687353095, "2427081642": 1177873348, "2430486902": 507400048, "2432853034": 4144123204, "2439112524": 1168370782, "2445219609": 1547183373, "2450060488": 298824490, "2461218970": 3641592628, "2461407681": 411397829, "2467867662": 3977387640, "2472122753": 1200256005, "2474293653": 4268030601, "2485608945": 1769846965, "2492527550": 684658728, "2495124174": 1332123064, "2496572186": 2770768564, "2510227202": 727872428, "2528318435": 3612142623, "2529663586": 3702689847, "2532975303": 641063251, "2535975195": 3423777255, "2551249699": 2181242591, "2570717855": 3678653083, "2602975238": 3414625920, "2605320510": 735535272, "2607072210": 1922402428, "2609207620": 3238797830, "2615541747": 3364817839, "2617594621": 1141547457, "2628207034": 1127943297, "2631606272": 2591241074, "2640011174": 2896109856, "2642969363": 2850664015, "2648146174": 2247299560, "2650869989": 2387737625, "2651894107": 230524071, "2660082239": 3862185275, "2660725645": 3763521327, "2663848874": 4291183556, "2664742934": 3261819408, "2664825549": 3451283089, "2664994076": 2899275886, "2666283098": 358788212, "2668199515": 3608857511, "2669950408": 1654117930, "2677528310": 3934586864, "2687811401": 4029987901, "2691087957": 146223817, "2701632970": 1637326795, "2710865799": 2796252563, "2718271926": 3504336176, "2720725271": 433905699, "2721354951": 2557210451, "2724714408": 1953621386, "2730887956": 3293524502, "2733913394": 2255073244, "2733974969": 597831725, "2742898269": 806374817, "2748982312": 921467658, "2754445092": 1218113510, "2755263039": 3106557243, "2756838942": 575830664, "2768502727": 4144779347, "2769684630": 3958993808, "2772236585": 2447741853, "2810404581": 4154074649, "2815070534": 3781549120, "2817346460": 2143618030, "2818658597": 3583275737, "2826830765": 4058198769, "2832727864": 888791258, "2843072129": 3132885765, "2849111819": 3273631255, "2851422678": 2340803152, "2851870940": 4057580974, "2852649264": 1903182562, "2853425267": 931507503, "2854025240": 3741471003, "2854976081": 1981173269, "2859670382": 3219256792, "2861374485": 3692827145, "2871721702": 1612781792, "2879998696": 1325838922, "2880450203": 3688534631, "2889504953": 1413409069, "2898525497": 3356526253, "2902329623": 4039932861, "2905536492": 391384020, "2905700895": 2251614491, "2912463567": 3749277835, "2928349050": 605724052, "2930629196": 4150060382, "2945177393": 1210111349, "2947742646": 2408514352, "2950694244": 1431190694, "2950782681": 771104589, "2957859219": 2478547407, "2967958507": 3570900471, "2968238583": 2405271939, "2974942014": 4269346472, "2976734344": 3651039338, "2978078741": 171695625, "2981276707": 2716373471, "2986361676": 925466718, "2989732283": 3631862279, "2997378201": 864224653, "2997491826": 2066074780, "2998475016": 508515818, "3006177242": 728668916, "3010462832": 4156676002, "3013156849": 863908533, "3021987691": 754069623, "3029570245": 32287609, "3034681814": 3091776080, "3041739793": 371726421, "3041947774": 687386728, "3054751781": 3153956825, "3063795377": 1329892341, "3067591475": 2105414511, "3074765785": 178749005, "3076072398": 1948035256, "3085357864": 213264394, "3086027926": 3617849232, "3106353652": 2645763830, "3112088101": 1276048857, "3114052816": 1381405058, "3119153287": 2075703443, "3122430471": 1403800851, "3125381141": 3860977161, "3139852187": 368733543, "3142178519": 3768376419, "3144750103": 2936230179, "3157594934": 2295273904, "3161246618": 3589022772, "3173816072": 3598520298, "3174721418": 3342536996, "3181759866": 175511444, "3187046535": 3328811155, "3187910761": 2072548445, "3188702156": 2534676958, "3189110091": 2347178967, "3191684715": 4258262391, "3200105099": 2566006935, "3200434396": 3009199534, "3204561957": 1630000089, "3215932179": 863163983, "3217523599": 1975125963, "3223924017": 2832013173, "3224735856": 1108278178, "3228993066": 2888266564, "3237668309": 2094233929, "3248496910": 2102025592, "3251175769": 2443507405, "3254804681": 3947966653, "3256047672": 2147175386, "3259487516": 844443758, "3272445958": 2002452096, "3281352910": 1602717624, "3282450867": 2344353519, "3287081371": 120441703, "3290572354": 1028582252, "3293679620": 938218310, "3296963846": 42874240, "3298006530": 3776430252, "3299723351": 117457123, "3300245016": 2932922810, "3310241668": 3747088838, "3310367410": 2845947996, "3318677781": 1571959827, "3323319039": 4287857019, "3337944718": 2097219064, "3342199133": 1249787553, "3346371707": 4152016199, "3357460865": 4184605701, "3357737661": 2553961985, "3359876034": 3303502828, "3368700430": 436615288, "3373908478": 1909470440, "3382832422": 2355943840, "3384059383": 3761819011, "3392240079": 2738060683, "3403264960": 2328531378, "3406420059": 1611241895, "3420177387": 615681527, "3422602782": 3982932616, "3424709896": 617501162, "3426900336": 355382946, "3434863786": 4201843274, "3436721716": 2848549302, "3437505629": 1637623713, "3437969024": 533855986, "3446623301": 4061292537, "3450156559": 963710795, "3453261711": 4255171531, "3459488441": 1084190509, "3460423082": 3258665412, "3462471786": 3793245444, "3464883458": 2304861612, "3468506669": 5159537, "3469063381": 2732820041, "3469351144": 3281673290, "3471717205": 2565829065, "3475683133": 1856777089, "3484903008": 3775606738, "3486679768": 1060317946, "3487993338": 3403647252, "3489910822": 1505862304, "3498657110": 2195467472, "3500791139": 3436626079, "3506304844": 48110686, "3510230835": 1009385327, "3512588219": 1811579911, "3513036502": 1602419024, "3527930483": 2666189615, "3533534867": 2671565007, "3534797310": 188882152, "3537285454": 2821677368, "3538579376": 91690594, "3538991101": 4010324161, "3539478895": 3883286571, "3541993780": 664109750, "3546314515": 2273643087, "3548628854": 2915174768, "3553325799": 2717305289, "3553875570": 2135836316, "3555286847": 267672635, "3556063855": 821154603, "3557821742": 1978467312, "3557991772": 2472090414, "3562567620": 1674833286, "3566338354": 3487011804, "3570710433": 3960926757, "3572799518": 4086100104, "3577148367": 3638406027, "3577777240": 3545094010, "3588527091": 198854575, "3599713355": 3597838551, "3600661228": 3251195262, "3607545635": 1148781279, "3617033640": 2096711562, "3617994312": 2612856746, "3618831903": 2856514843, "3629641744": 3106222402, "3639816879": 606902507, "3640152843": 357179927, "3649173192": 253196586, "3650390908": 1938324686, "3651818922": 212971972, "3656122215": 304385651, "3663176799": 2321064411, "3663305082": 2439379860, "3664957074": 2218948028, "3668693055": 4060352315, "3671588056": 1960663290, "3675599216": 2231910050, "3688579389": 2405271937, "3705854184": 1437375562, "3705995974": 3797729472, "3706702514": 925466716, "3711090475": 2286507447, "3716635620": 1069002790, "3717480197": 1741360441, "3717864060": 751880654, "3729506850": 1000646860, "3731724843": 932578999, "3732924936": 1884301546, "3739947031": 3694642467, "3742527360": 4032295410, "3743691434": 3624199242, "3744426107": 867154247, "3749148603": 77816839, "3753382047": 1148460187, "3755738975": 486861531, "3761864384": 511709874, "3762760128": 3753063346, "3765731481": 539816333, "3769720823": 2013313923, "3771606505": 1283202269, "3776165335": 3962575203, "3776837386": 2050789284, "3784881871": 1080431755, "3796222038": 3909729744, "3802805445": 1256804729, "3809684989": 2381337281, "3809979407": 1179169099, "3812440761": 2820165421, "3815376960": 2949791538, "3820433909": 2152484073, "3825975800": 3838077978, "3826782839": 3719480067, "3827474196": 2767482390, "3827697821": 1011882337, "3828502736": 233635202, "3844627809": 368733541, "3847138256": 2389585538, "3848039961": 1805830669, "3850234099": 3517179757, "3852825844": 390139894, "3856300708": 447296102, "3864386550": 649524976, "3870549112": 3252994970, "3876012135": 1659679091, "3880653774": 2046130360, "3881082126": 1914589560, "3883022513": 1779796469, "3884696647": 736382419, "3887291648": 4200122994, "3891912205": 868732497, "3895829948": 1578461326, "3898129411": 1182214975, "3910013888": 4165710258, "3920299599": 2751189771, "3923407559": 1045743955, "3924951683": 1737518783, "3930115709": 875848769, "3939284543": 2540765499, "3940601453": 2026755633, "3946759767": 60193507, "3947737539": 3637669759, "3950759129": 2119346509, "3957453475": 4177778015, "3959223835": 3139221735, "3960749708": 2715240478, "3960914123": 113167959, "3966240957": 2002759681, "3967888097": 2035738085, "3969198262": 2574019120, "3970937914": 1854337492, "3982212757": 2602559881, "3989100739": 1054377087, "3992290300": 133666382, "4001764690": 2322926844, "4005993978": 1466284308, "4021652784": 459833058, "4021927132": 3722981806, "4023039160": 4077588826, "4025410673": 3875549237, "4027344350": 4021402824, "4028427880": 3969379530, "4039633424": 2852052802, "4049241596": 3649264718, "4050536705": 2437632645, "4061367591": 3605603507, "4066300387": 4156253727, "4076975873": 1056103557, "4079614900": 2810038838, "4084143604": 4139274998, "4096757830": 2629204288, "4100036523": 1586892599, "4113712338": 4157369212, "4119202577": 852430165, "4120943058": 1854753404, "4130251279": 12440395, "4131549575": 2342054803, "4132121470": 1203306856, "4141497761": 1269679141, "4147463420": 1910323534, "4155923794": 1435559164, "4155947523": 1884579135, "4163796751": 3182840395, "4175277987": 1168370783, "4178321775": 1262430251, "4181859860": 3949234966, "4193598888": 3218364298, "4195360985": 3691455821, "4198182657": 154944645, "4222584181": 3570243433, "4223536259": 1793346751, "4224297358": 1495869176, "4229934495": 4161524635, "4236667838": 3821828136, "4237588213": 1880357865, "4241589952": 725297842, "4242416109": 859709617, "4245694331": 2321258055, "4273167814": 598995392, "4273639393": 3615421669, "4274637770": 3050442980, "4280633975": 996109059, "4287959325": 1259299553 } ================================================ FILE: src/data/d2/generated-enums.ts ================================================ export const enum PlugCategoryHashes { AmmoPerk = 4048143046, ArmorArchetypes = 778194869, ArmorSkinsEmpty = 3356843615, ArmorSkinsHunterArms = 4060972748, ArmorSkinsHunterChest = 4147546868, ArmorSkinsHunterClass = 1392996619, ArmorSkinsHunterExoticArms = 47799639, ArmorSkinsHunterExoticChest = 2149118857, ArmorSkinsHunterExoticClass = 619147446, ArmorSkinsHunterExoticHead = 284384596, ArmorSkinsHunterExoticLegs = 3110440717, ArmorSkinsHunterHead = 3952105943, ArmorSkinsHunterLegs = 1608828634, ArmorSkinsSharedHead = 2241987364, ArmorSkinsTitanArms = 3386643992, ArmorSkinsTitanChest = 3945759584, ArmorSkinsTitanClass = 2415787951, ArmorSkinsTitanExoticArms = 3918879531, ArmorSkinsTitanExoticChest = 1035149405, ArmorSkinsTitanExoticClass = 2575566674, ArmorSkinsTitanExoticHead = 4027746304, ArmorSkinsTitanExoticLegs = 1227326993, ArmorSkinsTitanHead = 3955281547, ArmorSkinsTitanLegs = 2112278838, ArmorSkinsWarlockArms = 3934361071, ArmorSkinsWarlockChest = 1509135441, ArmorSkinsWarlockClass = 505602046, ArmorSkinsWarlockExoticArms = 2247821314, ArmorSkinsWarlockExoticChest = 2584704302, ArmorSkinsWarlockExoticClass = 3844954073, ArmorSkinsWarlockExoticHead = 2107797925, ArmorSkinsWarlockExoticLegs = 63463744, ArmorSkinsWarlockHead = 454950060, ArmorSkinsWarlockLegs = 3281006437, ArmorStats = 748854354, Arrows = 1257608559, Barrels = 2833605196, Batteries = 1757026848, Blades = 1041766312, Bolts = 2540157675, Bowstrings = 3809303875, BuildPerk = 1760165654, Catalysts = 2142400139, CoreGearSystemsArmorTieringPlugsTuningMods = 3481777685, CoreGearSystemsEventGearItemSetsSelectors = 1313063513, CraftingPlugsFrameIdentifiers = 3425085882, CraftingPlugsWeaponsModsEnhancers = 711031169, CraftingPlugsWeaponsModsExtractors = 3520412733, CraftingPlugsWeaponsModsMemories = 2748073883, CraftingPlugsWeaponsModsTransfusersLevel = 1716719962, CraftingRecipesEmptySocket = 3618704867, DawningShipShader = 2492152783, DawningShipSpawnfx = 1767518647, Deprecated = 3247958962, DummyInfuse = 1654900552, EmblemPerk = 734522343, EmblemVariant = 3993099034, Emote = 3054419239, EnhancementsActivity = 1202876185, EnhancementsArms = 640682011, EnhancementsArtifact = 284811516, EnhancementsArtifice = 3773173029, EnhancementsArtificeExotic = 1934732343, EnhancementsChest = 383756333, EnhancementsClass = 1955304674, EnhancementsExoticAeonCult = 913734466, EnhancementsGhostsActivity = 1232333242, EnhancementsGhostsActivityFake = 623624742, EnhancementsGhostsEconomic = 2544257478, EnhancementsGhostsExperience = 2112073485, EnhancementsGhostsTracking = 1102742598, EnhancementsHead = 744326128, EnhancementsLegs = 1175552225, EnhancementsRaidDescent = 1703496685, EnhancementsRaidGarden = 1486918022, EnhancementsRaidV520 = 2274750776, EnhancementsRaidV600 = 2207493141, EnhancementsRaidV620 = 2173937871, EnhancementsRaidV700 = 2106680364, EnhancementsRaidV720 = 2140235634, EnhancementsRaidV800 = 125494331, EnhancementsRivensCurse = 2149155760, EnhancementsSeasonForge = 65589297, EnhancementsSeasonMaverick = 1081029832, EnhancementsSeasonOpulence = 2712224971, EnhancementsSeasonOutlaw = 13646368, EnhancementsSeasonPenumbra = 1962317640, EnhancementsUniversal = 3347429529, EnhancementsV2Arms = 3422420680, EnhancementsV2Chest = 1526202480, EnhancementsV2ClassItem = 912441879, EnhancementsV2General = 2487827355, EnhancementsV2Head = 2912171003, EnhancementsV2Legs = 2111701510, EventsDawningItV950IngredientA = 592218309, EventsDawningItV950IngredientB = 592218310, EventsDawningItV950NoRecipe = 1154320798, EventsDawningItV950OvenCombine = 1139677991, EventsDawningItV950OvenEmpty = 59064119, EventsDawningItV950OvenEmptyCombination = 2431328339, EventsDawningItV950OvenNotMasterworked = 507973944, EventsDawningItV950Recipe = 1294542606, EventsDawningOvenMasterworked = 2791193194, ExoticAllSkins = 3940152116, ExoticNewAutoRifle0Skins = 3325876128, ExoticNewAutoRifle1Skins = 2618920869, ExoticNewFusionRifle0Skins = 1427116567, ExoticNewGrenadeLauncher0Skins = 1915768453, ExoticNewGrenadeLauncher1Skins = 1177794816, ExoticNewHandCannon0Skins = 3795555851, ExoticNewHandCannon1Skins = 495200286, ExoticNewPulseRifle0Skins = 3024976096, ExoticNewPulseRifle1Skins = 2677573861, ExoticNewRocketLauncher0Skins = 3779357109, ExoticNewScoutRifle0Skins = 3053839327, ExoticNewShotgun0Skins = 2002113342, ExoticNewShotgun1Skins = 3115415723, ExoticNewSidearm1Skins = 2362123322, ExoticNewSniperRifle1Skins = 418027885, ExoticNewSubmachinegun1Skins = 4073971430, ExoticRepackageAutoRifle0Skins = 1822847543, ExoticRepackageScoutRifle0Skins = 3028606398, ExoticWeaponMasterworkUpgrade = 1932084301, Frames = 7906839, GenericAllVfx = 2019022937, GenericExoticMasterwork = 4138517961, GhostTrackerLeft = 3611137032, GhostTrackerRight = 2841059071, Grips = 3962145884, Guards = 683359327, Hafts = 1697972157, HolofoilSkinsShared = 1756031332, Hologram = 2272427828, HunterArcAspects = 185594100, HunterArcClassAbilities = 3956119552, HunterArcMelee = 2434874031, HunterArcMovement = 2101241798, HunterArcSupers = 4145425829, HunterPrismAspects = 1164816619, HunterPrismClassAbilities = 3324969927, HunterPrismGrenades = 2789335173, HunterPrismMelee = 511532732, HunterPrismMovement = 1681184239, HunterPrismPrismGrenade = 418351300, HunterPrismSupers = 180411040, HunterPrismTranscendence = 2415732645, HunterSharedAspects = 3032847657, HunterSolarAspects = 3052104375, HunterSolarClassAbilities = 3538316507, HunterSolarMelee = 4225254304, HunterSolarMovement = 3752921107, HunterSolarSupers = 3151809860, HunterStasisClassAbilities = 641408223, HunterStasisMelee = 3530064820, HunterStasisMovement = 1929408791, HunterStasisSupers = 818442312, HunterStasisTotems = 1853189378, HunterStrandAspects = 3805562622, HunterStrandClassAbilities = 2552562702, HunterStrandMelee = 3873313773, HunterStrandMovement = 1979332108, HunterStrandSupers = 144959979, HunterVoidAspects = 2905530840, HunterVoidClassAbilities = 3673640204, HunterVoidMelee = 1288993259, HunterVoidMovement = 1796328914, HunterVoidSupers = 2613010961, IntermediatePlugThatWorksInEveryCategory = 248344053, Intrinsics = 1744546145, LegendaryCrucibleCompetitivePulseRifle0Skins = 885533112, Magazines = 1806783418, MagazinesGl = 2718120384, Mementos = 4181669225, Mods = 3313201758, Origins = 164955586, PlugsGhostsMasterworks = 4096629092, PlugsMasterworksArmorDefault = 2457930460, PlugsMasterworksWeaponsDefault = 782502718, Rails = 2565854612, RandomPerk = 1135709122, SchismBoonsDestinationModsEfficiency = 3424523197, SchismBoonsDestinationModsInfo = 3096991326, SchismBoonsDestinationModsPlaystyle = 484496731, Scopes = 2619833294, Shader = 2973005342, SharedArcFragments = 2430016289, SharedArcGrenades = 404070091, SharedFragments = 1920373979, SharedPrismFragments = 2696330562, SharedSolarFragments = 3119191718, SharedSolarGrenades = 3369359206, SharedStasisGrenades = 900498880, SharedStasisTrinkets = 83940941, SharedStrandFragments = 685964393, SharedStrandGrenades = 2831653331, SharedVoidFragments = 39076551, SharedVoidGrenades = 3089520417, ShipSpawnfx = 3189353766, SocialClansPerks = 3898156960, SocialClansStaves = 3954618873, StatusEffectTooltip = 475911769, Stocks = 577918720, TitanArcAspects = 3460332466, TitanArcClassAbilities = 1281712906, TitanArcMelee = 1458470025, TitanArcMovement = 2415307576, TitanArcSupers = 1861253111, TitanPrismAspects = 912150793, TitanPrismClassAbilities = 3820930681, TitanPrismGrenades = 3205146347, TitanPrismMelee = 1422809918, TitanPrismMovement = 3777887553, TitanPrismPrismGrenade = 2370651390, TitanPrismSupers = 902963970, TitanPrismTranscendence = 742077731, TitanSharedAspects = 4032445539, TitanSolarAspects = 1970675705, TitanSolarClassAbilities = 1197336009, TitanSolarMelee = 605941486, TitanSolarMovement = 379285521, TitanSolarSupers = 2850085618, TitanStasisClassAbilities = 826897697, TitanStasisMelee = 3693308166, TitanStasisMovement = 3711066169, TitanStasisSupers = 635737914, TitanStasisTotems = 1491608144, TitanStrandAspects = 323641540, TitanStrandClassAbilities = 2480042224, TitanStrandMelee = 3826855743, TitanStrandMovement = 2139679542, TitanStrandSupers = 1080622901, TitanVoidAspects = 3990226434, TitanVoidClassAbilities = 3366817658, TitanVoidMelee = 4008726361, TitanVoidMovement = 1924069544, TitanVoidSupers = 3468785159, Tubes = 1202604782, V300ArmorSkinsHunterCrucibleArms0 = 2622950634, V300ArmorSkinsHunterCrucibleChest0 = 390592286, V300ArmorSkinsHunterCrucibleClass0 = 4015186727, V300ArmorSkinsHunterCrucibleHead0 = 1122092347, V300ArmorSkinsHunterCrucibleLegs0 = 1662162564, V300ArmorSkinsHunterDeadOrbitArms0 = 2259726502, V300ArmorSkinsHunterDeadOrbitChest0 = 3321067338, V300ArmorSkinsHunterDeadOrbitClass0 = 3491850843, V300ArmorSkinsHunterDeadOrbitHead0 = 2284282463, V300ArmorSkinsHunterDeadOrbitLegs0 = 1777179040, V300ArmorSkinsHunterFutureWarCultArms0 = 1074816138, V300ArmorSkinsHunterFutureWarCultChest0 = 4136114878, V300ArmorSkinsHunterFutureWarCultClass0 = 3890096199, V300ArmorSkinsHunterFutureWarCultHead0 = 3869028315, V300ArmorSkinsHunterFutureWarCultLegs0 = 114028196, V300ArmorSkinsHunterIronBannerArms0 = 2234756112, V300ArmorSkinsHunterIronBannerChest0 = 422812216, V300ArmorSkinsHunterIronBannerClass0 = 3113273813, V300ArmorSkinsHunterIronBannerHead0 = 2762971033, V300ArmorSkinsHunterIronBannerLegs0 = 2593391466, V300ArmorSkinsHunterNewMonarchyArms0 = 910610427, V300ArmorSkinsHunterNewMonarchyChest0 = 4132091733, V300ArmorSkinsHunterNewMonarchyClass0 = 3254040744, V300ArmorSkinsHunterNewMonarchyHead0 = 444154002, V300ArmorSkinsHunterNewMonarchyLegs0 = 3655617901, V300ArmorSkinsHunterTrialsArms0 = 3815310902, V300ArmorSkinsHunterTrialsArms1 = 3815310903, V300ArmorSkinsHunterTrialsChest0 = 3944217850, V300ArmorSkinsHunterTrialsChest1 = 3944217851, V300ArmorSkinsHunterTrialsClass0 = 3592671179, V300ArmorSkinsHunterTrialsClass1 = 3592671178, V300ArmorSkinsHunterTrialsHead0 = 1701755567, V300ArmorSkinsHunterTrialsHead1 = 1701755566, V300ArmorSkinsHunterTrialsLegs0 = 732725904, V300ArmorSkinsHunterTrialsLegs1 = 732725905, V300ArmorSkinsHunterVanguardArms0 = 2233776131, V300ArmorSkinsHunterVanguardChest0 = 705275341, V300ArmorSkinsHunterVanguardClass0 = 3155730624, V300ArmorSkinsHunterVanguardHead0 = 4291368762, V300ArmorSkinsHunterVanguardLegs0 = 284143029, V300ArmorSkinsTitanCrucibleArms0 = 3375811294, V300ArmorSkinsTitanCrucibleChest0 = 3812107282, V300ArmorSkinsTitanCrucibleClass0 = 112380963, V300ArmorSkinsTitanCrucibleHead0 = 7433047, V300ArmorSkinsTitanCrucibleLegs0 = 4219754424, V300ArmorSkinsTitanDeadOrbitArms0 = 2990888754, V300ArmorSkinsTitanDeadOrbitChest0 = 2255507318, V300ArmorSkinsTitanDeadOrbitClass0 = 2009591839, V300ArmorSkinsTitanDeadOrbitHead0 = 2319384163, V300ArmorSkinsTitanDeadOrbitLegs0 = 3816584524, V300ArmorSkinsTitanFutureWarCultArms0 = 2151056630, V300ArmorSkinsTitanFutureWarCultChest0 = 1210155450, V300ArmorSkinsTitanFutureWarCultClass0 = 434151307, V300ArmorSkinsTitanFutureWarCultHead0 = 3866331247, V300ArmorSkinsTitanFutureWarCultLegs0 = 3363335760, V300ArmorSkinsTitanIronBannerArms0 = 2006101700, V300ArmorSkinsTitanIronBannerChest0 = 4076359100, V300ArmorSkinsTitanIronBannerClass0 = 1533259569, V300ArmorSkinsTitanIronBannerHead0 = 3330258549, V300ArmorSkinsTitanIronBannerLegs0 = 3936747758, V300ArmorSkinsTitanNewMonarchyArms0 = 2088592431, V300ArmorSkinsTitanNewMonarchyChest0 = 608268681, V300ArmorSkinsTitanNewMonarchyClass0 = 2388571972, V300ArmorSkinsTitanNewMonarchyHead0 = 2860196350, V300ArmorSkinsTitanNewMonarchyLegs0 = 2238635089, V300ArmorSkinsTitanTrialsArms0 = 4183985634, V300ArmorSkinsTitanTrialsArms1 = 4183985635, V300ArmorSkinsTitanTrialsChest0 = 128174022, V300ArmorSkinsTitanTrialsChest1 = 128174023, V300ArmorSkinsTitanTrialsClass0 = 446165423, V300ArmorSkinsTitanTrialsClass1 = 446165422, V300ArmorSkinsTitanTrialsHead0 = 2354538195, V300ArmorSkinsTitanTrialsHead1 = 2354538194, V300ArmorSkinsTitanTrialsLegs0 = 1933164060, V300ArmorSkinsTitanTrialsLegs1 = 1933164061, V300ArmorSkinsTitanVanguardArms0 = 1200170135, V300ArmorSkinsTitanVanguardChest0 = 3582003425, V300ArmorSkinsTitanVanguardClass0 = 494101820, V300ArmorSkinsTitanVanguardHead0 = 1803432646, V300ArmorSkinsTitanVanguardLegs0 = 2584350649, V300ArmorSkinsWarlockCrucibleArms0 = 3021386333, V300ArmorSkinsWarlockCrucibleChest0 = 852400867, V300ArmorSkinsWarlockCrucibleClass0 = 4046191066, V300ArmorSkinsWarlockCrucibleHead0 = 3457072516, V300ArmorSkinsWarlockCrucibleLegs0 = 1050468095, V300ArmorSkinsWarlockDeadOrbitArms0 = 2035062749, V300ArmorSkinsWarlockDeadOrbitChest0 = 648471395, V300ArmorSkinsWarlockDeadOrbitClass0 = 3842261594, V300ArmorSkinsWarlockDeadOrbitHead0 = 2470748932, V300ArmorSkinsWarlockDeadOrbitLegs0 = 64144511, V300ArmorSkinsWarlockFutureWarCultArms0 = 3294793707, V300ArmorSkinsWarlockFutureWarCultChest0 = 2203061285, V300ArmorSkinsWarlockFutureWarCultClass0 = 1325010296, V300ArmorSkinsWarlockFutureWarCultHead0 = 3955252354, V300ArmorSkinsWarlockFutureWarCultLegs0 = 91583997, V300ArmorSkinsWarlockIronBannerArms0 = 3898104757, V300ArmorSkinsWarlockIronBannerChest0 = 1706193451, V300ArmorSkinsWarlockIronBannerClass0 = 62909634, V300ArmorSkinsWarlockIronBannerHead0 = 2543330620, V300ArmorSkinsWarlockIronBannerLegs0 = 1179259543, V300ArmorSkinsWarlockNewMonarchyArms0 = 2653495584, V300ArmorSkinsWarlockNewMonarchyChest0 = 578857064, V300ArmorSkinsWarlockNewMonarchyClass0 = 2280954437, V300ArmorSkinsWarlockNewMonarchyHead0 = 567119273, V300ArmorSkinsWarlockNewMonarchyLegs0 = 1348435162, V300ArmorSkinsWarlockTrialsArms0 = 3634502413, V300ArmorSkinsWarlockTrialsArms1 = 3634502412, V300ArmorSkinsWarlockTrialsChest0 = 2569729971, V300ArmorSkinsWarlockTrialsChest1 = 2569729970, V300ArmorSkinsWarlockTrialsClass0 = 480188650, V300ArmorSkinsWarlockTrialsClass1 = 480188651, V300ArmorSkinsWarlockTrialsHead0 = 467130100, V300ArmorSkinsWarlockTrialsHead1 = 467130101, V300ArmorSkinsWarlockTrialsLegs0 = 10334159, V300ArmorSkinsWarlockTrialsLegs1 = 10334158, V300ArmorSkinsWarlockVanguardArms0 = 1457854612, V300ArmorSkinsWarlockVanguardChest0 = 2116093612, V300ArmorSkinsWarlockVanguardClass0 = 519781601, V300ArmorSkinsWarlockVanguardHead0 = 718754501, V300ArmorSkinsWarlockVanguardLegs0 = 1321135774, V300GhostsModsPerks = 1820735122, V300NewAutoRifle0Masterwork = 3499970538, V300NewAutoRifle1Masterwork = 1928281557, V300NewFusionRifle0Masterwork = 960424623, V300NewGrenadeLauncher0Masterwork = 657092405, V300NewGrenadeLauncher1Masterwork = 4225663562, V300NewHandCannon0Masterwork = 746309217, V300NewHandCannon1Masterwork = 4249350918, V300NewPulseRifle0Masterwork = 520351532, V300NewPulseRifle1Masterwork = 3306482479, V300NewRocketLauncher0Masterwork = 1019850379, V300NewScoutRifle0Masterwork = 1359704841, V300NewShotgun0Masterwork = 3582972386, V300NewShotgun1Masterwork = 1940410957, V300NewSidearm1Masterwork = 1703860058, V300NewSniperRifle0Masterwork = 15335102, V300NewSniperRifle0Skins = 2679189543, V300NewSniperRifle1Masterwork = 1167690265, V300NewSubmachinegun1Masterwork = 277452006, V300PlugsMasterworksGenericArmorSuperDr = 3547196371, V300PlugsMasterworksGenericWeaponsKills = 2109207426, V300PlugsMasterworksGenericWeaponsKillsPvp = 2989652629, V300RepackageAutoRifle0Masterwork = 4057741607, V300RepackageScoutRifle0Masterwork = 3953106578, V300VehiclesModControls = 1417830429, V300VehiclesModFunction = 3536704835, V300VehiclesModSpeed = 231630128, V300WeaponDamageTypeAttack = 674045876, V300WeaponDamageTypeEnergy = 1466776700, V300WeaponDamageTypeKinetic = 405287501, V310ArmorSkinsHunterRaidArms0 = 3558667098, V310ArmorSkinsHunterRaidChest0 = 45580206, V310ArmorSkinsHunterRaidClass0 = 363674743, V310ArmorSkinsHunterRaidHead0 = 941442667, V310ArmorSkinsHunterRaidLegs0 = 520171668, V310ArmorSkinsTitanRaidArms0 = 3819569148, V310ArmorSkinsTitanRaidChest0 = 2208487012, V310ArmorSkinsTitanRaidClass0 = 3376774585, V310ArmorSkinsTitanRaidHead0 = 3167006477, V310ArmorSkinsTitanRaidLegs0 = 2822497030, V310ArmorSkinsWarlockRaidArms0 = 3304137951, V310ArmorSkinsWarlockRaidChest0 = 920543769, V310ArmorSkinsWarlockRaidClass0 = 2276286420, V310ArmorSkinsWarlockRaidHead0 = 1854477166, V310ArmorSkinsWarlockRaidLegs0 = 2225388193, V310NewAutoRifle0Masterwork = 3469209931, V310NewAutoRifle0Skins = 392238056, V310NewGrenadeLauncher0Masterwork = 1724403784, V310NewGrenadeLauncher0Skins = 3898287421, V310NewHandCannon0Masterwork = 2307840402, V310NewHandCannon0Skins = 3429262643, V310RepackageFusionRifle0Masterwork = 3467500035, V310RepackageFusionRifle0Skins = 1324640144, V310RepackageScoutRifle0Masterwork = 2523049293, V310RepackageScoutRifle0Skins = 1814947446, V320ArmorSkinsHunterRaidArms0 = 2428652891, V320ArmorSkinsHunterRaidChest0 = 3333494197, V320ArmorSkinsHunterRaidClass0 = 2031088776, V320ArmorSkinsHunterRaidHead0 = 2003876466, V320ArmorSkinsHunterRaidLegs0 = 878693069, V320ArmorSkinsTitanRaidArms0 = 2147095959, V320ArmorSkinsTitanRaidChest0 = 2941021153, V320ArmorSkinsTitanRaidClass0 = 4148086844, V320ArmorSkinsTitanRaidHead0 = 2750358470, V320ArmorSkinsTitanRaidLegs0 = 3531276473, V320ArmorSkinsWarlockRaidArms0 = 839011368, V320ArmorSkinsWarlockRaidChest0 = 2871629216, V320ArmorSkinsWarlockRaidClass0 = 1798887645, V320ArmorSkinsWarlockRaidHead0 = 2180883025, V320ArmorSkinsWarlockRaidLegs0 = 1474419586, V320NewScoutRifle0Masterwork = 1377184563, V320NewScoutRifle0Skins = 3215270144, V320NewSubmachinegun0Masterwork = 3943131211, V320NewSubmachinegun0Skins = 977490152, V320NewSword0Masterwork = 110665061, V320NewSword0Skins = 2415152446, V320RepackageAutoRifle0Masterwork = 3430012101, V320RepackageAutoRifle0Skins = 2839835678, V320RepackageFusionRifle0Masterwork = 3550935996, V320RepackageFusionRifle0Skins = 4051921705, V320RepackageSniperRifle0Masterwork = 449551925, V320RepackageSniperRifle0Skins = 1587544718, V350ParadeArmsGlow = 3597584499, V350ParadeChestGlow = 1828648477, V350ParadeClassGlow = 3578767056, V350ParadeHeadGlow = 3639363810, V350ParadeLegsGlow = 1118991365, V350PlugsArmorSkinsHunterBrokenArms0 = 305985703, V350PlugsArmorSkinsHunterBrokenChest0 = 1761529041, V350PlugsArmorSkinsHunterBrokenClass0 = 3532501484, V350PlugsArmorSkinsHunterBrokenHead0 = 3396859350, V350PlugsArmorSkinsHunterBrokenLegs0 = 3778216169, V350PlugsArmorSkinsHunterMendedArms0 = 2702647783, V350PlugsArmorSkinsHunterMendedChest0 = 175015441, V350PlugsArmorSkinsHunterMendedClass0 = 2370445356, V350PlugsArmorSkinsHunterMendedHead0 = 1498554134, V350PlugsArmorSkinsHunterMendedLegs0 = 1455453481, V350PlugsArmorSkinsTitanBrokenArms0 = 2445912777, V350PlugsArmorSkinsTitanBrokenChest0 = 1396923599, V350PlugsArmorSkinsTitanBrokenClass0 = 3068436574, V350PlugsArmorSkinsTitanBrokenHead0 = 1982109304, V350PlugsArmorSkinsTitanBrokenLegs0 = 2737854619, V350PlugsArmorSkinsTitanMendedArms0 = 2394562569, V350PlugsArmorSkinsTitanMendedChest0 = 785959695, V350PlugsArmorSkinsTitanMendedClass0 = 2074695070, V350PlugsArmorSkinsTitanMendedHead0 = 2355319736, V350PlugsArmorSkinsTitanMendedLegs0 = 2645030875, V350PlugsArmorSkinsWarlockBrokenArms0 = 3964401570, V350PlugsArmorSkinsWarlockBrokenChest0 = 2479342214, V350PlugsArmorSkinsWarlockBrokenClass0 = 3221791087, V350PlugsArmorSkinsWarlockBrokenHead0 = 2176427667, V350PlugsArmorSkinsWarlockBrokenLegs0 = 2179614172, V350PlugsArmorSkinsWarlockMendedArms0 = 1945108766, V350PlugsArmorSkinsWarlockMendedChest0 = 2816127314, V350PlugsArmorSkinsWarlockMendedClass0 = 3452944995, V350PlugsArmorSkinsWarlockMendedHead0 = 2871594647, V350PlugsArmorSkinsWarlockMendedLegs0 = 2364491256, V400ActivitiesGambitAutoRifle0Skins = 2155610010, V400ActivitiesGambitHandCannon0Skins = 1391357461, V400ActivitiesGambitPulseRifle0Skins = 2169149398, V400ActivitiesGambitRocketLauncher0Skins = 3171701971, V400ActivitiesGambitScoutRifle0Skins = 462794349, V400ActivitiesGambitShotgun0Skins = 4003974204, V400ActivitiesGambitSniperRifle0Skins = 614701202, V400ActivitiesGambitSubmachinegun0Skins = 3191795937, V400DestinationsTangledShoreShotgun0Skins = 3933191773, V400EmptyExoticMasterwork = 1915962497, V400EndgameQuestsBow0Skins = 1667999665, V400EndgameQuestsGrenadeLauncher0Skins = 3123633626, V400EndgameQuestsSidearm1Skins = 4260943415, V400FactionsVanguardFusionRifle0Skins = 3890097133, V400NewAutoRifle0Masterwork = 2176772846, V400NewAutoRifle0Skins = 1065976756, V400NewBow0Masterwork = 1212416230, V400NewBow0Skins = 84363132, V400NewBow1Masterwork = 255771725, V400NewBow1Skins = 813579777, V400NewFusionRifle0Masterwork = 2245960787, V400NewFusionRifle0Skins = 1857231011, V400NewHandCannon0Masterwork = 1227928911, V400NewHandCannon0Skins = 1792965495, V400NewRocketLauncher0Masterwork = 3780723205, V400NewRocketLauncher0Skins = 1976875945, V400NewSword0Masterwork = 806845881, V400NewSword0Skins = 1735079805, V400NewTraceRifle0Masterwork = 3885086550, V400NewTraceRifle0Skins = 3613020796, V400PlugsArmorMasterworksStatResistance1 = 1514141502, V400PlugsArmorMasterworksStatResistance2 = 1514141501, V400PlugsArmorMasterworksStatResistance3 = 1514141500, V400PlugsArmorMasterworksStatResistance4 = 1514141499, V400PlugsWeaponsMasterworks = 3185182717, V400PlugsWeaponsMasterworksStatAccuracy = 1238043140, V400PlugsWeaponsMasterworksStatBlastRadius = 1847616696, V400PlugsWeaponsMasterworksStatChargeTime = 2827428737, V400PlugsWeaponsMasterworksStatDamage = 2458812152, V400PlugsWeaponsMasterworksStatDrawTime = 482070447, V400PlugsWeaponsMasterworksStatHandling = 199786516, V400PlugsWeaponsMasterworksStatHeatEfficiency = 2437126983, V400PlugsWeaponsMasterworksStatPersistence = 854547368, V400PlugsWeaponsMasterworksStatProjectileSpeed = 2321551094, V400PlugsWeaponsMasterworksStatRange = 1392237582, V400PlugsWeaponsMasterworksStatReload = 717646604, V400PlugsWeaponsMasterworksStatStability = 1762223024, V400PlugsWeaponsMasterworksStatVentSpeed = 2876802050, V400PlugsWeaponsMasterworksTrackers = 2947756142, V400QuestsBow1Skins = 2797012568, V400RepackageHandCannon0Masterwork = 2621598992, V400RepackageHandCannon0Skins = 485644850, V400RepackageLinearFunsionRifle0Skins = 4050850050, V400RepackageLinearFusionRifle0Masterwork = 971045988, V400RepackageShotgun0Masterwork = 2610390651, V400RepackageShotgun0Skins = 2164270611, V400RepackageShotgun1Masterwork = 883741900, V400RepackagedShotgun1Skins = 350272236, V400WeaponModDamage = 974142739, V400WeaponModEmpty = 3945646995, V400WeaponModGuns = 510594033, V400WeaponModMagazine = 4087004818, V404ArmorFotlMasksAbyssPerks = 457704685, V404RepackageMachinegun0Masterwork = 363161694, V404RepackageMachinegun0Skins = 4273350564, V410ActivitiesBlackArmoryMachinegun0Skins = 3642094098, V410ActivitiesCrimsonBow0Skins = 1522299082, V410ActivitiesPinnacleFusionRifle0Skins = 4011742748, V410NewBow0Masterwork = 3801286525, V410NewBow0Skins = 976994985, V410NewGrenadeLauncher0Masterwork = 3601510960, V410NewGrenadeLauncher0Skins = 892144706, V410NewScoutRifle0Masterwork = 674719636, V410NewScoutRifle0Skins = 518948974, V410NewSniperRifle0Masterwork = 536688691, V410NewSniperRifle0Skins = 1969656843, V410RepackageHandCannon0Masterwork = 12126723, V410RepackageHandCannon0Skins = 2011181443, V420CruciblePulseRifle0Skins = 2321975333, V420HunterHead0Skins = 999254, V420MambaAutoRifle0Skins = 428199408, V420MambaGrenadeLauncher0Skins = 602227925, V420MambaHandCannon0Skins = 2267118235, V420MambaPulseRifle0Skins = 3862889328, V420MambaScoutRifle0Skins = 2929135407, V420MambaShotgun0Skins = 235518158, V420MambaSidearm0Skins = 4024114567, V420MambaSniperRifle0Skins = 1635489496, V420MambaSubmachinegun0Skins = 1995795395, V420NewFusionRifle0Skins = 3016261433, V420NewLinearFusionRifle0Masterwork = 1790848693, V420PinnacleMachinegun0Skins = 3876317651, V420PinnacleScoutRifle0Skins = 1783525359, V420PinnacleSubmachinegun0Skins = 2450539651, V420PlugsWeaponsMasterworksToggleVfx = 318988241, V420RepackageHandCannon0Masterwork = 3640971302, V420RepackageHandCannon0Skins = 2419369188, V420RepackagePulseRifle0Masterwork = 1739429903, V420RepackagePulseRifle0Skins = 1475038983, V420TitanHead0Skins = 3089276162, V420WarlockHead0Skins = 1241518579, V450ActivitiesCaluseumFusionRifle0Skins = 1408248247, V450ActivitiesCaluseumHandCannon0Skins = 1125118891, V450ActivitiesCaluseumMachinegun0Skins = 1977936579, V450ActivitiesCaluseumShotgun0Skins = 945668126, V450ActivitiesCaluseumSidearm0Skins = 1485843447, V450ActivitiesCaluseumSniperRifle0Skins = 899036872, V450ActivitiesCaluseumSubmachinegun0Skins = 3823441427, V450ActivitiesPinnacleBow0Skins = 2724631945, V450ActivitiesPinnacleGrenadeLauncher0Skins = 4284125154, V450ActivitiesPinnacleSniperRifle0Skins = 1866435883, V450NewHandCannon0Masterwork = 1534847920, V450NewHandCannon0Skins = 3090164258, V450NewSubmachinegun0Masterwork = 2931436176, V450NewSubmachinegun0Skins = 2452466562, V450RepackagePulseRifle0Masterwork = 70110646, V450RepackagePulseRifle0Skins = 3256117508, V450RepackageRocketLauncher0Masterwork = 129782381, V450RepackageRocketLauncher0Skins = 3471393313, V450SummerArmsGlow = 20929282, V450SummerChestGlow = 4238689326, V450SummerClassGlow = 439601311, V450SummerHeadGlow = 1635000747, V450SummerLegsGlow = 4238448668, V460ActivitiesSeasonPassAutoRifle0Skins = 4020011895, V460ActivitiesSeasonPassMachinegun0Skins = 1722030980, V460NewBow0Masterwork = 3311305128, V460NewBow0Skins = 1746205530, V460NewHandCannon0Masterwork = 3212906233, V460NewHandCannon0Skins = 2786376477, V460NewMachinegun0Masterwork = 4118756145, V460NewMachinegun0Skins = 3708089573, V460NewRocketLauncher0Masterwork = 1688857955, V460NewRocketLauncher0Skins = 44451851, V460NewTraceRifle0Masterwork = 3243981472, V460NewTraceRifle0Skins = 3268904306, V460PlugsArmorMasterworks = 2198080209, V460PlugsArmorMasterworksStatResistance1 = 400813040, V460PlugsArmorMasterworksStatResistance2 = 400813043, V460PlugsArmorMasterworksStatResistance3 = 400813042, V460PlugsArmorMasterworksStatResistance4 = 400813045, V460PlugsArmorMasterworksStatResistance5 = 400813044, V460RepackageAutoRifle0Masterwork = 2732333749, V460RepackageAutoRifle0Skins = 341105385, V460RepackageRocketLauncher0Masterwork = 2830931184, V460WeaponModSword = 680447589, V470ActivitiesIronBannerBow0Skins = 361926520, V470ActivitiesSeasonPassRocketLauncher0Skins = 2094479447, V470ActivitiesSeasonPassSniperRifle0Skins = 607917374, V470EventsDawningSubmachinegun0Skins = 1336807992, V470NewFusionRifle0Masterwork = 2086437224, V470NewFusionRifle0Skins = 643943386, V470NewScoutRifle0Masterwork = 4058834, V470NewScoutRifle0Skins = 3333180416, V470NewSidearm0Masterwork = 1764823814, V470NewSidearm0Skins = 3145470924, V480NewAutoRifle0Masterwork = 1365020630, V480NewAutoRifle0Skins = 221142412, V480NewMachinegun0Masterwork = 3883869807, V480NewMachinegun0Skins = 2780862839, V480PursuitShotgun0Skins = 2185442450, V480RepackageShotgun0Masterwork = 464940035, V480RepackageShotgun0Skins = 1703293515, V480SeasonPassShotgun0Skins = 835520543, V480SeasonPassSubmachinegun0Skins = 419074638, V490NewGrenadeLauncher0Masterwork = 4238615496, V490NewGrenadeLauncher0Skins = 3158134826, V490NewSidearm0Masterwork = 4133961368, V490NewSidearm0Skins = 1064681482, V490NewTraceRifle0Masterwork = 769288861, V490NewTraceRifle0Skins = 2619144553, V490WeaponsActivitiesSeasonPassPulseRifle0Skins = 3428111374, V490WeaponsActivitiesSeasonPassSword0Skins = 1440710215, V500NewGrenadeLauncher0Masterwork = 4002498006, V500NewGrenadeLauncher0Skins = 1144349164, V500NewRocketLauncher0Masterwork = 163781180, V500NewRocketLauncher0Skins = 635831614, V500NewShotgun0Masterwork = 3621127437, V500NewShotgun0Skins = 4188773649, V500NewSniperRifle0Masterwork = 326456325, V500NewSniperRifle0Skins = 977729529, V500NewSword0Masterwork = 1571696116, V500NewSword0Skins = 1322133870, V500PursuitSniperRifle0Skins = 1062900453, V500RepackageHandCannon0Masterwork = 3140088685, V500RepackageHandCannon0Skins = 3354669985, V500RepackagePulseRifle0Masterwork = 3370425072, V500RepackagePulseRifle0Skins = 939817746, V500ShipsEventsDawningExoticShip0Engines = 3754191634, V510NewBow0Masterwork = 2855954680, V510NewBow0Skins = 2920760650, V510NewScoutRifle0Skins = 392596597, V510PursuitGrenadeLauncher0Skins = 3928246899, V520NewSidearm0Masterwork = 1334864500, V520NewSidearm0Skins = 3585770670, V520PursuitFusionRifle0Skins = 2234989376, V520RepackageFusionRifle0Masterwork = 1951259483, V520RepackageFusionRifle0Skins = 3764575515, V530NewLinearFusionRifle0Masterwork = 196432975, V530NewLinearFusionRifle0Skins = 2706864319, V530NewTraceRifle0Masterwork = 2975259730, V530NewTraceRifleSkins = 1307969312, V530PursuitRocketLauncher0Skins = 2974188781, V540NewHandCannon0Masterwork = 3379994138, V540NewHandCannon0Skins = 1671564328, V540RepackageRocketLauncher0Masterwork = 3305750067, V540RepackageRocketLauncher0Skins = 3752242995, V540WeaponModConfetti = 1265510581, V600NewGlaiveHunterMasterwork = 4031794373, V600NewGlaiveTitanMasterwork = 1751014081, V600NewGlaiveWarlockMasterwork = 1103244178, V600NewGrenadeLauncher0Masterwork = 4237179951, V600NewGrenadeLauncher0Skins = 3668060823, V600NewGrenadeLauncher1Masterwork = 2207504144, V600NewGrenadeLauncher1Skins = 2344200762, V600NewMachinegun0Masterwork = 1427548885, V600NewMachinegun0Skins = 2266129105, V600NewPulseRifle0Masterwork = 3098024512, V600NewPulseRifle0Skins = 2310078298, V600NewSubmachinegun0Skins = 1697359525, V600PlugsWeaponsMasterworksStatShieldDuration = 1210640601, V600PursuitShotgun0Skins = 2835787192, V610NewMachinegun0Skins = 425188854, V610NewSidearm0Masterwork = 2765279178, V610NewSidearm0Skins = 214872328, V610NewSword0Masterwork = 2543210952, V610NewSword0Skins = 3670567962, V620ExoticWeaponMasterwork = 503826300, V620NewAutoRifle0Skins = 2920012332, V620NewFusionRifle0Skins = 964101403, V620PursuitGrenadeLauncher0Skins = 1006653369, V620RepackageScoutRifle0Skins = 2565240946, V630NewBow0Masterwork = 1131152613, V630NewBow0Skins = 970213409, V630NewPulseRifle0Skins = 3446925989, V630NewSubmachinegun0Masterwork = 725540660, V630NewSubmachinegun0Skins = 1942616782, V630PursuitPulseRifle0Skins = 3180604129, V700ExoticWeaponMasterwork = 4109527333, V700NewBow0Skins = 1242165521, V700NewGlaive0Skins = 2359272137, V700NewGlaive1Skins = 1043851684, V700NewMachinegun0Skins = 1750087276, V700NewShotgun0Skins = 2557113039, V700NewSidearm0Skins = 236268474, V700PursuitGlaive0Skins = 1980932117, V700WeaponsModsMissionAvalon = 709861688, V710NewAutoRifle0Masterwork = 1602267206, V710NewAutoRifle0Skins = 3118667164, V710NewScoutRifle0Masterwork = 2944685043, V710NewScoutRifle0Skins = 2078000051, V710NewTraceRifle0Masterwork = 880719790, V710NewTraceRifle0Skins = 2775050468, V710PursuitScoutRifle0Skins = 657820579, V720NewFusionRifle0Masterwork = 1306261452, V720NewFusionRifle0Skins = 129564502, V720NewGrenadeLauncher0Masterwork = 3535138282, V720NewGrenadeLauncher0Skins = 1250777624, V720PursuitHandcannon0Skins = 3351746883, V720RepackageAutoRifle0Masterwork = 282796296, V720RepackageAutoRifle0Skins = 3113397682, V730NewBow0Skins = 3480541086, V730NewSidearm0Masterwork = 4103783549, V730NewSidearm0Skins = 3206497209, V730PursuitSword0Skins = 4166989435, V730RepackageRocketLauncher0Masterwork = 3867121356, V730RepackageRocketLauncher0Skins = 3831742870, V800NewAutoRifle0Skins = 3362902088, V800NewGrenadeLauncher0Masterwork = 11234841, V800NewLinearFusionRifle0Masterwork = 3297970367, V800NewLinearFusionRifle0Skins = 2833377167, V800NewSniperRifle0Masterwork = 1615215818, V800NewSniperRifle0Skins = 889298944, V800NewSword0Masterwork = 1231552077, V800NewSword0Skins = 1819523641, V800NewTraceRifle0Masterwork = 2837543682, V800NewTraceRifle0Skins = 2673070336, V800PursuitSubmachinegun0Skins = 4042778950, V800RepackageAutoRifle0Masterwork = 1144321079, V800RepackageAutoRifle0Skins = 1166372799, V800RepackagePulseRifle0Masterwork = 606228473, V800RepackagePulseRifle0Skins = 794172773, V800RepackageSniperRifle0Masterwork = 2456910795, V810NewGrenadeLauncher0Skins = 216123502, V810NewShotgun0Skins = 2093338915, V810PursuitAutoRifle0Skins = 4116933935, V810RepackageSniperRifle0Skins = 1428267180, V820NewLinearFusionRifle0Masterwork = 3150802305, V820NewLinearFusionRifle0Skins = 1322048781, V820NewPulseRifle0Masterwork = 56525756, V820NewPulseRifle0Skins = 382969102, V820NewSubmachinegun0Skins = 2748353481, V820PursuitFusionRifle0Skins = 700552526, V820RepackageSniperRifle0Masterwork = 2705359473, V820RepackageSniperRifle0Skins = 3948052341, V900CrucibleHandCannon0Skins = 2742828941, V900CruciblePulseRifle0Skins = 3137270494, V900CrucibleShotgun0Skins = 1427248756, V900CrucibleSidearm0Skins = 3042297661, V900FireteamBow0Skins = 4230280310, V900FireteamLinearFusionRifle0Skins = 2165401497, V900FireteamSubmachinegun0Skins = 3584817293, V900FireteamSword0Skins = 3924991979, V900KeplerAutoRifle0Skins = 1606765948, V900KeplerHandCannon0Skins = 1810816159, V900KeplerMachinegun0Skins = 1772231047, V900KeplerPulseRifle0Skins = 768914020, V900KeplerScoutRifle0Skins = 3933502995, V900KeplerShotgun0Skins = 743754162, V900NewHandCannon0Skins = 3256463360, V900NewRocketLauncher0Masterwork = 3222373880, V900NewRocketLauncher0Skins = 1962134082, V900NewScoutRifle0Masterwork = 980492902, V900NewScoutRifle0Skins = 752486508, V900PinnacleBow0Skins = 729711807, V900PinnacleFusionRifle0Skins = 1763926114, V900PinnacleGrenadeLauncher0Skins = 1492251348, V900PinnacleGrenadeLauncher1Skins = 1087163257, V900PinnaclePulseRifle0Skins = 3097272591, V900RaidAutoRifle0Skins = 630327265, V900RaidBow0Skins = 2117497331, V900RaidFusionRifle0Skins = 2188287822, V900RaidRocketLauncher0Skins = 2478444458, V900RaidSniperRifle0Skins = 3901240637, V900RaidSubmachinegun0Skins = 3906375868, V900SeasonalAutoRifle0Skins = 1483634785, V900SeasonalScoutRifle0Skins = 858256116, V900SeasonalSidearm0Skins = 731928384, V900SeasonalSniperRifle0Skins = 3012516285, V900SeasonalSword0Skins = 1273066882, V900SharedHandCannon0Skins = 1067507917, V900SharedShotgun0Skins = 2956217780, V900SharedSword0Skins = 3044073815, V900SharedTraceRifle0Skins = 3802997954, V900SolsticeBow0Skins = 789453099, V900SolsticeGrenadeLauncher0Skins = 3895906184, V900SolsticeSubmachinegun0Skins = 1899383508, V900TrialsAutoRifle0Skins = 2908858734, V900TrialsHandCannon0Skins = 3790481121, V900TrialsMachinegun0Skins = 3662647465, V900TrialsScoutRifle0Skins = 1660208313, V900TrialsSidearm0Skins = 4061496193, V900WeaponModMagAdjusting = 3056511964, V900weaponModConfetti = 461937739, V910CrucibleShotgun0Skins = 2241760711, V910FireteamSidearm0Skins = 377466082, V910FotlBow0Skins = 3771433223, V910FotlShotgun0Skins = 670204241, V910FotlSubmachinegun0Skins = 4261144664, V910IronBannerHandCannon0Skins = 3179622974, V910NewPulseRifle0Masterwork = 3717742928, V910NewPulseRifle0Skins = 3567853450, V910NewSword0Skins = 1147736243, V910PinnacleScoutRifle0Skins = 948944743, V910PinnacleSubmachinegun0Skins = 2821625291, V910RaidAutoRifle0Skins = 1573678764, V910RaidGrenadeLauncher0Skins = 4244696809, V910RaidShotgun0Skins = 2911785122, V910RallyBow0Skins = 2302851568, V910SeasonalRocketLauncher0Skins = 3607804553, V910SeasonalSidearm0Skins = 1922051427, V910SeasonalSubmachinegun0Skins = 2476775551, V910SharedGrenadeLauncher0Skins = 2673163978, V910SharedRocketLauncher0Skins = 2777687964, V910TrialsAutoRifle0Skins = 1756408975, V910TrialsFusionRifle0Skins = 507759744, V950CrucibleFusionRifle0Skins = 1435837052, V950CrucibleHandCannon0Skins = 3282809834, V950CrucibleMachinegun0Skins = 4147069320, V950CrucibleSubmachinegun0Skins = 3748892522, V950DawningGrenadeLauncher0Skins = 3577158843, V950DawningLinearFusionRifle0Skins = 1390451709, V950DawningSidearm0Skins = 2130471037, V950DungeonAutoRifle0Skins = 1032896694, V950DungeonMachinegun0Skins = 3341746145, V950DungeonPulseRifle0Skins = 3253502346, V950DungeonScoutRifle0Skins = 1538436721, V950DungeonSniperRifle0Skins = 3381155862, V950DungeonSword0Skins = 2392064691, V950ExpansionHandCannon0Skins = 2429354874, V950ExpansionHandCannon1Skins = 3753214935, V950ExpansionPulseRifle0Skins = 1891500865, V950ExpansionSidearm0Skins = 1601675014, V950ExpansionSniperRifle0Skins = 2009770575, V950ExpansionSubmachinegun0Skins = 2162765114, V950FireteamFusionRifle0Skins = 3632244732, V950FireteamPulseRifle0Skins = 605166065, V950FireteamSidearm0Skins = 939443990, V950FireteamSniperRifle0Skins = 1428998495, V950GuardianGamesFusionRifle0Skins = 2651686574, V950GuardianGamesHandCannon0Skins = 3624687000, V950GuardianGamesTraceRifle0Skins = 3833883083, V950IronBannerAutoRifle0Skins = 1425916923, V950NewBow0Masterwork = 556715872, V950NewBow0Skins = 264094498, V950NewMachinegun0Masterwork = 406976985, V950NewMachinegun0Skins = 186307901, V950NewSword0Blades = 3761829271, V950NewSword0Cosmetics = 2322407058, V950NewSword0Forms = 4113079231, V950NewSword0Guards = 190208416, V950NewSword0Masterwork = 2306593303, V950NewSword0PerkUpgrades = 1217981202, V950NewSword0Skins = 494458223, V950NewSword0StatUpgrades = 804736300, V950PinnacleAutoRifle0Skins = 4220403404, V950PinnacleBow0Skins = 2652645684, V950PinnacleHandCannon0Skins = 4111710831, V950PinnaclePulseRifle0Skins = 367148788, V950PinnacleScoutRifle0Skins = 2900230627, V950RallyGrenadeLauncher0Skins = 2820901201, V950SeasonalFusionRifle0Skins = 2612108191, V950SeasonalGrenadeLauncher0Skins = 3676640669, V950SeasonalShotgun0Skins = 2166243462, V950SeasonalSword0Skins = 3614854585, V950SharedBow0Skins = 1439251285, V950SharedGlaive0Skins = 964507541, V950SharedRocketLauncher0Skins = 1041486592, V950SharedTraceRifle0Skins = 2326137949, V950TrialsGrenadeLauncher0Skins = 141111862, V950TrialsHandCannon0Skins = 2601497498, V950TrialsPulseRifle0Skins = 975813985, V950TrialsShotgun0Skins = 2225584379, V950TrialsSubmachinegun0Skins = 1881974426, WarlockArcAspects = 2111409167, WarlockArcClassAbilities = 1308084083, WarlockArcMelee = 1387605624, WarlockArcMovement = 1943502171, WarlockArcSupers = 2285394316, WarlockPrismAspects = 769886388, WarlockPrismClassAbilities = 3339135424, WarlockPrismGrenades = 3287837048, WarlockPrismMelee = 204703343, WarlockPrismMovement = 2883193222, WarlockPrismPrismGrenade = 1341484747, WarlockPrismSupers = 1684765285, WarlockPrismTranscendence = 1121283194, WarlockSharedAspects = 3908861120, WarlockSolarAspects = 81856188, WarlockSolarClassAbilities = 1662395848, WarlockSolarMelee = 2822977079, WarlockSolarMovement = 1763298974, WarlockSolarSupers = 2997411645, WarlockStasisClassAbilities = 1960796738, WarlockStasisMelee = 4031311265, WarlockStasisMovement = 1191502208, WarlockStasisSupers = 3379648287, WarlockStasisTotems = 2997725741, WarlockStrandAspects = 2557935615, WarlockStrandClassAbilities = 2200902275, WarlockStrandMelee = 3904090216, WarlockStrandMovement = 3728449707, WarlockStrandSupers = 1774026300, WarlockVoidAspects = 227647633, WarlockVoidClassAbilities = 3202031457, WarlockVoidMelee = 2900030790, WarlockVoidMovement = 3427909241, WarlockVoidSupers = 4141244538, WeaponTieringKillVfx = 2339465330, WeaponTieringTier5Skins = 1092466115, } export const enum StatHashes { Accuracy = 1591432999, AimAssistance = 1345609583, AirborneEffectiveness = 2714457168, AmmoCapacity = 925767036, AmmoGeneration = 1931675084, AnyEnergyTypeCost = 3578062600, ArcCost = 3779394102, ArcDamageResistance = 1546607978, ArmorEnergyCapacity_16120457 = 16120457, ArmorEnergyCapacity_2018193158 = 2018193158, ArmorEnergyCapacity_2441327376 = 2441327376, ArmorEnergyCapacity_3625423501 = 3625423501, ArmorEnergyCapacity_3950461274 = 3950461274, AspectEnergyCapacity = 2223994109, Attack = 1480404414, BlastRadius = 3614673599, Boost = 3017642079, ChargeRate = 3022301683, ChargeTime = 2961396640, Class = 1943323491, ClassDupe = 2135857333, CoolingEfficiency = 4006394725, Defense = 3897883278, DrawTime = 447667954, Durability = 360359141, FragmentCost = 119204074, GhostEnergyCapacity = 237763788, Grenade = 1735777505, GuardEfficiency = 2762071195, GuardEndurance = 3736848092, GuardResistance = 209426660, Handicap = 2341766298, Handling = 943549884, Health = 392767087, HeatGenerated = 3481294762, HeroicResistance = 1546607977, Impact = 4043523819, Magazine = 3871231066, Melee = 4244567218, MeleeDupe = 3493869314, ModCost = 514071887, MoveSpeed = 3907551967, Persistence = 3085395333, Power = 1935470627, PowerBonus = 3289069874, PrecisionDamage = 3597844532, Range = 1240592695, RecoilDirection = 2715839340, ReloadSpeed = 4188031367, RoundsPerMinute = 4284893193, ScoreMultiplier = 2733264856, ShieldDuration = 1842278586, SolarCost = 3344745325, SolarDamageResistance = 1546607979, Speed = 1501155019, Stability = 155624089, StasisCost = 998798867, Super = 144602215, SwingSpeed = 2837207746, TimeToAimDownSights = 3988418950, Velocity = 2523465841, VentSpeed = 602570185, VoidCost = 2399985800, VoidDamageResistance = 1546607980, Weapons = 2996146975, Zoom = 3555269338, } export const enum ItemCategoryHashes { AdventureTokens = 3134521018, Armor = 20, ArmorMods = 4104513227, ArmorModsChest = 3723676689, ArmorModsClass = 3196106184, ArmorModsClassHunter = 1037516129, ArmorModsClassTitan = 1650311619, ArmorModsClassWarlock = 2955376534, ArmorModsGameplay = 4062965806, ArmorModsGauntlets = 3872696960, ArmorModsGlowEffects = 1875601085, ArmorModsHelmet = 1362265421, ArmorModsLegs = 3607371986, ArmorModsOrnaments = 1742617626, ArmorModsOrnamentsHunter = 3683250363, ArmorModsOrnamentsTitan = 3229540061, ArmorModsOrnamentsWarlock = 3684181176, Arms = 46, Aura = 57, AutoRifle = 5, Blueprint = 51, BonusMods = 303512563, Bounties = 1784235469, Bows = 3317538576, BreakerDisruption = 964228942, BreakerPiercing = 1793728308, BreakerStagger = 2906646562, ChallengeCards = 3041770480, Chest = 47, ClanBanner = 58, ClanBanners = 874645359, ClanBannersPerks = 1576735337, ClanBannersStaff = 1873949940, ClassItems = 49, Consumables = 35, Currencies = 18, Dummies = 3109687656, Emblems = 19, Emotes = 44, EnergyWeapon = 3, Engrams = 34, Finishers = 1112488720, FusionRifle = 9, GagPrizes = 2150402250, Ghost = 39, GhostMods = 1449602859, GhostModsPerks = 4176831154, GhostModsProjections = 1404791674, GhostModsTrackers = 1826038950, Glaives = 3871742104, GrenadeLaunchers = 153950757, HandCannon = 6, Helmets = 45, Hunter = 23, InfusionMaterials = 2266591099, Inventory = 52, ItemSets = 2423200735, KineticWeapon = 2, Legs = 48, LinearFusionRifles = 1504945536, MachineGun = 12, Mask = 55, MasterworksMods = 141186804, Materials = 40, Mods_Mod = 59, Mods_Ornament = 56, Packages = 268598612, Patterns = 3726054802, PowerWeapon = 4, ProphecyOfferings = 2005599723, ProphecyTablets = 2250046497, PulseRifle = 7, Quest = 53, QuestStep = 16, RepeatableBounties = 713159888, ReputationTokens = 2088636411, RocketLauncher = 13, ScoutRifle = 8, SeasonalArtifacts = 1378222069, Shaders = 41, ShipMods = 177260082, ShipModsTransmatEffects = 208981632, Ships = 42, Shotgun = 11, Sidearm = 14, SniperRifle = 10, SparrowMods = 152608020, SparrowModsPerks = 319279448, SparrowModsSpeed = 2294843884, Sparrows = 43, StoryPrizes = 3747263590, SubclassMods = 1043342778, Subclasses = 50, SubmachineGuns = 3954685534, Sword = 54, TicketMods = 3448069192, Titan = 22, TraceRifles = 2489664120, TreasureMaps = 2253669532, VendorWrappedItems = 3301210334, Warlock = 21, Weapon = 1, WeaponMods = 610365472, WeaponModsArrows = 3360831066, WeaponModsBarrels = 3085181971, WeaponModsBatteries = 1334054322, WeaponModsBowstring = 444756050, WeaponModsDamage = 1052191496, WeaponModsFrame = 3708671066, WeaponModsGameplay = 945330047, WeaponModsGrips = 3836367751, WeaponModsHafts = 2840834688, WeaponModsIntrinsic = 2237038328, WeaponModsLaunchTubes = 2076918099, WeaponModsMagazines = 4184407433, WeaponModsOriginTraits = 2779167812, WeaponModsOrnaments = 3124752623, WeaponModsScopes = 2411768833, WeaponModsSights = 3866509906, WeaponModsStocks = 3055157023, WeaponModsSwordBlades = 1709863189, WeaponModsSwordGuards = 3072652064, } export const enum SocketCategoryHashes { Abilities_Abilities = 309722977, Abilities_Abilities_Ikora = 3218807805, ArmorCosmetics = 1926152773, ArmorMods = 590099826, ArmorPerks_LargePerk = 3154740035, ArmorPerks_Reusable = 2518356196, ArmorTier = 760375309, Aspects_Abilities = 2047681910, Aspects_Abilities_Ikora = 2140934067, Aspects_Abilities_Neomuna = 764703411, Aspects_Abilities_Stranger = 3400923910, ClanPerks_Unlockable_ClanBanner = 3898156960, ClanPerks_Unlockable_UNUSED = 1683579090, ClanStaves = 3954618873, DestinationMods = 2787222118, EmblemBonuses = 513547461, EmblemCustomization = 279738248, Emotes = 1093090108, ForgeMaterials = 3970188346, Fragments_Abilities = 271461480, Fragments_Abilities_Ikora = 1313488945, Fragments_Abilities_Neomuna = 193371309, Fragments_Abilities_Stranger = 2819965312, GambitPrimePerks = 2283447921, GhostCosmetics = 2549160099, GhostMods = 3886482628, GhostShellMods = 3379164649, GhostShellPerks = 3301318876, GhostTier = 446880883, Information = 2217462106, Ingredients = 1191575720, IntrinsicTraits = 3956125808, NightfallModifiers = 2622243744, PowerAndEfficiency = 2529820344, Recipes_Consumable_Container = 2212127078, Recipes_Consumable_UNUSED = 2059652296, RuneBonus = 879083882, RuneCompatibility = 2316895247, Super = 457473665, Transcendence = 1905270138, VehicleMods_Consumable_Ship = 4265082475, VehicleMods_Consumable_Vehicle = 4243480345, VehiclePerks = 2278110604, WeaponCosmetics = 2048875504, WeaponMods = 2685412949, WeaponPerks_Consumable = 3410521964, WeaponPerks_Reusable = 4241085061, } export const enum BucketHashes { Accessories = 687325600, BrightDust = 2689798311, ChestArmor = 14239492, ClanBanners = 4292445962, ClassArmor = 1585787867, Consumables = 1469714392, Emblems = 4274335291, Emotes = 1107761855, EnergyWeapons = 2465295065, Engrams = 375726501, EventTickets = 1582878835, Finishers = 3683254069, Gauntlets = 3551918588, General = 138197802, Ghost = 4023194814, Glimmer = 2689798308, Helmet = 3448274439, KineticWeapons = 1498876634, LegArmor = 20886954, LostItems = 215593132, Materials = 3865314626, Messages = 3161908920, Modifications = 3313201758, Orders = 635141261, PowerWeapons = 953998645, Quests = 1345459588, SeasonalArtifact = 1506418338, Ships = 284967655, Silver = 2689798310, SpecialOrders = 1367666825, StrangeCoin = 2689798305, Subclass = 3284755031, SynthweaveBolt = 4092644516, SynthweavePlate = 4092644519, SynthweaveStrap = 4092644518, SynthweaveTemplate = 4092644517, UnstableCores = 2689798309, UpgradePoint = 2689798304, Vehicle = 2025709351, WrappedItems = 3350918817, } export const enum BreakerTypeHashes { Disruption = 2611060930, ShieldPiercing = 485622768, Stagger = 3178805705, } export const enum ProgressionHashes { CompetitiveDivision = 3696598664, StrangeFavor = 527867935, } export const enum TraitHashes { ActivitiesBlackArmory = 2944045106, ActivitiesGambit = 853784306, ActivitiesIronBanner = 2716563063, ActivitiesMamba = 1781288324, ActivitiesTrials = 3439101959, All = 1434215347, Amplified = 3291013836, Blind = 500183315, BoltCharge = 2935077680, Career = 4263853822, Cure = 3263723277, DarknessBuffs = 1891050213, DarknessDebuffs = 1514833946, Devour = 3078132110, Exotics = 370766376, FactionCrucible = 2951764300, FactionDeadOrbit = 3331226384, FactionFutureWarCult = 1345630660, FactionNewMonarchy = 1221030001, FactionVanguard = 3359893241, Firesprite = 37177486, FoundryDaito = 1866367371, FoundryFieldForged = 3475344486, FoundryFotc = 2217328812, FoundryHakke = 2210483526, FoundryOmolon = 192828432, FoundrySuros = 3690635686, FoundryTexMechanica = 1821231131, FoundryVeist = 963390771, Freeze = 2968599152, FrostArmor = 106947924, Ignition = 3268862716, InventoryFilteringBounty = 201433599, InventoryFilteringQuest = 1861210184, InventoryFilteringQuestFeatured = 3034243664, Invisibility = 655301426, IonicTrace = 3824458961, ItemArmorArms = 1851377542, ItemArmorChest = 374319058, ItemArmorClass = 3367459877, ItemArmorExotic = 4252733117, ItemArmorHead = 1075323345, ItemArmorLegs = 1968436740, ItemAura = 3553898659, ItemBoost = 1030789163, ItemBounty = 2443101659, ItemConsumable = 2062186907, ItemCurrency = 3906525419, ItemEmblem = 2455696884, ItemEmote = 888082966, ItemEngram = 2893978702, ItemExoticCatalyst = 4036726046, ItemFinisher = 2582082890, ItemGhost = 2570676179, ItemGhostHologram = 4118304139, ItemOrnamentArmor = 3477257717, ItemOrnamentWeapon = 3828004164, ItemPackage = 151064318, ItemPlugAspect = 577926988, ItemPlugFragment = 2833630124, ItemQuestAnnualV460 = 2908763903, ItemQuestAnnualV500 = 2774395792, ItemQuestAnnualV600 = 929402123, ItemQuestAnnualV700 = 2976021378, ItemQuestAnnualV800 = 3011401061, ItemQuestAnnualV900 = 763053052, ItemQuestCampaign = 2973844452, ItemQuestEvent = 1056186694, ItemQuestFrontierApollo = 2799343944, ItemQuestFrontierBehemoth = 904453863, ItemQuestOnramp = 170945933, ItemShader = 2652561225, ItemShip = 3607584152, ItemSpawnfx = 856705125, ItemSubclassDark = 3224025418, ItemSubclassLight = 482679394, ItemSubclassPrism = 3820193993, ItemVehicle = 3977049418, ItemWeapon = 567594262, ItemWeaponAutoRifle = 2729780558, ItemWeaponBow = 195373008, ItemWeaponExotic = 791530618, ItemWeaponFusionRifle = 2891203715, ItemWeaponGlaive = 888940472, ItemWeaponGrenadeLauncher = 130863397, ItemWeaponHandCannon = 3602983853, ItemWeaponLinearFusionRifle = 2100142349, ItemWeaponMachinegun = 1143070403, ItemWeaponPulseRifle = 1648572040, ItemWeaponRocketLauncher = 3925016055, ItemWeaponScoutRifle = 12026609, ItemWeaponShotgun = 2114179114, ItemWeaponSidearm = 2034403781, ItemWeaponSniperRifle = 3300229618, ItemWeaponSubmachinegun = 2659552777, ItemWeaponSword = 1531673855, ItemWeaponTraceRifle = 446244952, Jolted = 3221118171, LightBuffs = 2713325501, LightDebuffs = 3023190802, MambaRoleCollector = 3791840693, MambaRoleDefender = 2712954769, MambaRoleInvader = 3090596947, MambaRoleKiller = 3460933757, NewLight = 520867389, Overshield = 2485406866, Playlists = 500105683, Radiant = 157469667, ReleasesV300Annual = 2677200345, ReleasesV310Season = 3750900718, ReleasesV320Season = 3990406773, ReleasesV350Season = 977620370, ReleasesV400Annual = 1385893620, ReleasesV400Season = 1416106830, ReleasesV410Season = 3619103539, ReleasesV420Season = 117031016, ReleasesV450Season = 1357347767, ReleasesV460Season = 1160263324, ReleasesV470Season = 2326993577, ReleasesV480Season = 1573004294, ReleasesV490Season = 2405803211, ReleasesV500Annual = 2184280643, ReleasesV500Season = 2752740613, ReleasesV510Season = 3361847320, ReleasesV520Season = 4020167523, ReleasesV530Season = 3353022846, ReleasesV540Season = 2656809369, ReleasesV600Annual = 823756278, ReleasesV600Season = 3596220576, ReleasesV610Season = 2868778669, ReleasesV620Season = 2572971238, ReleasesV630Season = 2208921643, ReleasesV700Annual = 2606653893, ReleasesV700Season = 3833926855, ReleasesV710Season = 661041410, ReleasesV720Season = 687504889, ReleasesV730Season = 866931116, ReleasesV800Annual = 2906302736, ReleasesV800Season = 1348188306, ReleasesV810Season = 4062709591, ReleasesV820Season = 3870807100, ReleasesV900Core = 1858131755, ReleasesV900Dlc = 2725534325, ReleasesV910 = 753559279, ReleasesV910Core = 2052231686, ReleasesV950 = 686448803, ReleasesV950Core = 2987314010, ReleasesV950Dlc = 2773025918, Restoration = 3488482714, Scorch = 1096356879, Seasonal_Metric = 2230116619, Seasonal_Quests = 3671004794, Seasonal_Quests_UNUSED = 4105527943, Sever = 2519102437, Shatter = 37938188, Slow = 4239423954, StasisCrystal = 3385340084, StasisShard = 4043161234, Suppression = 2578642829, Suspend = 2679722414, Tangle = 1577394840, TheFinalShape = 2878306895, ThePast = 2387836362, Threadling = 2724747993, Transcendence = 345967499, Unravel = 945613349, VoidBreach = 3328352616, Volatile = 4105407564, Weaken = 3336638905, Weekly = 2356777566, WovenMail = 3173573497, } ================================================ FILE: src/data/d2/ghost-perks.json ================================================ { "30834361": { "location": "mars", "type": { "xp": false, "resource": true, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "36804749": { "location": "dreaming", "type": { "xp": false, "resource": true, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "66383463": { "location": "nessus", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "103219604": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": true, "void": true, "solar": true }, "improved": false } }, "183999083": { "location": "mars", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "263169843": { "location": "titan", "type": { "xp": false, "resource": true, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "281656579": { "location": "mars", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "322369214": { "location": "dreaming", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "329782251": { "location": "crucible", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "390974608": { "location": "edz", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "392619685": { "location": "crucible", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "478676376": { "location": "dreaming", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "566395092": { "location": "mercury", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "693114386": { "location": "gambit", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "707631447": { "location": "io", "type": { "xp": false, "resource": true, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "741416970": { "location": "leviathan", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "815624991": { "location": "titan", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "816744355": { "location": "dreaming", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "819844944": { "location": "mars", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "932058162": { "location": "titan", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1048118643": { "location": "io", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1064347710": { "location": "io", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1085640235": { "location": "nessus", "type": { "xp": false, "resource": true, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1143206163": { "location": "tangled", "type": { "xp": false, "resource": true, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1198449474": { "location": "mercury", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1255371660": { "location": "mercury", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1255489387": { "location": false, "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1287422957": { "location": "moon", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1329583752": { "location": "crucible", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1365595761": { "location": "moon", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1400394461": { "location": "titan", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1586768444": { "location": "mars", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "1612078667": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1626532497": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": true, "void": false, "solar": false }, "improved": false } }, "1702115876": { "location": "tangled", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "1712870644": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": true, "void": true, "solar": true }, "improved": true } }, "1765741659": { "location": "io", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "1791543972": { "location": "tangled", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1894263102": { "location": "nessus", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1916863581": { "location": "io", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1933479768": { "location": "titan", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1942048138": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": true }, "improved": false } }, "1946260457": { "location": "titan", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1990928495": { "location": "edz", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "1991567150": { "location": "strikes", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2010132761": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2096072732": { "location": "moon", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2126415067": { "location": "mercury", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2142971474": { "location": "mercury", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "2199055252": { "location": "io", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2207103371": { "location": "strikes", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2224144604": { "location": "mars", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2274579115": { "location": "titan", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "2278653192": { "location": "moon", "type": { "xp": false, "resource": true, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2404852074": { "location": "mercury", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2425296638": { "location": "edz", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2536988426": { "location": "dreaming", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2595207061": { "location": "tangled", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2602884912": { "location": "dreaming", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2645492862": { "location": "edz", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "2856116177": { "location": "nessus", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2858914062": { "location": "tangled", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "2956832212": { "location": false, "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3126446358": { "location": "tangled", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3138059283": { "location": "mercury", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3147447938": { "location": "edz", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3249887691": { "location": "dreaming", "type": { "xp": false, "resource": true, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3260411014": { "location": "mercury", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3300497782": { "location": "strikes", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3393447213": { "location": "nessus", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3447587684": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": true, "void": false, "solar": false }, "improved": true } }, "3448093826": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": true, "solar": false }, "improved": true } }, "3457795268": { "location": "nessus", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3523930524": { "location": "edz", "type": { "xp": false, "resource": true, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3528214957": { "location": "tangled", "type": { "xp": false, "resource": true, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3569214087": { "location": "nessus", "type": { "xp": false, "resource": true, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3785828493": { "location": "leviathan", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3883296839": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": true }, "improved": true } }, "3900721702": { "location": "mars", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3920312498": { "location": "moon", "type": { "xp": false, "resource": true, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "3935848740": { "location": "crucible", "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": true, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "4040200521": { "location": "io", "type": { "xp": false, "resource": false, "cache": false, "scanner": true, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "4051388481": { "location": "edz", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "4082217637": { "location": "mercury", "type": { "xp": false, "resource": true, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "4212305433": { "location": "moon", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": true } }, "4217001755": { "location": "moon", "type": { "xp": false, "resource": false, "cache": true, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "4258605391": { "location": "gambit", "type": { "xp": true, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": false, "solar": false }, "improved": false } }, "4287983117": { "location": false, "type": { "xp": false, "resource": false, "cache": false, "scanner": false, "glimmer": false, "telemetry": { "arc": false, "void": true, "solar": false }, "improved": false } } } ================================================ FILE: src/data/d2/item-def-workaround-replacements.json ================================================ {} ================================================ FILE: src/data/d2/legacy-triumphs.json ================================================ [ 1238389519, 2940918151, 2388152175, 741263299, 2711883372, 1605418532, 520045964, 2548421444, 3579759870, 362133172, 4026489705, 3007266598, 3087434129, 3245402197, 1403792946, 3002736314, 199260626, 4020528891, 1487722505, 1504605755, 419233155, 300073435, 1544191462, 801453721, 4205280592, 4222058179, 2516850755, 1051654420, 1719592571, 3450412105, 2901923505, 98447849, 1444407971, 696175076, 2595141627, 1251772809, 2216557404, 2319750236, 2219999707, 3024875718, 3759366231, 635174847, 1319909495, 2163056166, 2738665088, 2032752906, 4041232608, 1761789705, 294322125, 1351982072, 1368759659, 890367121, 3920378224, 3937155811, 1919487861, 1428420761, 1911491978, 3108158456, 3428527852, 2356701558, 668362217, 1755196920, 3058814955, 3765429897, 2027886756, 4156353017, 653350487, 3709742728, 1995878493, 1549892907, 3957574964, 3566059558, 4278867533, 3303390072, 3026670632, 1914666283, 599626507, 2778379986, 158452041, 3906772567, 50106418, 3819920005, 4189480969, 122129118, 1485121063, 87609703, 190498113, 955522375, 1397652882, 1268449134, 945314810, 2400201239, 545760049, 1745319359, 3357503754, 3291796550, 2970271298, 992007906, 206063775, 3641080561, 1473714136, 2194379228, 1515947769, 3108476717, 837553231, 4138047631, 2175796146, 3053687420, 3303729824, 1341325320, 656498579, 1175797132, 3963239036, 3074780428, 3807924444, 1327869331, 1205516741, 915223473, 2462707519, 2943357143, 825425501, 262330401, 1753083296, 3137136954, 2266548618, 3666883430, 76808929, 3763143014, 3656200582, 3925437282, 1926238990, 517657777, 2316097579, 586316675, 2110987253, 2737832720, 3318199669, 3155364169, 1915077419, 1703455291, 1183021806, 909564168, 2790141380, 510151900, 3709680455, 1119249224, 859223080, 2791791752, 2265703576, 3693733297, 1927065093, 2282573299, 3056033539, 2851043161, 1430747933, 957831750, 2934266400, 623937160, 3417737709, 3374539358, 2323654178, 2417378659, 3080061772, 1396554507, 1790031820, 108257051, 1032620927, 1025917306, 4204990045, 1549003459, 3175460993, 720205407, 915055151, 2439024316, 212587483, 3289153909, 2257421674, 3812674679, 1135755896, 3608027344, 2316952678, 4247122247, 2170106955, 876661133, 709251999, 3501618455, 2422246593, 2041489007, 654986383, 427517758, 4132148533, 2439024306, 903845240, 2264077519, 2422246605, 114259053, 31686901, 2439024307, 660046832, 2422246606, 2354302733, 2957876347, 2590294698, 76056909, 1290403512, 3350489579, 3758540824, 3154122421, 2422246607, 3204879741, 2422246604, 3206626971, 3141945846, 2567210364, 417454997, 2422246602, 562163044, 1769517637, 2316952679, 3229767955, 1417036993, 2364729185, 1355802591, 3450819748, 3575189553, 1585753315, 2316952677, 2439024318, 398866567, 1773475425, 2516080100, 3781961120, 3174045247, 2230249087, 378857807, 1497969739, 2422246592, 1675858334, 3749164188, 2439024319, 3967582262, 189012149, 3369372011, 4120805470, 2633814448, 142013189, 2472579459, 2487226217, 2167527924, 2075566064, 2129401906, 807845972, 3387412013, 1593047161, 1959753477, 2100504924, 2172518965, 66744960, 2422246600, 2372209106, 66744961, 2422246601, 1363982253, 1178901007, 66744963, 2351146132, 1559002718, 2567210365, 588012753, 1291112668, 137611858, 2567210366, 952321049, 1416925544, 94777801, 142290502, 2575965017, 3311833517, 2439024314, 976885986, 976885985, 976885987, 2362979568, 2472579457, 3273410592, 3601025763, 2690344985, 1466482627, 590872239, 3981762193, 3700968734, 1968031376, 3079212897, 2175353922, 2575452461, 3382182066, 4206656175, 3206158567, 4168712242, 99050004, 3836008081, 830121872, 3865475919, 1124631436, 2970941799, 836362355, 2776744497, 2241590012, 673722307, 2745164727, 1767590660, 2698848157, 3786862122, 1745790710, 2256027242, 3568848026, 3242204786, 3675487637, 2543223197, 232584921, 75852146, 794571444, 3742000974, 4183245640, 1568997843, 2582522989, 225013228, 3388126667, 2572613793, 1850469012, 1052090143, 667993428, 1420454218, 1294968143, 548399698, 1489828608, 1622150852, 1784977487, 78340251, 3303459931, 1649258247, 4140588287, 625268251, 1599750028, 3191146526, 452100546, 2874049730, 2421801389, 1158520774, 535644892, 215085557, 734114175, 820921504, 936519399, 1912542720, 1485847905, 3006220811, 1501812325, 2974733474, 2355480639, 673789974, 3991784984, 1014828242, 1230668439, 2889459581, 4060779237, 1053902591, 4003648997, 1228579173, 3656702813, 2208219663, 3325360309, 990362719, 770493095, 3685340058, 55098543, 1624361599, 2976232082, 479438672, 1381558154, 734525036, 1286790825, 3672040342, 3393252660, 52390681, 3940815508, 3940815511, 2589331138, 366622223, 2912918660, 2581768637, 3529019298, 3882301596, 582114738, 4205106950, 518035568, 1417584842, 2049155790, 221623575, 3684290925, 150760896, 4105782263, 3717583795, 2818425184, 196386292, 3929418444, 1560412314, 610196493, 3474000123, 3216121021, 2220885103, 3061566494, 2281908957, 3215676856, 940023078, 2425245158, 2570359818, 3512158517, 932359421, 3998407620, 1264724241, 2127354430, 1946732367, 2859617572, 810596715, 2173454726, 660188555, 3477624600, 69622129, 1974526186, 2312577621, 2191229220, 2397844361, 645697534, 459249011, 1807909225, 3216522804, 3180635594, 3967884273, 668304667, 112406565, 3566973509, 1099196574, 3664983231, 2588095697, 3657273027, 3903376255, 1686267788, 4163099445, 2341886425, 3759633613, 3846075781, 184258406, 108280137, 1339925682, 2519410346, 2913109294, 3800989613, 4026109699, 2804904089, 630510424, 3302752741, 2648263259, 3115239680, 2069084282, 1783991073, 2923920213, 912770300, 2818354043, 3672750091, 2053546184, 2653351430, 3020697345, 2953158791, 417192912, 3225699997, 810213052, 3558002660, 2910489519, 3518355463, 2094467183, 2382088899, 3931440391, 1111992238, 3084815879, 3362185512, 2357163007, 379624208, 3195682571, 1074336130, 1799797321, 396038457, 1839524615, 473586745, 230421321, 2122679994, 1686327621, 684525211, 2972583416, 3791036271, 2801882959, 3196543652, 2250060994, 2914618589, 4201266719, 3996842932, 3930580689, 2497233656, 3147085345, 3199735617, 1642140110, 1883059838, 3801239892, 3342375663, 827895011, 3433615715, 2703117203, 3055480593, 3055480594, 3111471346, 3055480592, 1341533303, 2136564733, 1997406032, 3348410972, 3089073815, 20493479, 402610684, 1012728572, 2594154573, 270943800, 4096604676, 1484250585, 2765638457, 2793295036, 3351023720, 183892908, 1620273941, 2070972007, 624350677, 2923250426, 4066171783, 2602370549, 1428463716, 1558682417, 2055584838, 940998165, 2702532926, 1558682423, 3758103712, 1558682416, 1266663497, 1590210414, 2863337003, 503811851, 1871570556, 3985356666, 2489553554, 3861076347, 1558682422, 3029681530, 4062087976, 1742897719, 3574310085, 3420353827, 1879702969, 2412880299, 2478001043, 3957303142, 3231147005, 1804999028, 1575460005, 1558682421, 3636866482, 2648109757, 1575460004, 1558682429, 2234827297, 574761812, 614406941, 1726429346, 1144140061, 2224919182, 3181604446, 4255945596, 1322754255, 3136771444, 2332658765, 639901446, 3765705655, 3647083276, 190188375, 1364605277, 2994199542, 444152197, 3158715546, 2846658569, 2701908644, 1651981295, 4140553751, 3793243435, 1214251150, 3305512855, 1312270975, 452451691, 1522954160, 1856139708, 1518196036, 3591836320, 3657886407, 2982664966, 43046618, 1742345588, 2927675329, 1345953030, 622242972, 97558110, 2052871747, 3363274714, 345067936, 288478788, 4276872224, 223970495, 1438813765, 2647057001, 64778617, 2229373471, 2405724331, 2413868417, 3232635341, 1575271155, 1721328830, 1821758900, 1142177491, 1026882738, 1558682419, 772878705, 2442138301, 1442950315, 581089627, 435060157, 2765010546, 1398454187, 3367594762, 2808299284, 1289798960, 709188219, 3013611925, 4162926221, 1575460003, 4060320345, 1558682418, 496309570, 2502625450, 1841174948, 1214241085, 1575460002, 559593008, 2725374066, 105811740, 1558682428, 3780682732, 1987500905 ] ================================================ FILE: src/data/d2/lightcap-to-season.json ================================================ { "300": 1, "330": 2, "380": 3, "550": 28, "600": 4, "650": 5, "700": 6, "750": 7, "960": 8, "970": 9, "1010": 10, "1060": 11, "1260": 12, "1310": 13, "1320": 14, "1330": 15, "1560": 16, "1570": 17, "1580": 18, "1590": 19, "1810": 23, "2000": 24, "2010": 25, "2020": 26 } ================================================ FILE: src/data/d2/masterworks-with-cond-stats.json ================================================ [ 131028559, 186337601, 266016299, 384158423, 471689728, 622249644, 651347274, 758092021, 882794620, 905637376, 915325363, 1154004463, 1246656862, 1532397225, 1639384016, 2303087634, 2638039968, 2697220197, 2782281470, 2993547493, 3128594062, 3481828396, 3486498337, 3513245618, 3587454724, 3803457565 ] ================================================ FILE: src/data/d2/missing-faction-tokens.json ================================================ [] ================================================ FILE: src/data/d2/missing-source-info.ts ================================================ const missingSources: { [key: string]: number[] } = { '30th': [ 286271818, // Twisting Echo Cloak 399065241, // Descending Echo Greaves 587312237, // Twisting Echo Grips 833653807, // Twisting Echo Strides 1756483796, // Twisting Echo Mask 1951355667, // Twisting Echo Vest 2244604734, // Corrupting Echo Gloves 2663987096, // Corrupting Echo Boots 2885497847, // Descending Echo Gauntlets 3048458482, // Corrupting Echo Robes 3171090615, // Corrupting Echo Cover 3267969345, // Descending Echo Cage 3685276035, // Corrupting Echo Bond 3871537958, // Descending Echo Helm 4050474396, // Descending Echo Mark ], ada: [ 2533990645, // Blast Furnace ], adventure: [ 11686457, // Unethical Experiments Cloak 11686458, // Orobas Vectura Cloak 320310249, // Orobas Vectura Bond 320310250, // Unethical Experiments Bond 886128573, // Mindbreaker Boots 1096417434, // Shieldbreaker Robes 1286488743, // Shieldbreaker Plate 1355771621, // Shieldbreaker Vest 1701005142, // Songbreaker Gloves 1701005143, // Gearhead Gloves 2317191363, // Mindbreaker Boots 2426340788, // Orobas Vectura Mark 2426340791, // Unethical Experiments Mark 2486041712, // Gearhead Gauntlets 2486041713, // Songbreaker Gauntlets 2913284400, // Mindbreaker Boots 3706457514, // Gearhead Grips 3706457515, // Songbreaker Grips ], blackarmory: [ 2533990645, // Blast Furnace ], brave: [ 211732170, // Hammerhead 243425374, // Falling Guillotine 570866107, // Succession 2228325504, // Edge Transit 2499720827, // Midnight Coup 3757612024, // Luna's Howl 3851176026, // Elsie's Rifle ], campaign: [ 423789, // Mythos Hack 4.1 644105, // Heavy Ammo Finder 11686456, // Dreamer's Cloak 13719069, // Atgeir Mark 40512774, // Farseeker's Casque 56663992, // Solar Scavenger 59990642, // Refugee Plate 67798808, // Atonement Tau 76554114, // Cry Defiance 83898430, // Scavenger Suit 91289429, // Atonement Tau 95934356, // Strand Loader 96682422, // Arc Targeting 124410141, // Shadow Specter 126602378, // Primal Siege Type 1 137713267, // Refugee Vest 174910288, // Mark of Inquisition 177215556, // Shadow Specter 182285650, // Kit Fox 1.5 201644247, // Hardcase Battleplate 202783988, // Black Shield Mark 203317967, // Fieldplate Type 10 226227391, // Fortress Field 246765359, // Mythos Hack 4.1 255520209, // Cloak of Retelling 280187206, // Hardcase Battleplate 288815409, // Renegade Greaves 293178904, // Unflinching Harmonic Aim 320174990, // Bond of Chiron 320310251, // Dreamer's Bond 331268185, // Solar Targeting 341343759, // Prophet Snow 341468857, // Bond of Insight 366418892, // The Outlander's Grip 387708030, // Prophet Snow 392489920, // Primal Siege Type 1 397654099, // Wastelander Vest 402937789, // Shadow Specter 406995961, // Stagnatious Rebuke 407150808, // Ribbontail 407150809, // Ribbontail 407150810, // Ribbontail 407150811, // Ribbontail 411014648, // Solar Ammo Generation 417821705, // Primal Siege Type 1 418611312, // Shadow Specter 420937712, // War Mantis Cloak 422994787, // Emergency Reinforcement 452060094, // Refugee Gloves 457297858, // Atgeir 2T1 459778797, // Refugee Mask 461025654, // Primal Siege Type 1 463563656, // Primal Siege Type 1 467612864, // Chiron's Cure 474150341, // RPC Valiant 482091581, // Hardcase Stompers 484126150, // Chiron's Cure 516502270, // Firebreak Field 531665167, // Solar Dexterity 539726822, // Refugee Boots 550258943, // Chiron's Cure 558125905, // Frumious Mask 579997810, // Kinetic Scavenger 598178607, // Mark of Confrontation 600059642, // The Outlander's Cloak 610837228, // Raven Shard 612495088, // Homeward 622291842, // Farseeker's March 625602056, // Memory of Cayde Cloak 627055961, // Fortress Field 634608391, // Solar Loader 643145875, // Legion-Bane 648022469, // Makeshift Suit 648638907, // Kit Fox 2.1 657773637, // Sniper Damage Resistance 674335586, // Chiron's Cure 696808195, // Refugee Mark 703683040, // Atgeir 2T1 703902595, // Stasis Loader 720723122, // At Least It's a Cape 721208609, // Farseeker's Intuition 732520437, // Baseline Mark 733635242, // RPC Valiant 735669834, // Shadow Specter 739196403, // RPC Valiant 739406993, // Atgeir 2T1 747210772, // Mythos Hack 4.1 777818225, // Fieldplate Type 10 789384557, // Atonement Tau 792400107, // Unflinching Arc Aim 795389673, // The Outlander's Cloak 803939997, // War Mantis 830369300, // Lucent Blades 833626649, // Chiron's Cure 844823562, // Mechanik 1.1 846463017, // Fieldplate Type 10 856745412, // War Mantis 857264972, // Scavenger Suit 863007481, // Farseeker's March 867963905, // Hardcase Brawlers 868799838, // Renegade Helm 871442456, // Refugee Boots 877723168, // Harmonic Scavenger 881194063, // Prophet Snow 897275209, // The Outlander's Heart 897335593, // Kinetic Siphon 905249529, // Shadow Specter 911039437, // Refugee Gloves 930759851, // Concussive Dampener 933345182, // Fieldplate Type 10 965934024, // Firepower 974161790, // Tactical 995248967, // Makeshift Suit 1012254326, // The Outlander's Steps 1014677029, // Memory of Cayde 1017385934, // Void Dexterity 1019574576, // Unflinching Solar Aim 1022126988, // Baseline Mark 1036557198, // Hands-On 1044888195, // Utility Kickstart 1045948748, // Mythos Hack 4.1 1048498953, // Bond of the Raven Shard 1070180272, // Hardcase Helm 1086997255, // Solar Siphon 1103878128, // Harmonic Ammo Generation 1118428792, // Unflinching Stasis Aim 1118437892, // War Mantis 1124184622, // Minor Class Mod 1139671158, // Melee Kickstart 1153260021, // Impact Induction 1169595348, // Mythos Hack 4.1 1176372075, // Stasis Resistance 1208761894, // Empowered Finish 1210012576, // Void Siphon 1255614814, // Grenade Font 1256569366, // Raven Shard 1279721672, // Fortress Field 1300106409, // Prophet Snow 1305848463, // Strand Scavenger 1328755281, // Farseeker's Casque 1331205087, // Cosmic Wind III 1360445272, // Firebreak Field 1365739620, // Mythos Hack 4.1 1365979278, // Legion-Bane 1378545975, // Refugee Helm 1443091319, // Firebreak Field 1452147980, // Makeshift Suit 1455694321, // Prophet Snow 1473385934, // Mark of the Golden Citadel 1479532637, // The Outlander's Cover 1479892134, // War Mantis 1484009400, // RPC Valiant 1486292360, // Chiron's Cure 1488618333, // Chiron's Cure 1500704923, // Prophet Snow 1501094193, // Strand Weapon Surge 1503713660, // Stagnatious Rebuke 1512570524, // Hardcase Stompers 1537074069, // Phoenix Cradle 1556652797, // The Outlander's Grip 1561736585, // Kinetic Dexterity 1578478684, // Refugee Gloves 1581838479, // Refugee Boots 1596513538, // Tactical 1604394872, // Dynamo 1611221278, // Prophet Snow 1616317796, // Prophet Snow 1627901452, // Stacks on Stacks 1630079134, // Bond of Forgotten Wars 1658512403, // Mythos Hack 4.1 1665016007, // Primal Siege Type 1 1672155562, // Class Font 1691784182, // Mythos Hack 4.1 1701236611, // The Outlander's Heart 1702273159, // Harmonic Loader 1709236482, // Heavy Handed 1715842350, // Generalist Shell 1736993473, // Legion-Bane 1763607626, // Melee Mod 1775818231, // Legion-Bane 1783952505, // Time Dilation 1784774885, // Vector Home 1801153435, // Stasis Targeting 1824298413, // War Mantis 1848999098, // Bond of Symmetry 1862164825, // War Mantis Cloak 1866564759, // Super Mod 1872887954, // Atonement Tau 1891463783, // Harmonic Targeting 1901221009, // Weapons Font 1912568536, // Primal Siege Type 1 1915498345, // Cloak of Retelling 1924584408, // Grenade Kickstart 1933944659, // Hardcase Helm 1965476837, // War Mantis 1981225397, // Shadow Specter 1988790493, // Stagnatious Rebuke 1992338980, // The Outlander's Cover 1996008488, // Stormdancer's Brace 2002682954, // Vector Home 2031584061, // Momentum Transfer 2049820819, // Vector Home 2065578431, // Shadow Specter 2113881316, // Minor Health Mod 2136310244, // Ashes to Assets 2148305277, // Raven Shard 2151724216, // Prophet Snow 2159062493, // Mythos Hack 4.1 2162276668, // Cry Defiance 2165661157, // Baseline Mark 2183384906, // War Mantis 2190967049, // Prophet Snow 2203146422, // Assassin's Cowl 2211544324, // The Outlander's Cloak 2214424583, // Kinetic Targeting 2230522771, // War Mantis 2237975061, // Kinetic Loader 2245839670, // Proximity Ward 2246316031, // Arc Weapon Surge 2253044470, // Legion-Bane 2267311547, // Stasis Dexterity 2283894334, // Solar Weapon Surge 2303417969, // Strand Ammo Generation 2305736470, // Kinetic Ammo Generation 2317046938, // Shadow Specter 2318667184, // Kinetic Weapon Surge 2325151798, // Unflinching Kinetic Aim 2329963686, // Mark of Confrontation 2339344379, // Atonement Tau 2343139242, // Bond of Refuge 2362809459, // Hardcase Stompers 2363903643, // Makeshift Suit 2413278875, // Void Ammo Generation 2426340790, // Dreamer's Mark 2436471653, // Arc Scavenger 2441435355, // Prophet Snow 2459075622, // RPC Valiant 2466525328, // RPC Valiant 2476964124, // War Mantis 2479297167, // Harmonic Dexterity 2493161484, // Class Mod 2504771764, // Refugee Helm 2519597513, // Minor Super Mod 2526922422, // Stasis Weapon Surge 2541019576, // Mark of Confrontation 2554933025, // Stunloader 2562645296, // Melee Damage Resistance 2567295299, // Cosmic Wind III 2568808786, // Health Mod 2574857320, // Sly Cloak 2583547635, // Cry Defiance 2626766308, // Mark of the Longest Line 2634786903, // Void Holster 2640935765, // Memory of Cayde 2644553610, // Renegade Hood 2689896341, // Mythos Hack 4.1 2739875972, // RPC Valiant 2742930797, // Fatum Praevaricator 2745108287, // War Mantis 2765451288, // Synanceia 2765451289, // Synanceia 2765451290, // Synanceia 2765451291, // Synanceia 2771425787, // Melee Font 2788997987, // Void Resistance 2794359402, // Arc Dexterity 2801811288, // Stasis Holster 2803009638, // Cry Defiance 2803481901, // RPC Valiant 2805854721, // Strand Holster 2813695893, // Fatum Praevaricator 2814965254, // Aspirant Boots 2815743359, // Legion-Bane 2815817957, // Void Scavenger 2822491218, // Atonement Tau 2825160682, // RPC Valiant 2833813592, // Bond of Chiron 2854973517, // Farseeker's Casque 2871824910, // Mythos Hack 4.1 2880545163, // Black Shield Mark 2886651369, // Renegade Plate 2888021252, // Trachinus 2888021253, // Trachinus 2888021254, // Trachinus 2888021255, // Trachinus 2888195476, // Void Targeting 2893448006, // Farseeker's March 2930768301, // Wastelander Wraps 2937068650, // Chiron's Cure 2943629439, // Chiron's Cure 2959986506, // Prophet Snow 2982306509, // Power Preservation 2983961673, // Primal Siege Type 1 2985655620, // Refugee Vest 2994740249, // RPC Valiant 2996369932, // Elemental Charge 3007889693, // RPC Valiant 3013778406, // Strand Targeting 3035240099, // Shadow Specter 3046678542, // Arc Loader 3047946307, // Shield Break Charge 3061532064, // Farseeker's Intuition 3075302157, // Health Font 3080409700, // Bond of Forgotten Wars 3102366928, // Atonement Tau 3121104079, // Rite of Refusal 3159474701, // Aspirant Helm 3160437036, // Shadow Specter 3163241201, // Primal Siege Type 1 3164547673, // Atonement Tau 3174394351, // The Outlander's Grip 3174771856, // Stasis Scavenger 3181984586, // Charged Up 3183585337, // Legion-Bane 3184690956, // Absolution 3188328909, // Stasis Siphon 3212340413, // War Mantis 3224649746, // Void Loader 3238424670, // Memory of Cayde Mark 3245543337, // Bolstering Detonation 3260546749, // Cosmic Wind 3264653916, // Mythos Hack 4.1 3276278122, // Kinetic Holster 3279257734, // Strand Siphon 3294892432, // Stasis Ammo Generation 3302420523, // Hardcase Brawlers 3309120116, // Shadow Specter 3310450277, // Scavenger Suit 3313352164, // Cosmic Wind 3349439959, // Farseeker's Reach 3352069677, // Kit Fox 1.1 3382396922, // Primal Siege Type 1 3391214896, // Atonement Tau 3403897789, // Vector Home 3419425578, // Atonement Tau 3437155610, // War Mantis Cloak 3438103366, // Black Shield Mark 3456147612, // RPC Valiant 3456250548, // Stasis Resistance 3461249873, // Super Font 3465323600, // Legion-Bane 3468148580, // Aspirant Robes 3483602905, // Mark of Inquisition 3507639356, // Farseeker's Reach 3508205736, // Fatum Praevaricator 3519241547, // Fortress Field 3523134386, // Firebreak Field 3524846593, // Atonement Tau 3539253011, // Arc Resistance 3544711340, // Memory of Cayde Mark 3544884935, // Hood of Tallies 3550545621, // Stunloader 3554672786, // Memory of Cayde Cloak 3556023425, // Scavenger Cloak 3573886331, // Bond of Chiron 3585730968, // Shadow Specter 3598972737, // Unflinching Strand Aim 3639035739, // Mechanik 1.2 3643144047, // Wastelander Boots 3650925928, // Atgeir 2T1 3656549306, // Legion-Bane 3657186535, // Focusing Strike 3675553168, // Solar Holster 3693917763, // Mark of the Fire 3725709067, // Chiron's Cure 3748997649, // The Outlander's Steps 3763392098, // Hardcase Brawlers 3775800797, // Special Ammo Finder 3790903614, // Mechanik 2.1 3791691774, // Orbs of Restoration 3798468567, // Arc Holster 3808902618, // Weapons Mod 3812037372, // Aspirant Gloves 3846931924, // Solar Resistance 3847471926, // Arc Siphon 3867725217, // Legion-Bane 3877365781, // Kit Fox 1.4 3880804895, // The Outlander's Steps 3885104741, // Hardcase Battleplate 3887037435, // Unflinching Void Aim 3896141096, // Grenade Mod 3904524734, // The Outlander's Cover 3914973263, // Void Weapon Surge 3922069396, // The Outlander's Heart 3958133156, // Farseeker's Intuition 3962776002, // Hardcase Helm 3967705743, // Renegade Gauntlets 3968319087, // Legion-Bane 3979300428, // Strand Dexterity 4012302343, // Bond of Forgotten Wars 4035217656, // Atonement Tau 4052950089, // Shadow Specter 4062934448, // Primal Siege Type 1 4069941456, // Legion-Bane 4091127092, // Scavenger Suit 4100043028, // Wastelander Mask 4133705268, // Raven Shard 4135938411, // Last City Shell (Damaged) 4149682173, // Reaper 4155348771, // War Mantis 4166795065, // Primal Siege Type 1 4174470997, // Mark of Inquisition 4177795589, // Chiron's Cure 4179002916, // Mechanik 1.1 4195519897, // Refugee Cloak 4200817316, // Mark of the Renegade 4230626646, // Shadow Specter 4248632159, // Frumious Mask 4267244538, // Distribution 4267370571, // Chiron's Cure 4281850920, // Farseeker's Reach 4283953067, // Arc Ammo Generation 4288395850, // Cloak of Retelling ], cos: [ 17280095, // Shadow's Strides 256904954, // Shadow's Grips 309687341, // Shadow's Greaves 325125949, // Shadow's Helm 560455272, // Penumbral Mark 612065993, // Penumbral Mark 874272413, // Shadow's Robes 974648224, // Shadow's Boots 1434870610, // Shadow's Helm 1457195686, // Shadow's Gloves 1481751647, // Shadow's Mind 1862963733, // Shadow's Plate 1901223867, // Shadow's Gauntlets 1934647691, // Shadow's Mask 1937834292, // Shadow's Strides 1946621757, // Shadow's Grips 1999427172, // Shadow's Mask 2023695690, // Shadow's Robes 2153222031, // Shadow's Gloves 2194479195, // Penumbral Bond 2765688378, // Penumbral Cloak 2769298993, // Shadow's Boots 3082625196, // Shadow's Gauntlets 3108321700, // Penumbral Bond 3349283422, // Shadow's Mind 3483984579, // Shadow's Vest 3517729518, // Shadow's Vest 3518193943, // Penumbral Cloak 3759659288, // Shadow's Plate 4152814806, // Shadow's Greaves ], crownofsorrow: [ 17280095, // Shadow's Strides 256904954, // Shadow's Grips 309687341, // Shadow's Greaves 325125949, // Shadow's Helm 560455272, // Penumbral Mark 612065993, // Penumbral Mark 874272413, // Shadow's Robes 974648224, // Shadow's Boots 1434870610, // Shadow's Helm 1457195686, // Shadow's Gloves 1481751647, // Shadow's Mind 1862963733, // Shadow's Plate 1901223867, // Shadow's Gauntlets 1934647691, // Shadow's Mask 1937834292, // Shadow's Strides 1946621757, // Shadow's Grips 1999427172, // Shadow's Mask 2023695690, // Shadow's Robes 2153222031, // Shadow's Gloves 2194479195, // Penumbral Bond 2765688378, // Penumbral Cloak 2769298993, // Shadow's Boots 3082625196, // Shadow's Gauntlets 3108321700, // Penumbral Bond 3349283422, // Shadow's Mind 3483984579, // Shadow's Vest 3517729518, // Shadow's Vest 3518193943, // Penumbral Cloak 3759659288, // Shadow's Plate 4152814806, // Shadow's Greaves ], crucible: [ 85800627, // Ankaa Seeker IV 98331691, // Binary Phoenix Mark 120859138, // Phoenix Strife Type 0 185853176, // Wing Discipline 252414402, // Swordflight 4.1 283188616, // Wing Contender 290136582, // Wing Theorem 315615761, // Ankaa Seeker IV 327530279, // Wing Theorem 328902054, // Swordflight 4.1 356269375, // Wing Theorem 388771599, // Phoenix Strife Type 0 419812559, // Ankaa Seeker IV 438224459, // Wing Discipline 449878234, // Phoenix Strife Type 0 468899627, // Binary Phoenix Mark 494475253, // Ossuary Boots 530558102, // Phoenix Strife Type 0 628604416, // Ossuary Bond 631191162, // Ossuary Cover 636679949, // Ankaa Seeker IV 657400178, // Swordflight 4.1 670877864, // Binary Phoenix Mark 727838174, // Swordflight 4.1 744199039, // Wing Contender 761953100, // Ankaa Seeker IV 820446170, // Phoenix Strife Type 0 849529384, // Phoenix Strife Type 0 874101646, // Wing Theorem 876608500, // Ankaa Seeker IV 920187221, // Wing Discipline 929917162, // Wing Theorem 944242985, // Ankaa Seeker IV 979782821, // Hinterland Cloak 987343638, // Ankaa Seeker IV 997903134, // Wing Theorem 1036467370, // Wing Theorem 1062166003, // Wing Contender 1063904165, // Wing Discipline 1069887756, // Wing Contender 1071350799, // Binary Phoenix Cloak 1084033161, // Wing Contender 1127237110, // Wing Contender 1167444103, // Biosphere Explorer Mark 1245115841, // Wing Theorem 1294217731, // Binary Phoenix Cloak 1307478991, // Ankaa Seeker IV 1323862250, // Riptide 1330581478, // Phoenix Strife Type 0 1333087155, // Ankaa Seeker IV 1381742107, // Biosphere Explorer Helm 1464207979, // Wing Discipline 1467590642, // Binary Phoenix Bond 1484937602, // Phoenix Strife Type 0 1497354980, // Biosphere Explorer Greaves 1548928853, // Phoenix Strife Type 0 1571781304, // Swordflight 4.1 1648675919, // Binary Phoenix Mark 1654427223, // Swordflight 4.1 1658896287, // Binary Phoenix Cloak 1673285051, // Wing Theorem 1716643851, // Wing Contender 1722623780, // Wing Discipline 1742680797, // Binary Phoenix Mark 1742940528, // Phoenix Strife Type 0 1764274932, // Ankaa Seeker IV 1801625827, // Swordflight 4.1 1828358334, // Swordflight 4.1 1830829330, // Swordflight 4.1 1837817086, // Biosphere Explorer Plate 1838158578, // Binary Phoenix Bond 1838273186, // Wing Contender 1852468615, // Ankaa Seeker IV 1904811766, // Swordflight 4.1 1929596421, // Ankaa Seeker IV 2048762125, // Ossuary Robes 2070517134, // Wing Contender 2124666626, // Wing Discipline 2191401041, // Phoenix Strife Type 0 2191437287, // Ankaa Seeker IV 2206581692, // Phoenix Strife Type 0 2231762285, // Phoenix Strife Type 0 2247740696, // Swordflight 4.1 2291226602, // Binary Phoenix Bond 2293476915, // Swordflight 4.1 2296560252, // Swordflight 4.1 2296691422, // Swordflight 4.1 2323865727, // Wing Theorem 2331227463, // Wing Contender 2339694345, // Hinterland Cowl 2402428483, // Ossuary Gloves 2415711886, // Wing Contender 2426070307, // Binary Phoenix Cloak 2466453881, // Wing Discipline 2473130418, // Swordflight 4.1 2496309431, // Wing Discipline 2511045676, // Binary Phoenix Bond 2525395257, // Wing Theorem 2543903638, // Phoenix Strife Type 0 2555965565, // Wing Discipline 2627852659, // Phoenix Strife Type 0 2670393359, // Phoenix Strife Type 0 2718495762, // Swordflight 4.1 2727890395, // Ankaa Seeker IV 2754844215, // Swordflight 4.1 2775298636, // Ankaa Seeker IV 2815422368, // Phoenix Strife Type 0 2841023690, // Biosphere Explorer Gauntlets 3089908066, // Wing Discipline 3098328572, // The Recluse 3098458331, // Ankaa Seeker IV 3119528729, // Wing Contender 3121010362, // Hinterland Strides 3140634552, // Swordflight 4.1 3148195144, // Hinterland Vest 3211001969, // Wing Contender 3223280471, // Swordflight 4.1 3298826188, // Swordflight 4.1 3313736739, // Binary Phoenix Cloak 3315265682, // Phoenix Strife Type 0 3483546829, // Wing Discipline 3522021318, // Wing Discipline 3538513130, // Binary Phoenix Bond 3724026171, // Wing Theorem 3756286064, // Phoenix Strife Type 0 3772194440, // Wing Contender 3781722107, // Phoenix Strife Type 0 3818803676, // Wing Discipline 3839561204, // Wing Theorem 4043189888, // Hinterland Grips 4043921923, // The Mountaintop 4043980813, // Ankaa Seeker IV 4123918087, // Wing Contender 4134090375, // Ankaa Seeker IV 4136212668, // Wing Discipline 4144133120, // Wing Theorem 4211218181, // Ankaa Seeker IV 4264096388, // Wing Theorem ], deluxe: [ 1952218242, // Sequence Flourish 2683682447, // Traitor's Fate ], do: [ 66235782, // Anti-Extinction Grasps 132368575, // Anti-Extinction Mask 387100392, // Anti-Extinction Plate 1978760489, // Anti-Extinction Helm 2089197765, // Stella Incognita Mark 2760076378, // Anti-Extinction Greaves 2873960175, // Anti-Extinction Robes 3146241834, // Anti-Extinction Vest 3299588760, // Anti-Extinction Hood 3763392361, // Anti-Extinction Gloves 3783059515, // Stella Incognita Cloak 3920232320, // Anti-Extinction Legs 4055334203, // Anti-Extinction Boots 4065136800, // Anti-Extinction Gauntlets 4121118846, // Stella Incognita Bond ], dreaming: [ 99549082, // Reverie Dawn Helm 185695659, // Reverie Dawn Hood 188778964, // Reverie Dawn Boots 344548395, // Reverie Dawn Strides 871900124, // Retold Tale 934704429, // Reverie Dawn Plate 998096007, // Reverie Dawn Hood 1018012078, // Horror's Least 1452333832, // Reverie Dawn Boots 1593474975, // Reverie Dawn Hauberk 1705856569, // Reverie Dawn Grasps 1903023095, // Reverie Dawn Grasps 1928769139, // Reverie Dawn Bond 1980768298, // Reverie Dawn Mark 2336820707, // Reverie Dawn Gauntlets 2467635521, // Reverie Dawn Hauberk 2503434573, // Reverie Dawn Gauntlets 2704876322, // Reverie Dawn Tabard 2761343386, // Reverie Dawn Gloves 2824453288, // Reverie Dawn Casque 2859583726, // Reverie Dawn Tabard 2889063206, // Reverie Dawn Casque 3174233615, // Reverie Dawn Greaves 3239662350, // Reverie Dawn Gloves 3250140572, // Reverie Dawn Cloak 3306564654, // Reverie Dawn Cloak 3343583008, // Reverie Dawn Mark 3602032567, // Reverie Dawn Bond 3711557785, // Reverie Dawn Strides 4070309619, // Reverie Dawn Plate 4097166900, // Reverie Dawn Helm 4257800469, // Reverie Dawn Greaves ], drifter: [ 9767416, // Ancient Apocalypse Bond 94425673, // Ancient Apocalypse Gloves 127018032, // Ancient Apocalypse Grips 191247558, // Ancient Apocalypse Plate 191535001, // Ancient Apocalypse Greaves 230878649, // Ancient Apocalypse Mask 386367515, // Ancient Apocalypse Boots 392058749, // Ancient Apocalypse Boots 485653258, // Ancient Apocalypse Strides 494475253, // Ossuary Boots 509238959, // Ancient Apocalypse Mark 628604416, // Ossuary Bond 629787707, // Ancient Apocalypse Mask 631191162, // Ossuary Cover 759348512, // Ancient Apocalypse Mask 787909455, // Ancient Apocalypse Robes 887818405, // Ancient Apocalypse Robes 978447246, // Ancient Apocalypse Gauntlets 979782821, // Hinterland Cloak 1013137701, // Ancient Apocalypse Hood 1167444103, // Biosphere Explorer Mark 1169857924, // Ancient Apocalypse Strides 1188039652, // Ancient Apocalypse Gauntlets 1193646249, // Ancient Apocalypse Boots 1236746902, // Ancient Apocalypse Hood 1237661249, // Ancient Apocalypse Plate 1356064950, // Ancient Apocalypse Grips 1359908066, // Ancient Apocalypse Gauntlets 1381742107, // Biosphere Explorer Helm 1488486721, // Ancient Apocalypse Bond 1497354980, // Biosphere Explorer Greaves 1548620661, // Ancient Apocalypse Cloak 1741396519, // Ancient Apocalypse Vest 1752237812, // Ancient Apocalypse Gloves 1837817086, // Biosphere Explorer Plate 2020166300, // Ancient Apocalypse Mark 2039976446, // Ancient Apocalypse Boots 2048762125, // Ossuary Robes 2088829612, // Ancient Apocalypse Bond 2130645994, // Ancient Apocalypse Grips 2339694345, // Hinterland Cowl 2402428483, // Ossuary Gloves 2440840551, // Ancient Apocalypse Gloves 2451538755, // Ancient Apocalypse Strides 2459422430, // Ancient Apocalypse Bond 2506514251, // Ancient Apocalypse Cloak 2512196373, // Ancient Apocalypse Helm 2518527196, // Ancient Apocalypse Plate 2568447248, // Ancient Apocalypse Strides 2620389105, // Ancient Apocalypse Grips 2677967607, // Ancient Apocalypse Gauntlets 2694124942, // Ancient Apocalypse Greaves 2728668760, // Ancient Apocalypse Vest 2841023690, // Biosphere Explorer Gauntlets 2858060922, // Ancient Apocalypse Vest 2881248566, // Ancient Apocalypse Cloak 3031848199, // Ancient Apocalypse Helm 3121010362, // Hinterland Strides 3148195144, // Hinterland Vest 3184912423, // Ancient Apocalypse Cloak 3339632627, // Ancient Apocalypse Mark 3404053788, // Ancient Apocalypse Greaves 3486086024, // Ancient Apocalypse Greaves 3537476911, // Ancient Apocalypse Mask 3550729740, // Ancient Apocalypse Robes 3595268459, // Ancient Apocalypse Gloves 3664007718, // Ancient Apocalypse Helm 3804360785, // Ancient Apocalypse Mark 3825427923, // Ancient Apocalypse Helm 3855285278, // Ancient Apocalypse Vest 3925589496, // Ancient Apocalypse Hood 4043189888, // Hinterland Grips 4115739810, // Ancient Apocalypse Plate 4188366993, // Ancient Apocalypse Robes 4255727106, // Ancient Apocalypse Hood ], duality: [ 145651147, // Deep Explorer Vest 420895300, // Deep Explorer Mark 1148597205, // Deep Explorer Grasps 2057955626, // Deep Explorer Vestments 2499351855, // Deep Explorer Gauntlets 2649394513, // Deep Explorer Greaves 2694773307, // Deep Explorer Bond 2724719415, // Deep Explorer Strides 2797334754, // Deep Explorer Cloak 2819810688, // Deep Explorer Boots 2935559305, // Deep Explorer Plate 3260781446, // Deep Explorer Gloves 3270955774, // Deep Explorer Helmet 3326914239, // Deep Explorer Hood 4047213660, // Deep Explorer Mask ], dungeon: [ 51786498, // Vest of the Taken King 145651147, // Deep Explorer Vest 286271818, // Twisting Echo Cloak 399065241, // Descending Echo Greaves 420895300, // Deep Explorer Mark 436695703, // TM-Cogburn Custom Plate 498918879, // TM-Earp Custom Grips 557092665, // Dark Age Cloak 587312237, // Twisting Echo Grips 632989816, // Dark Age Gauntlets 638836294, // Plate of the Taken King 708921139, // TM-Cogburn Custom Legguards 767306222, // Grasps of the Taken King 806004493, // Dark Age Gloves 833653807, // Twisting Echo Strides 837865641, // Vestment of the Taken King 851401651, // Dark Age Overcoat 956827695, // Mark of the Taken King 1148597205, // Deep Explorer Grasps 1349399252, // TM-Earp Custom Cloaked Stetson 1460079227, // Liminal Vigil 1476803535, // Dark Age Legbraces 1664757090, // Gauntlets of the Taken King 1756483796, // Twisting Echo Mask 1773934241, // Judgment 1904170910, // A Sudden Death 1913823311, // Gloves of the Taken King 1933599476, // Dark Age Visor 1951355667, // Twisting Echo Vest 2057955626, // Deep Explorer Vestments 2129814338, // Prosecutor 2244604734, // Corrupting Echo Gloves 2341879253, // TM-Moss Custom Bond 2426502022, // Dark Age Strides 2488323569, // Boots of the Taken King 2499351855, // Deep Explorer Gauntlets 2565015142, // TM-Cogburn Custom Mark 2618168932, // Bond of the Taken King 2643850526, // Hood of the Taken King 2649394513, // Deep Explorer Greaves 2662590925, // Dark Age Mark 2663987096, // Corrupting Echo Boots 2694773307, // Deep Explorer Bond 2724719415, // Deep Explorer Strides 2760833884, // Cold Comfort 2771011469, // Dark Age Mask 2797334754, // Deep Explorer Cloak 2819810688, // Deep Explorer Boots 2820604007, // Mask of the Taken King 2850384360, // Strides of the Taken King 2885497847, // Descending Echo Gauntlets 2935559305, // Deep Explorer Plate 2963224754, // Dark Age Sabatons 2982006965, // Wilderflight 3048458482, // Corrupting Echo Robes 3056827626, // Dark Age Bond 3171090615, // Corrupting Echo Cover 3260781446, // Deep Explorer Gloves 3267969345, // Descending Echo Cage 3270955774, // Deep Explorer Helmet 3326914239, // Deep Explorer Hood 3344225390, // TM-Earp Custom Hood 3423574140, // Dark Age Grips 3511740432, // TM-Moss Custom Gloves 3570749779, // Cloak of the Taken King 3683772388, // Dark Age Harness 3685276035, // Corrupting Echo Bond 3708902812, // Greaves of the Taken King 3715136417, // TM-Earp Custom Chaps 3735435664, // Dark Age Chestrig 3870375786, // TM-Moss Custom Pants 3871537958, // Descending Echo Helm 3933500353, // TM-Cogburn Custom Gauntlets 3946384952, // TM-Moss Custom Duster 4039955353, // TM-Moss Custom Hat 4047213660, // Deep Explorer Mask 4050474396, // Descending Echo Mark 4090037601, // Dark Age Helm 4097972038, // A Sudden Death 4130276947, // Helm of the Taken King 4177293424, // TM-Cogburn Custom Cover 4288623897, // TM-Earp Custom Vest ], edgeoffate: [ 407150808, // Ribbontail 407150809, // Ribbontail 407150810, // Ribbontail 407150811, // Ribbontail 2765451288, // Synanceia 2765451289, // Synanceia 2765451290, // Synanceia 2765451291, // Synanceia 2888021252, // Trachinus 2888021253, // Trachinus 2888021254, // Trachinus 2888021255, // Trachinus ], edz: [ 10307688, // Wildwood Plate 11686458, // Orobas Vectura Cloak 320310249, // Orobas Vectura Bond 872284448, // Wildwood Gauntlets 1304122208, // Wildwood Bond 1664741411, // Wildwood Gloves 1701005143, // Gearhead Gloves 1712405061, // Wildwood Mark 2426340788, // Orobas Vectura Mark 2486041712, // Gearhead Gauntlets 2724176749, // Wildwood Robes 2729740202, // Wildwood Vest 3080875433, // Wildwood Helm 3366557883, // Wildwood Cloak 3466255616, // Wildwood Strides 3706457514, // Gearhead Grips 3764013786, // Wildwood Cover 3862191322, // Wildwood Greaves 3907226374, // Wildwood Grips 3973359167, // Wildwood Mask 4051755349, // Wildwood Boots ], eow: [ 239489770, // Bond of Sekris 253344425, // Mask of Feltroc 340118991, // Boots of Sekris 383742277, // Cloak of Feltroc 588627781, // Bond of Sekris 666883012, // Gauntlets of Nohr 796914932, // Mask of Sekris 845536715, // Vest of Feltroc 1034660314, // Boots of Feltroc 1242139836, // Plate of Nohr 1256688732, // Mask of Feltroc 1756558505, // Mask of Sekris 1991039861, // Mask of Nohr 2329031091, // Robes of Sekris 2339720736, // Grips of Feltroc 2369496221, // Plate of Nohr 2537874394, // Boots of Sekris 2597529070, // Greaves of Nohr 2653039573, // Grips of Feltroc 2976612200, // Vest of Feltroc 2994007601, // Mark of Nohr 3099636805, // Greaves of Nohr 3181497704, // Robes of Sekris 3359121706, // Mask of Nohr 3364682867, // Gauntlets of Nohr 3497220322, // Cloak of Feltroc 3831484112, // Mark of Nohr 3842934816, // Wraps of Sekris 3964287245, // Wraps of Sekris 4229161783, // Boots of Feltroc ], events: [ 72775246, // Gunburn 76739872, // The Beacon 116784191, // Solstice Boots (Renewed) 140842223, // Solstice Mask (Drained) 143299650, // Solstice Plate (Renewed) 153144587, // Solstice Cloak (Drained) 179654396, // Sublime Vest 226436555, // Solstice Mask (Renewed) 231432261, // Solstice Bond (Resplendent) 234970842, // Solstice Boots (Resplendent) 250513201, // Solstice Greaves (Resplendent) 270610849, // Mistral Lift 327680457, // Sublime Boots 335763433, // Solstice Plate (Resplendent) 346065606, // Solstice Cloak (Rekindled) 350054538, // Sublime Strides 391889347, // Solstice Robes (Drained) 419435523, // Inaugural Revelry Grips 425681240, // Acosmic 437854388, // Action Item 437854389, // Action Item 437854390, // Action Item 437854391, // Action Item 450844637, // Solstice Robes (Majestic) 464814870, // Sublime Greaves 492834021, // Inaugural Revelry Hood 495940989, // Avalanche 503145555, // Sublime Gloves 518930465, // Solstice Grasps (Rekindled) 531005896, // Solstice Cloak (Resplendent) 540653483, // Solstice Vest (Scorched) 574167778, // Solstice Gauntlets (Drained) 574790717, // Solstice Gloves (Drained) 591672323, // Fortunate Star 601948197, // Zephyr 602331464, // Micromort 602331465, // Micromort 602331466, // Micromort 602331467, // Micromort 617566156, // The Heron 617566157, // The Heron 617566158, // The Heron 617566159, // The Heron 627596132, // Solstice Hood (Drained) 677939288, // Solstice Helm (Scorched) 689294985, // Jurassic Green 721146704, // Solstice Mask (Rekindled) 784499738, // Solstice Bond (Renewed) 784540300, // King Orfeo 784540301, // King Orfeo 784540302, // King Orfeo 784540303, // King Orfeo 807693916, // Sublime Hood 830497630, // Solstice Helm (Resplendent) 929148730, // Solstice Vest (Drained) 967650555, // Solstice Greaves (Scorched) 981450701, // Keraunios 1056992393, // Inaugural Revelry Plate 1123433952, // Stay Frosty 1141639721, // Solstice Gauntlets (Scorched) 1183116657, // Glacioclasm 1229961870, // Solstice Vest (Renewed) 1273510836, // Inaugural Revelry Wraps 1280894514, // Mechabre 1288683596, // Solstice Plate (Majestic) 1341471164, // Solstice Mask (Scorched) 1361620030, // Solstice Mark (Scorched) 1365491398, // Solstice Plate (Drained) 1376763596, // Inaugural Revelry Robes 1450633717, // Solstice Vest (Resplendent) 1502692899, // Solstice Robes (Renewed) 1510405477, // Solstice Helm (Majestic) 1540031264, // Solstice Gloves (Resplendent) 1548056407, // Solstice Cloak (Renewed) 1556831535, // Inaugural Revelry Gauntlets 1561249470, // Inaugural Revelry Boots 1572604081, // Gizmo Weft 1589318419, // Solstice Strides (Rekindled) 1609141880, // Sublime Plate 1649929380, // Solstice Mark (Resplendent) 1651275175, // Solstice Helm (Renewed) 1683482799, // Solstice Mark (Drained) 1706764072, // Quilted Winter Mark 1706874193, // Inaugural Revelry Greaves 1724104236, // Submersion 1752648948, // Sublime Sleeves 1775707016, // Solstice Grasps (Majestic) 1812385587, // Festive Winter Bond 1813474267, // Arcane Embrace 1845372864, // Albedo Wing 1845978721, // Avalanche 1862324869, // Solstice Boots (Majestic) 1897528210, // Solstice Robes (Scorched) 2079349511, // Sublime Helm 2105409832, // Solstice Greaves (Renewed) 2111111693, // Solstice Strides (Resplendent) 2120905920, // Inaugural Revelry Cloak 2127474099, // Solstice Gloves (Majestic) 2150778206, // Solstice Gloves (Scorched) 2155928170, // Solstice Mark (Rekindled) 2156817213, // Solstice Cloak (Majestic) 2223901117, // Allstar Vector 2261046232, // Jurassic Green 2287277682, // Solstice Robes (Rekindled) 2291082292, // Solstice Gauntlets (Majestic) 2316331767, // Permafrost 2328435454, // Inaugural Revelry Helm 2337290000, // Solstice Bond (Majestic) 2419100474, // Solstice Grasps (Renewed) 2470583197, // Solstice Gloves (Renewed) 2477028154, // Inaugural Revelry Mask 2477980485, // Mechabre 2492769187, // Solstice Bond (Scorched) 2523388612, // Solstice Hood (Renewed) 2543971899, // Sublime Mark 2546370410, // Solstice Hood (Majestic) 2578820926, // Solstice Greaves (Majestic) 2603335652, // Jurassic Green 2616697701, // Sublime Robes 2618313500, // Solstice Greaves (Drained) 2645567209, // Fimbulwinter Stitch 2685001662, // Solstice Gloves (Rekindled) 2696245301, // Solstice Grasps (Scorched) 2720534902, // Solstice Grasps (Drained) 2764769717, // Inaugural Revelry Strides 2770157746, // Solstice Mask (Resplendent) 2777913564, // Warm Winter Cloak 2805101184, // Solstice Vest (Majestic) 2812100428, // Stay Frosty 2814093983, // Cold Front 2824302184, // Solstice Robes (Resplendent) 2837295684, // Inaugural Revelry Mark 2869466318, // BrayTech Werewolf 2877046370, // Solstice Strides (Majestic) 2908653246, // Triple Laureate 2924095235, // Solstice Bond (Rekindled) 2940416351, // Solstice Boots (Drained) 2965080304, // Yeartide Apex 2978747767, // Solstice Vest (Rekindled) 2994721336, // Solstice Boots (Scorched) 3015197581, // Solstice Gauntlets (Rekindled) 3039687635, // Solstice Helm (Drained) 3077367255, // Solstice Hood (Scorched) 3104384024, // Solstice Boots (Rekindled) 3159052337, // Solstice Mask (Majestic) 3192336962, // Solstice Cloak (Scorched) 3232831379, // Sublime Mask 3236510875, // Solstice Grasps (Resplendent) 3240434620, // Something New 3328019216, // Arcane Embrace 3329514528, // Sublime Gauntlets 3400256755, // Zephyr 3558681245, // BrayTech Werewolf 3559361670, // The Title 3573686365, // Glacioclasm 3574168117, // Hushed Whisper 3611487543, // Solstice Hood (Rekindled) 3685996623, // Solstice Greaves (Rekindled) 3748622249, // Solstice Hood (Resplendent) 3804242792, // Phoneutria Fera 3804242793, // Phoneutria Fera 3804242794, // Phoneutria Fera 3804242795, // Phoneutria Fera 3809902215, // Sublime Cloak 3829285960, // Horror Story 3892841518, // Solstice Gauntlets (Renewed) 3929403535, // Solstice Gauntlets (Resplendent) 3932814032, // Solstice Strides (Drained) 3943394479, // Solstice Plate (Scorched) 3965417933, // Inaugural Revelry Vest 3968560442, // Solstice Bond (Drained) 3970040886, // Sublime Bond 3977654524, // Festival Flight 3987442049, // Solstice Mark (Majestic) 4075522049, // Inaugural Revelry Bond 4100029812, // Solstice Strides (Renewed) 4128297107, // Solstice Mark (Renewed) 4142792564, // Solstice Helm (Rekindled) 4245469491, // Solstice Plate (Rekindled) 4272367383, // Solstice Strides (Scorched) ], eververse: [ 80338527, // Flutter By 138961800, // Helm of Optimacy 163660481, // Bond of Optimacy 167651268, // Crimson Passion 269339124, // Dawning Hope 599687980, // Purple Dawning Lanterns 639457414, // Necrosis 691914261, // Silver Dawning Lanterns 706111909, // Hood of Optimacy 710937567, // Legs of Optimacy 921357268, // Winterhart Plate 989291706, // Cloak of Optimacy 1051903593, // Dawning Bauble Shell 1126785375, // Great White 1135293055, // Plate of Optimacy 1290784012, // Winterhart Gauntlets 1397284432, // Jasper Dawn Shell 1445212020, // Arms of Optimacy 1602334068, // Regent Redeemer 1706764073, // Winterhart Mark 1707587907, // Vest of Optimacy 1732950654, // Legs of Optimacy 1812385586, // Winterhart Bond 1816495538, // Sweet Memories Shell 1844125034, // Dawning Festiveness 1936516278, // Winterhart Greaves 1956273477, // Winterhart Gloves 1984190529, // Magikon 2112889975, // Crimson Valor 2225903500, // Robes of Optimacy 2303499975, // Winterhart Boots 2378378507, // Legs of Optimacy 2623660327, // Dawning Brilliance 2640279229, // Arms of Optimacy 2693084644, // Mask of Optimacy 2717158440, // Winterhart Grips 2760398988, // Winterhart Cover 2777913565, // Winterhart Cloak 2806805902, // Mark of Optimacy 2828252061, // Winterhart Helm 2998296658, // Ice Ball Effects 3086696388, // Itsy-Bitsy Spider 3161524490, // Rupture 3168164098, // Yellow Dawning Lanterns 3177119978, // Carmina Commencing 3352566658, // Winterhart Strides 3455566107, // Winterhart Robes 3569791559, // Shimmering Iris 3729709035, // Joyfire 3781263385, // Arms of Optimacy 3850655136, // Winterhart Vest 3866715933, // Dawning Warmth 3916428099, // Holochip Extractor 3947596543, // Green Dawning Lanterns 4059030097, // Winterhart Mask ], fwc: [ 680327840, // Simulator Greaves 807866445, // Simulator Gloves 1162875302, // Simulator Gauntlets 1187431263, // Simulator Helm 1355893732, // Simulator Vest 1418921862, // Simulator Boots 1478665487, // Simulator Legs 1566612778, // Entanglement Bond 1763431309, // Simulator Mask 2401598772, // Simulator Hood 2415993980, // Simulator Grips 2524181305, // Entanglement Cloak 3656154099, // Simulator Robes 3671665226, // Simulator Plate 3842448731, // Entanglement Mark ], gambit: [ 9767416, // Ancient Apocalypse Bond 94425673, // Ancient Apocalypse Gloves 127018032, // Ancient Apocalypse Grips 191247558, // Ancient Apocalypse Plate 191535001, // Ancient Apocalypse Greaves 230878649, // Ancient Apocalypse Mask 386367515, // Ancient Apocalypse Boots 392058749, // Ancient Apocalypse Boots 485653258, // Ancient Apocalypse Strides 494475253, // Ossuary Boots 509238959, // Ancient Apocalypse Mark 628604416, // Ossuary Bond 629787707, // Ancient Apocalypse Mask 631191162, // Ossuary Cover 759348512, // Ancient Apocalypse Mask 787909455, // Ancient Apocalypse Robes 887818405, // Ancient Apocalypse Robes 978447246, // Ancient Apocalypse Gauntlets 979782821, // Hinterland Cloak 1013137701, // Ancient Apocalypse Hood 1167444103, // Biosphere Explorer Mark 1169857924, // Ancient Apocalypse Strides 1188039652, // Ancient Apocalypse Gauntlets 1193646249, // Ancient Apocalypse Boots 1236746902, // Ancient Apocalypse Hood 1237661249, // Ancient Apocalypse Plate 1356064950, // Ancient Apocalypse Grips 1359908066, // Ancient Apocalypse Gauntlets 1381742107, // Biosphere Explorer Helm 1488486721, // Ancient Apocalypse Bond 1497354980, // Biosphere Explorer Greaves 1548620661, // Ancient Apocalypse Cloak 1741396519, // Ancient Apocalypse Vest 1752237812, // Ancient Apocalypse Gloves 1837817086, // Biosphere Explorer Plate 2020166300, // Ancient Apocalypse Mark 2039976446, // Ancient Apocalypse Boots 2048762125, // Ossuary Robes 2088829612, // Ancient Apocalypse Bond 2130645994, // Ancient Apocalypse Grips 2339694345, // Hinterland Cowl 2402428483, // Ossuary Gloves 2440840551, // Ancient Apocalypse Gloves 2451538755, // Ancient Apocalypse Strides 2459422430, // Ancient Apocalypse Bond 2506514251, // Ancient Apocalypse Cloak 2512196373, // Ancient Apocalypse Helm 2518527196, // Ancient Apocalypse Plate 2568447248, // Ancient Apocalypse Strides 2620389105, // Ancient Apocalypse Grips 2677967607, // Ancient Apocalypse Gauntlets 2694124942, // Ancient Apocalypse Greaves 2728668760, // Ancient Apocalypse Vest 2841023690, // Biosphere Explorer Gauntlets 2858060922, // Ancient Apocalypse Vest 2881248566, // Ancient Apocalypse Cloak 3031848199, // Ancient Apocalypse Helm 3121010362, // Hinterland Strides 3148195144, // Hinterland Vest 3184912423, // Ancient Apocalypse Cloak 3339632627, // Ancient Apocalypse Mark 3404053788, // Ancient Apocalypse Greaves 3486086024, // Ancient Apocalypse Greaves 3537476911, // Ancient Apocalypse Mask 3550729740, // Ancient Apocalypse Robes 3595268459, // Ancient Apocalypse Gloves 3664007718, // Ancient Apocalypse Helm 3804360785, // Ancient Apocalypse Mark 3825427923, // Ancient Apocalypse Helm 3855285278, // Ancient Apocalypse Vest 3925589496, // Ancient Apocalypse Hood 4043189888, // Hinterland Grips 4115739810, // Ancient Apocalypse Plate 4188366993, // Ancient Apocalypse Robes 4255727106, // Ancient Apocalypse Hood ], gambitprime: [ 95332289, // Notorious Collector Strides 95332290, // Outlawed Collector Strides 98700833, // Outlawed Reaper Cloak 98700834, // Notorious Reaper Cloak 130287073, // Notorious Sentry Gauntlets 130287074, // Outlawed Sentry Gauntlets 154180149, // Outlawed Sentry Cloak 154180150, // Notorious Sentry Cloak 223681332, // Notorious Reaper Helm 223681335, // Outlawed Reaper Helm 234582861, // Outlawed Reaper Mark 234582862, // Notorious Reaper Mark 264182640, // Outlawed Collector Grips 264182643, // Notorious Collector Grips 370332340, // Notorious Collector Cloak 370332343, // Outlawed Collector Cloak 420625860, // Outlawed Invader Plate 420625863, // Notorious Invader Plate 432797516, // Outlawed Collector Bond 432797519, // Notorious Collector Bond 563461320, // Outlawed Reaper Greaves 563461323, // Notorious Reaper Greaves 722344177, // Outlawed Reaper Gloves 722344178, // Notorious Reaper Gloves 759881004, // Outlawed Sentry Plate 759881007, // Notorious Sentry Plate 893169981, // Outlawed Invader Cloak 893169982, // Notorious Invader Cloak 975478397, // Outlawed Collector Helm 975478398, // Notorious Collector Helm 1039402696, // Notorious Reaper Boots 1039402699, // Outlawed Reaper Boots 1159077396, // Outlawed Reaper Strides 1159077399, // Notorious Reaper Strides 1208982392, // Outlawed Reaper Hood 1208982395, // Notorious Reaper Hood 1295793304, // Notorious Reaper Mask 1295793307, // Outlawed Reaper Mask 1386198149, // Notorious Reaper Gauntlets 1386198150, // Outlawed Reaper Gauntlets 1438999856, // Notorious Collector Boots 1438999859, // Outlawed Collector Boots 1477025072, // Outlawed Sentry Bond 1477025075, // Notorious Sentry Bond 1505642257, // Outlawed Collector Robes 1505642258, // Notorious Collector Robes 1920676413, // Notorious Invader Bond 1920676414, // Outlawed Invader Bond 1951201409, // Notorious Invader Hood 1951201410, // Outlawed Invader Hood 1979001652, // Outlawed Reaper Bond 1979001655, // Notorious Reaper Bond 1984789548, // Outlawed Reaper Vest 1984789551, // Notorious Reaper Vest 1989814421, // Notorious Invader Grips 1989814422, // Outlawed Invader Grips 2051266836, // Outlawed Sentry Greaves 2051266839, // Notorious Sentry Greaves 2187982744, // Notorious Sentry Helm 2187982747, // Outlawed Sentry Helm 2334120368, // Outlawed Reaper Plate 2334120371, // Notorious Reaper Plate 2336344261, // Outlawed Sentry Gloves 2336344262, // Notorious Sentry Gloves 2371932404, // Outlawed Collector Gauntlets 2371932407, // Notorious Collector Gauntlets 2565812704, // Outlawed Collector Hood 2565812707, // Notorious Collector Hood 2591049236, // Notorious Invader Robes 2591049239, // Outlawed Invader Robes 2593076932, // Notorious Invader Mask 2593076935, // Outlawed Invader Mask 2698109345, // Outlawed Collector Mask 2698109346, // Notorious Collector Mask 2710420856, // Outlawed Sentry Vest 2710420859, // Notorious Sentry Vest 2799932928, // Notorious Collector Mark 2799932931, // Outlawed Collector Mark 2976484617, // Notorious Invader Gauntlets 2976484618, // Outlawed Invader Gauntlets 3088740176, // Notorious Invader Gloves 3088740179, // Outlawed Invader Gloves 3166483968, // Outlawed Sentry Strides 3166483971, // Notorious Sentry Strides 3168759585, // Outlawed Sentry Mark 3168759586, // Notorious Sentry Mark 3220030412, // Notorious Sentry Mask 3220030415, // Outlawed Sentry Mask 3373994936, // Outlawed Invader Strides 3373994939, // Notorious Invader Strides 3403732217, // Outlawed Collector Gloves 3403732218, // Notorious Collector Gloves 3489978605, // Outlawed Invader Boots 3489978606, // Notorious Invader Boots 3525447589, // Notorious Collector Vest 3525447590, // Outlawed Collector Vest 3533064929, // Notorious Reaper Grips 3533064930, // Outlawed Reaper Grips 3583507225, // Outlawed Reaper Robes 3583507226, // Notorious Reaper Robes 3636943392, // Notorious Invader Helm 3636943395, // Outlawed Invader Helm 3660501108, // Outlawed Sentry Hood 3660501111, // Notorious Sentry Hood 3837542169, // Outlawed Invader Mark 3837542170, // Notorious Invader Mark 3948054485, // Notorious Collector Greaves 3948054486, // Outlawed Collector Greaves 3981071584, // Outlawed Invader Vest 3981071587, // Notorious Invader Vest 4020124605, // Outlawed Sentry Robes 4020124606, // Notorious Sentry Robes 4026665500, // Outlawed Invader Greaves 4026665503, // Notorious Invader Greaves 4060232809, // Notorious Collector Plate 4060232810, // Outlawed Collector Plate 4245233853, // Notorious Sentry Grips 4245233854, // Outlawed Sentry Grips 4266990316, // Notorious Sentry Boots 4266990319, // Outlawed Sentry Boots ], garden: [ 11974904, // Greaves of Ascendancy 281660259, // Temptation's Mark 519078295, // Helm of Righteousness 557676195, // Cowl of Righteousness 1653741426, // Grips of Exaltation 2015894615, // Gloves of Exaltation 2054979724, // Strides of Ascendancy 2320830625, // Robes of Transcendence 3001934726, // Mask of Righteousness 3103335676, // Temptation's Bond 3388655311, // Tyranny of Heaven 3549177695, // Cloak of Temptation 3824429433, // Boots of Ascendancy 3887559710, // Gauntlets of Exaltation 3939809874, // Plate of Transcendence 4177973942, // Vest of Transcendence ], gardenofsalvation: [ 11974904, // Greaves of Ascendancy 281660259, // Temptation's Mark 519078295, // Helm of Righteousness 557676195, // Cowl of Righteousness 1653741426, // Grips of Exaltation 2015894615, // Gloves of Exaltation 2054979724, // Strides of Ascendancy 2320830625, // Robes of Transcendence 3001934726, // Mask of Righteousness 3103335676, // Temptation's Bond 3388655311, // Tyranny of Heaven 3549177695, // Cloak of Temptation 3824429433, // Boots of Ascendancy 3887559710, // Gauntlets of Exaltation 3939809874, // Plate of Transcendence 4177973942, // Vest of Transcendence ], ghostsofthedeep: [ 51786498, // Vest of the Taken King 638836294, // Plate of the Taken King 767306222, // Grasps of the Taken King 837865641, // Vestment of the Taken King 956827695, // Mark of the Taken King 1664757090, // Gauntlets of the Taken King 1913823311, // Gloves of the Taken King 2488323569, // Boots of the Taken King 2618168932, // Bond of the Taken King 2643850526, // Hood of the Taken King 2760833884, // Cold Comfort 2820604007, // Mask of the Taken King 2850384360, // Strides of the Taken King 3570749779, // Cloak of the Taken King 3708902812, // Greaves of the Taken King 4130276947, // Helm of the Taken King ], gos: [ 11974904, // Greaves of Ascendancy 281660259, // Temptation's Mark 519078295, // Helm of Righteousness 557676195, // Cowl of Righteousness 1653741426, // Grips of Exaltation 2015894615, // Gloves of Exaltation 2054979724, // Strides of Ascendancy 2320830625, // Robes of Transcendence 3001934726, // Mask of Righteousness 3103335676, // Temptation's Bond 3388655311, // Tyranny of Heaven 3549177695, // Cloak of Temptation 3824429433, // Boots of Ascendancy 3887559710, // Gauntlets of Exaltation 3939809874, // Plate of Transcendence 4177973942, // Vest of Transcendence ], gotd: [ 51786498, // Vest of the Taken King 638836294, // Plate of the Taken King 767306222, // Grasps of the Taken King 837865641, // Vestment of the Taken King 956827695, // Mark of the Taken King 1664757090, // Gauntlets of the Taken King 1913823311, // Gloves of the Taken King 2488323569, // Boots of the Taken King 2618168932, // Bond of the Taken King 2643850526, // Hood of the Taken King 2760833884, // Cold Comfort 2820604007, // Mask of the Taken King 2850384360, // Strides of the Taken King 3570749779, // Cloak of the Taken King 3708902812, // Greaves of the Taken King 4130276947, // Helm of the Taken King ], grasp: [ 286271818, // Twisting Echo Cloak 399065241, // Descending Echo Greaves 587312237, // Twisting Echo Grips 833653807, // Twisting Echo Strides 1756483796, // Twisting Echo Mask 1951355667, // Twisting Echo Vest 2244604734, // Corrupting Echo Gloves 2663987096, // Corrupting Echo Boots 2885497847, // Descending Echo Gauntlets 3048458482, // Corrupting Echo Robes 3171090615, // Corrupting Echo Cover 3267969345, // Descending Echo Cage 3685276035, // Corrupting Echo Bond 3871537958, // Descending Echo Helm 4050474396, // Descending Echo Mark ], haunted: [ 3864896927, // Nightmare Harvester ], heresy: [ 267671509, // Skull of the Flain 571745874, // Hooks of the Flain 601574723, // Adamantite (Adept) 608948636, // Carapace of the Flain 615373993, // Eyes Unveiled 704410186, // Psychopomp (Adept) 768610585, // Watchful Eye 861521336, // Afterlight (Adept) 874160718, // Claws of the Flain 1274101249, // Mask of the Flain 1834313033, // Afterlight 1930656621, // Husk's Cloak 2112020760, // Grasps of the Flain 2299285295, // Adornment of the Flain 2319342865, // Attendant's Mark 2501377328, // Division (Adept) 2501618648, // Visage of the Flain 2578940720, // Scales of the Flain 2671849376, // Refusal of the Call 2755584425, // Refusal of the Call (Adept) 2791329915, // Talons of the Flain 2856225832, // Watchful Eye (Adept) 2965319081, // Reach of the Flain 2987244302, // Adamantite 2992463569, // Division 3054597646, // Abyssal Edge (Adept) 3238482084, // Grips of the Flain 3417731926, // Anamnesis 3840794631, // Psychopomp 3877448149, // Mirror Imago 3949253499, // Anamnesis (Adept) 4012478142, // Weaver's Bond 4116546788, // Mirror Imago (Adept) 4173311704, // Eyes Unveiled (Adept) 4221591387, // Abyssal Edge ], ikora: [ 89175653, // Noble Constant Mark 185326970, // Noble Constant Type 2 385045066, // Frumious Vest 555828571, // Frumious Cloak 662797277, // Frumious Cloak 868792277, // Ego Talon IV 1490387264, // Noble Constant Type 2 1532009197, // Ego Talon IV 1698434490, // Ego Talon Bond 1735538848, // Frumious Vest 1842727357, // Ego Talon IV 1895532772, // Ego Talon IV 1940451444, // Noble Constant Type 2 2416730691, // Ego Talon IV 2615512594, // Ego Talon IV 2682045448, // Noble Constant Type 2 2684281417, // Noble Constant Mark 2688111404, // Noble Constant Type 2 3081969019, // Ego Talon IV 3511221544, // Frumious Grips 3741528736, // Frumious Strides 3758301014, // Noble Constant Type 2 4081859017, // Noble Constant Type 2 4146629762, // Frumious Strides 4208352991, // Ego Talon IV 4224076198, // Frumious Grips 4225579453, // Noble Constant Type 2 4285708584, // Ego Talon Bond ], intothelight: [ 211732170, // Hammerhead 243425374, // Falling Guillotine 570866107, // Succession 2228325504, // Edge Transit 2499720827, // Midnight Coup 3757612024, // Luna's Howl 3851176026, // Elsie's Rifle ], io: [ 886128573, // Mindbreaker Boots 2317191363, // Mindbreaker Boots 2913284400, // Mindbreaker Boots ], ironbanner: [ 21320325, // Bond of Remembrance 63725907, // Iron Remembrance Plate 75550387, // Iron Truage Legs 92135663, // Iron Remembrance Vest 124696333, // Iron Truage Vestments 130221063, // Iron Truage Vestments 131359121, // Iron Fellowship Casque 142417051, // Iron Fellowship Casque 167461728, // Iron Remembrance Gloves 197164672, // Iron Truage Hood 198946996, // Iron Symmachy Helm 219816655, // Iron Fellowship Bond 228784708, // Iron Symmachy Robes 258029924, // Iron Fellowship Strides 279785447, // Iron Remembrance Vest 287471683, // Iron Truage Gloves 344804890, // Iron Fellowship Cloak 423204919, // Iron Truage Hood 425007249, // Iron Remembrance Plate 473526496, // Iron Fellowship Vest 479917491, // Mantle of Efrideet 481390023, // Iron Truage Casque 485774636, // Iron Remembrance Helm 487361141, // Gunnora's Axe 500363457, // Iron Symmachy Grips 510020159, // Iron Fellowship Strides 511170376, // Iron Truage Boots 540880995, // Dark Decider 559176540, // Iron Symmachy Gloves 561808153, // Mantle of Efrideet 691332172, // Iron Truage Gauntlets 706104224, // Iron Truage Gauntlets 713182381, // Iron Remembrance Gauntlets 738836759, // Iron Truage Vestments 738938985, // Radegast's Iron Sash 739655237, // Iron Truage Helm 741704251, // Iron Remembrance Plate 744156528, // Iron Symmachy Mask 770140877, // Iron Will Greaves 808693674, // Iron Symmachy Mark 829330711, // Peacebond 831464034, // Iron Truage Vest 863444264, // Iron Will Gloves 888872889, // Point of the Stag 892360677, // Iron Fellowship Helm 935677805, // Iron Truage Casque 957732971, // Iron Symmachy Grips 959040145, // Iron Symmachy Bond 995283190, // Cloak of Remembrance 1015625830, // Iron Truage Boots 1027482647, // Iron Fellowship Boots 1058936857, // Iron Will Vest 1062998051, // Iron Fellowship Vest 1084553865, // Iron Symmachy Greaves 1098138990, // Iron Will Mask 1105558158, // Iron Truage Helm 1127757814, // Iron Symmachy Helm 1161561386, // The Guiding Sight 1164755828, // Iron Fellowship Bond 1166260237, // Iron Truage Vestments 1173846338, // Iron Fellowship Bond 1181560527, // Iron Truage Vest 1233689371, // Iron Remembrance Hood 1234228360, // Iron Will Mark 1245456047, // Iron Fellowship Gauntlets 1279731468, // Iron Symmachy Mark 1311649814, // Timur's Iron Bond 1313089081, // Iron Truage Plate 1313767877, // Radegast's Iron Sash 1337167606, // Iron Truage Greaves 1339294334, // Cloak of Remembrance 1342036510, // Iron Truage Greaves 1349302244, // Iron Remembrance Legs 1395498705, // Iron Fellowship Greaves 1425558127, // Iron Remembrance Greaves 1438648985, // Iron Symmachy Bond 1452894389, // Mantle of Efrideet 1465485698, // Iron Fellowship Gloves 1469050017, // Iron Will Boots 1476572353, // Iron Truage Greaves 1478755348, // Iron Truage Gauntlets 1496224967, // Iron Truage Casque 1498852482, // Iron Will Steps 1526005320, // Iron Truage Boots 1532276803, // Allied Demand 1570751539, // Iron Symmachy Strides 1601698634, // Iron Fellowship Grips 1604601714, // Iron Truage Vestments 1618191618, // Iron Symmachy Mask 1631733639, // Bond of Remembrance 1631922345, // Iron Remembrance Greaves 1673037492, // Iron Fellowship Gauntlets 1675022998, // Iron Remembrance Helm 1717896437, // Iron Truage Legs 1764868900, // Riiswalker 1804445917, // Iron Truage Helm 1822989604, // Iron Symmachy Gloves 1854612346, // Iron Truage Hood 1876007169, // Iron Fellowship Mark 1882457108, // Iron Remembrance Helm 1889355043, // Iron Truage Legs 1891964978, // Iron Fellowship Greaves 1895324274, // Iron Will Helm 1944853984, // Iron Remembrance Casque 1960776126, // Iron Fellowship Greaves 1990315366, // Iron Symmachy Cloak 1999697514, // The Wizened Rebuke 2017059966, // Iron Fellowship Helm 2049490557, // Iron Symmachy Strides 2054377692, // Iron Truage Grips 2055774222, // Iron Fellowship Hood 2058205265, // Iron Truage Gloves 2083136519, // Iron Fellowship Cloak 2189073092, // Lethal Abundance 2205315921, // Iron Will Hood 2234855160, // Iron Symmachy Cloak 2241419267, // Timur's Iron Bond 2266122060, // Iron Truage Gauntlets 2274205961, // Iron Fellowship Plate 2302106622, // Iron Remembrance Vestments 2310625418, // Mark of Remembrance 2320100699, // Iron Will Gauntlets 2331748167, // Iron Symmachy Gauntlets 2340483067, // Iron Remembrance Hood 2391553724, // Iron Fellowship Hood 2414679508, // Iron Will Cloak 2426788417, // Iron Fellowship Boots 2455992644, // Iron Remembrance Legs 2488587246, // The Hero's Burden 2500327265, // Radegast's Iron Sash 2536633781, // Iron Will Plate 2547799775, // Iron Will Sleeves 2555322239, // Iron Truage Gauntlets 2589114445, // Iron Fellowship Mark 2614190248, // Iron Remembrance Vestments 2620437164, // Mark of Remembrance 2627255028, // Radegast's Iron Sash 2674485749, // Iron Truage Legs 2692970954, // Iron Remembrance Gloves 2723059534, // Iron Truage Grips 2753509502, // Iron Fellowship Vest 2758933481, // Iron Remembrance Hood 2811201658, // Iron Truage Hood 2817130155, // Iron Fellowship Robes 2845071512, // Iron Remembrance Casque 2850783764, // Iron Truage Plate 2853073502, // Mantle of Efrideet 2863819165, // Iron Fellowship Grips 2867156198, // Timur's Iron Bond 2879116647, // Iron Remembrance Gauntlets 2885394189, // Iron Remembrance Strides 2898234995, // Iron Symmachy Plate 2900181965, // Iron Symmachy Gauntlets 2911957494, // Iron Truage Greaves 2914695209, // Iron Truage Helm 2916624580, // Iron Fellowship Casque 2999505920, // Timur's Iron Bond 3018777825, // Iron Fellowship Helm 3042878056, // Iron Fellowship Grips 3055410141, // Iron Will Bond 3057399960, // Iron Truage Vest 3112906149, // Iron Symmachy Vest 3115791898, // Iron Remembrance Legs 3147146325, // Iron Symmachy Hood 3169616514, // Bite of the Fox 3292445816, // Iron Truage Casque 3300129601, // Iron Truage Gloves 3308875113, // Iron Remembrance Grips 3329206472, // Cloak of Remembrance 3345886183, // Bond of Remembrance 3369424240, // Iron Truage Grips 3379235805, // Iron Truage Helm 3420845681, // Iron Symmachy Plate 3472216012, // Iron Fellowship Plate 3505538303, // Iron Fellowship Gloves 3543613212, // Iron Symmachy Robes 3543922672, // Iron Truage Hood 3544440242, // Iron Remembrance Casque 3551208252, // Iron Fellowship Boots 3570981007, // Iron Symmachy Greaves 3600816955, // Iron Remembrance Strides 3625849667, // Iron Truage Gloves 3646911172, // Iron Truage Vest 3661959184, // Iron Fellowship Plate 3678620931, // Iron Remembrance Strides 3686482762, // Iron Truage Boots 3696011098, // Iron Truage Greaves 3735443949, // Iron Symmachy Hood 3737894478, // Iron Truage Grips 3746327861, // Iron Fellowship Gloves 3753635534, // Iron Symmachy Boots 3756249289, // Iron Truage Grips 3791686334, // Iron Truage Gloves 3799661482, // Iron Remembrance Gloves 3815391974, // Iron Symmachy Boots 3817948370, // Mark of Remembrance 3818295475, // Mantle of Efrideet 3847368113, // Iron Remembrance Grips 3856062457, // Iron Truage Casque 3856697336, // Iron Fellowship Gauntlets 3865618708, // Iron Truage Plate 3899385447, // Iron Remembrance Greaves 3906637800, // Iron Truage Plate 3972479219, // Iron Fellowship Hood 3974682334, // Iron Remembrance Vestments 3976616421, // Iron Remembrance Gauntlets 4009352833, // Roar of the Bear 4010793371, // Iron Remembrance Grips 4019071337, // Radegast's Iron Sash 4041069824, // Timur's Iron Bond 4048191131, // Iron Truage Boots 4054509252, // Iron Fellowship Mark 4078529821, // Iron Fellowship Cloak 4096639276, // Iron Truage Plate 4128151712, // Iron Will Vestments 4144217282, // Iron Fellowship Strides 4145557177, // Iron Fellowship Robes 4156963223, // Iron Symmachy Vest 4169842018, // Iron Truage Vest 4196689510, // Iron Fellowship Robes 4211068696, // Iron Truage Legs 4248834293, // Iron Remembrance Vest ], itl: [ 211732170, // Hammerhead 243425374, // Falling Guillotine 570866107, // Succession 2228325504, // Edge Transit 2499720827, // Midnight Coup 3757612024, // Luna's Howl 3851176026, // Elsie's Rifle ], lastwish: [ 4968701, // Greaves of the Great Hunt 16387641, // Mark of the Great Hunt 49280456, // Gloves of the Great Hunt 65929376, // Gauntlets of the Great Hunt 70083888, // Nation of Beasts 146275556, // Vest of the Great Hunt 196235132, // Grips of the Great Hunt 424291879, // Age-Old Bond 501329015, // Chattering Bone 576683388, // Gauntlets of the Great Hunt 726265506, // Boots of the Great Hunt 776723133, // Robes of the Great Hunt 778784376, // Mark of the Great Hunt 821841934, // Bond of the Great Hunt 972689703, // Vest of the Great Hunt 1021341893, // Mark of the Great Hunt 1127835600, // Grips of the Great Hunt 1190016345, // Mask of the Great Hunt 1195800715, // Boots of the Great Hunt 1258342944, // Mask of the Great Hunt 1314563129, // Cloak of the Great Hunt 1432728945, // Hood of the Great Hunt 1444894250, // Strides of the Great Hunt 1477271933, // Bond of the Great Hunt 1646520469, // Cloak of the Great Hunt 1656835365, // Plate of the Great Hunt 1851777734, // Apex Predator 2112541750, // Cloak of the Great Hunt 2274520361, // Helm of the Great Hunt 2280287728, // Bond of the Great Hunt 2550116544, // Robes of the Great Hunt 2598685593, // Gloves of the Great Hunt 2868042232, // Vest of the Great Hunt 2884596447, // The Supremacy 2950533187, // Strides of the Great Hunt 3055836250, // Greaves of the Great Hunt 3119383537, // Grips of the Great Hunt 3143067364, // Plate of the Great Hunt 3208178411, // Gauntlets of the Great Hunt 3227674085, // Boots of the Great Hunt 3251351304, // Hood of the Great Hunt 3445296383, // Robes of the Great Hunt 3445582154, // Hood of the Great Hunt 3492720019, // Gloves of the Great Hunt 3494130310, // Strides of the Great Hunt 3591141932, // Techeun Force 3614211816, // Plate of the Great Hunt 3838639757, // Mask of the Great Hunt 3868637058, // Helm of the Great Hunt 3874578566, // Greaves of the Great Hunt 3885259140, // Transfiguration 4219088013, // Helm of the Great Hunt ], legendaryengram: [ 24598504, // Red Moon Phantom Vest 25091086, // Tangled Web Cloak 32806262, // Cloak of Five Full Moons 42219189, // Tangled Web Gauntlets 73720713, // High-Minded Complex 107232578, // Tangled Web Gauntlets 107582877, // Kerak Type 2 130772858, // Tangled Web Vest 133227345, // Kerak Type 2 144651852, // Prodigal Mask 155832748, // Icarus Drifter Mask 160388292, // Kerak Type 2 265279665, // Clandestine Maneuvers 269552461, // Road Complex AA1 308026950, // Road Complex AA1 311394919, // Insight Unyielding Greaves 316000947, // Dead End Cure 2.1 339438127, // High-Minded Complex 362404956, // Terra Concord Plate 369384485, // Insight Rover Vest 373203219, // Philomath Bond 388625893, // Insight Unyielding Gauntlets 410671183, // High-Minded Complex 417345678, // Thorium Holt Gloves 432525353, // Red Moon Phantom Mask 433294875, // Devastation Complex 434243995, // Hodiocentrist Bond 474076509, // Errant Knight 1.0 489114030, // Philomath Gloves 489480785, // High-Minded Complex 489743173, // Insight Unyielding Gauntlets 493299171, // Errant Knight 1.0 494682309, // Massyrian's Draw 532728591, // Thorium Holt Gloves 537272242, // Tangled Web Boots 545134223, // Tangled Web Mark 548907748, // Devastation Complex 553373026, // Tangled Web Hood 554000115, // Thorium Holt Bond 597618504, // Insight Vikti Hood 629469344, // Heiro Camo 629482101, // Dead End Cure 2.1 633160551, // Insight Rover Vest 635809934, // Terra Concord Helm 639670612, // Mimetic Savior Plate 655964556, // Mimetic Savior Gauntlets 683173058, // Philomath Robes 690335398, // Terra Concord Helm 695071581, // Tesseract Trace IV 731888972, // Insight Vikti Robes 737010724, // Thorium Holt Bond 836969671, // Insight Unyielding Greaves 854373147, // Insight Unyielding Plate 875215126, // Prodigal Mark 880368054, // Tangled Web Grips 881579413, // Terra Concord Helm 919186882, // Tangled Web Mark 922218300, // Road Complex AA1 966777042, // Anti-Hero Victory 974507844, // Insight Rover Grips 983115833, // Terra Concord Plate 993844472, // High-Minded Complex 1006824129, // Terra Concord Greaves 1020198891, // Insight Rover Grips 1024867629, // Errant Knight 1.0 1028913028, // Tesseract Trace IV 1034149520, // Tangled Web Robes 1063507982, // Terra Concord Greaves 1088960547, // Prodigal Greaves 1111042046, // High-Minded Complex 1127029635, // Insight Rover Boots 1148805553, // Thorium Holt Boots 1153347999, // Icarus Drifter Cape 1192751404, // Insight Unyielding Helm 1195298951, // Be Thy Champion 1213841242, // Red Moon Phantom Steps 1257810769, // Prodigal Gauntlets 1260134370, // Devastation Complex 1266060945, // Prodigal Mark 1293868684, // Insight Unyielding Helm 1295776817, // Insight Rover Grips 1301696822, // Mimetic Savior Greaves 1330107298, // Thorium Holt Robes 1330542168, // Tangled Web Bond 1348658294, // Clandestine Maneuvers 1364856221, // Retro-Grade TG2 1367655773, // Tangled Web Boots 1399263478, // Icarus Drifter Vest 1415533220, // Road Complex AA1 1425077417, // Mimetic Savior Mark 1429424420, // Prodigal Gauntlets 1432831619, // Red Moon Phantom Steps 1432969759, // Mimetic Savior Greaves 1457647945, // High-Minded Complex 1512829977, // Terra Concord Greaves 1513486336, // Road Complex AA1 1548943654, // Tesseract Trace IV 1553407343, // Prodigal Robes 1598372079, // Retro-Grade TG2 1601578801, // Red Moon Phantom Grips 1618341271, // Tangled Web Greaves 1648238545, // Terra Concord Mark 1655109893, // Tesseract Trace IV 1664085089, // Tangled Web Hood 1664611474, // Heiro Camo 1680657538, // Insight Rover Mask 1693706589, // Prodigal Cloak 1726695877, // Cloak of Five Full Moons 1728789982, // Thorium Holt Hood 1740873035, // Icarus Drifter Grips 1742735530, // Road Complex AA1 1749589787, // High-Minded Complex 1761136389, // Errant Knight 1.0 1772639961, // Hodiocentrist Bond 1810399711, // Philomath Bond 1847870034, // Icarus Drifter Cape 1854024004, // Be Thy Cipher 1865671934, // Devastation Complex 1892576458, // Devastation Complex 1893349933, // Tesseract Trace IV 1904199788, // Mark of the Unassailable 1920259123, // Tesseract Trace IV 1954457094, // Road Complex AA1 1964977914, // Mimetic Savior Mark 1978110490, // Mark of the Unassailable 1998314509, // Dead End Cure 2.1 2012084760, // Prodigal Hood 2020589887, // Road Complex AA1 2026285619, // Errant Knight 1.0 2048751167, // Kerak Type 2 2082184158, // Be Thy Cipher 2085574015, // Terra Concord Fists 2092750352, // Tangled Web Strides 2111956477, // Insight Rover Boots 2112821379, // Insight Unyielding Helm 2148295091, // Tangled Web Helm 2151378428, // Tangled Web Greaves 2159363321, // Be Thy Guide 2173858802, // Prodigal Cloak 2185500219, // Insight Unyielding Plate 2193432605, // Mimetic Savior Helm 2205604183, // Dead End Cure 2.1 2206284939, // Tangled Web Strides 2265859909, // Retro-Grade TG2 2297281780, // Terra Concord Mark 2298664693, // Insight Rover Mask 2332398934, // Kerak Type 2 2339155434, // Tesseract Trace IV 2360521872, // A Cloak Called Home 2364041279, // Insight Vikti Robes 2379553211, // Be Thy Guide 2402435619, // Philomath Cover 2414278933, // Errant Knight 1.0 2439195958, // Philomath Robes 2442805346, // Icarus Drifter Mask 2445181930, // Errant Knight 1.0 2454861732, // Prodigal Robes 2470746631, // Thorium Holt Hood 2475888361, // Prodigal Gloves 2478301019, // Insight Vikti Hood 2502004600, // Tangled Web Gloves 2518901664, // Red Moon Phantom Grips 2521426922, // Far Gone Hood 2525344810, // Retro-Grade TG2 2530905971, // Retro-Grade TG2 2542514983, // Philomath Cover 2546015644, // Tesseract Trace IV 2550994842, // Errant Knight 1.0 2561056920, // Retro-Grade TG2 2562470699, // Tangled Web Plate 2562555736, // Icarus Drifter Cape 2567710435, // Icarus Drifter Mask 2581516944, // Hodiocentrist Bond 2629014079, // Anti-Hero Victory 2648545535, // Tangled Web Vest 2669113551, // Dead End Cure 2.1 2674524165, // Tangled Web Robes 2696303651, // Kerak Type 2 2713755753, // Kerak Type 2 2728535008, // Tesseract Trace IV 2734010957, // Prodigal Hood 2753581141, // Prodigal Helm 2762426792, // Prodigal Grasps 2766448160, // Prodigal Vest 2767830203, // Prodigal Steps 2770578349, // Massyrian's Draw 2772485446, // Prodigal Steps 2791527489, // Heiro Camo 2800566014, // Prodigal Bond 2808379196, // Insight Rover Vest 2811180959, // Tesseract Trace IV 2819613314, // Far Gone Hood 2826844112, // Retro-Grade Mark 2837138379, // Insight Vikti Boots 2838060329, // Heiro Camo 2845530750, // Retro-Grade Mark 2905153902, // Insight Rover Boots 2905154661, // Insight Vikti Hood 2924984456, // Thorium Holt Boots 2932121030, // Devastation Complex 2982412348, // Tangled Web Helm 2996649640, // Philomath Boots 3018268196, // Insight Vikti Boots 3024860521, // Retro-Grade TG2 3061780015, // Tangled Web Mask 3066154883, // Mimetic Savior Plate 3066593211, // Icarus Drifter Vest 3087552232, // Heiro Camo 3125909492, // Dead End Cure 2.1 3169402598, // Tesseract Trace IV 3198691833, // Prodigal Bond 3239215026, // Icarus Drifter Grips 3250112431, // Be Thy Champion 3250360146, // Insight Unyielding Gauntlets 3257088093, // Icarus Drifter Legs 3291075521, // Terra Concord Plate 3299386902, // Insight Unyielding Plate 3304280092, // Devastation Complex 3316802363, // Retro-Grade TG2 3360070350, // Prodigal Greaves 3386676796, // Prodigal Gloves 3397835010, // Prodigal Strides 3403784957, // Mimetic Savior Gauntlets 3430647425, // Synaptic Construct 3433746208, // A Cloak Called Home 3434158555, // Prodigal Vest 3498500850, // Philomath Gloves 3506159922, // Anti-Hero Victory 3516789127, // Prodigal Strides 3527995388, // Dead End Cure 2.1 3536492583, // Kerak Type 2 3569443559, // Icarus Drifter Legs 3593916933, // Prodigal Grasps 3609169817, // Tangled Web Grips 3611199822, // Synaptic Construct 3612275815, // Red Moon Phantom Vest 3619376218, // Heiro Camo 3629447000, // Heiro Camo 3646674533, // Icarus Drifter Grips 3651598572, // Insight Unyielding Greaves 3685831476, // Insight Vikti Gloves 3688229984, // Insight Rover Mask 3691737472, // Prodigal Helm 3717812073, // Thorium Holt Robes 3725654227, // Devastation Complex 3786300792, // Clandestine Maneuvers 3839471140, // Mimetic Savior Helm 3850634012, // Prodigal Cuirass 3852389988, // Terra Concord Fists 3884999792, // Heiro Camo 3899739148, // Philomath Boots 3906537733, // Icarus Drifter Vest 3920228039, // Synaptic Construct 3973570110, // Insight Vikti Boots 3979056138, // Insight Vikti Gloves 3988753671, // Prodigal Cuirass 3994031968, // Red Moon Phantom Mask 3999262583, // Terra Concord Fists 4064910796, // Icarus Drifter Legs 4073580572, // Terra Concord Mark 4074193483, // Tangled Web Cloak 4079913195, // Dead End Cure 2.1 4092393610, // Tesseract Trace IV 4097652774, // Tangled Web Plate 4104298449, // Prodigal Mask 4146408011, // Tangled Web Gloves 4166246718, // Insight Vikti Robes 4239920089, // Insight Vikti Gloves 4256272077, // Tangled Web Bond 4261835528, // Tangled Web Mask ], leviathan: [ 30962015, // Boots of the Ace-Defiant 64543268, // Boots of the Emperor's Minister 64543269, // Boots of the Fulminator 288406317, // Greaves of Rull 311429765, // Mark of the Emperor's Champion 325434398, // Vest of the Ace-Defiant 325434399, // Vest of the Emperor's Agent 336656483, // Boots of the Emperor's Minister 407863747, // Vest of the Ace-Defiant 455108040, // Helm of the Emperor's Champion 455108041, // Mask of Rull 574137192, // Shadow's Mark 581908796, // Bond of the Emperor's Minister 608074492, // Robes of the Emperor's Minister 608074493, // Robes of the Fulminator 618662448, // Headpiece of the Emperor's Minister 641933203, // Mask of the Emperor's Agent 748485514, // Mask of the Fulminator 748485515, // Headpiece of the Emperor's Minister 754149842, // Wraps of the Emperor's Minister 754149843, // Wraps of the Fulminator 853543290, // Greaves of Rull 853543291, // Greaves of the Emperor's Champion 917591018, // Grips of the Ace-Defiant 917591019, // Gloves of the Emperor's Agent 1108389626, // Gloves of the Emperor's Agent 1230192769, // Robes of the Emperor's Minister 1354679721, // Cloak of the Emperor's Agent 1390282760, // Chassis of Rull 1390282761, // Cuirass of the Emperor's Champion 1413589586, // Mask of Rull 1876645653, // Chassis of Rull 1879942843, // Gauntlets of Rull 1960303677, // Grips of the Ace-Defiant 2013109092, // Helm of the Ace-Defiant 2070062384, // Shadow's Bond 2070062385, // Bond of the Emperor's Minister 2158603584, // Gauntlets of Rull 2158603585, // Gauntlets of the Emperor's Champion 2183861870, // Gauntlets of the Emperor's Champion 2193494688, // Boots of the Fulminator 2232730708, // Vest of the Emperor's Agent 2676042150, // Wraps of the Fulminator 2700598111, // Mask of the Fulminator 2758465168, // Greaves of the Emperor's Champion 2913992255, // Helm of the Emperor's Champion 3092380260, // Mark of the Emperor's Champion 3092380261, // Shadow's Mark 3292127944, // Cuirass of the Emperor's Champion 3530284425, // Wraps of the Emperor's Minister 3592548938, // Robes of the Fulminator 3711700026, // Mask of the Emperor's Agent 3711700027, // Helm of the Ace-Defiant 3763332443, // Shadow's Bond 3853397100, // Boots of the Emperor's Agent 3950028838, // Cloak of the Emperor's Agent 3950028839, // Shadow's Cloak 3984534842, // Shadow's Cloak 4251770244, // Boots of the Ace-Defiant 4251770245, // Boots of the Emperor's Agent ], limited: [ 1952218242, // Sequence Flourish 2683682447, // Traitor's Fate ], lostsectors: [ 322173891, // Mask of Fealty 1188437342, // Blastwave Striders 4060793397, // Rime-coat Raiment ], lw: [ 4968701, // Greaves of the Great Hunt 16387641, // Mark of the Great Hunt 49280456, // Gloves of the Great Hunt 65929376, // Gauntlets of the Great Hunt 70083888, // Nation of Beasts 146275556, // Vest of the Great Hunt 196235132, // Grips of the Great Hunt 424291879, // Age-Old Bond 501329015, // Chattering Bone 576683388, // Gauntlets of the Great Hunt 726265506, // Boots of the Great Hunt 776723133, // Robes of the Great Hunt 778784376, // Mark of the Great Hunt 821841934, // Bond of the Great Hunt 972689703, // Vest of the Great Hunt 1021341893, // Mark of the Great Hunt 1127835600, // Grips of the Great Hunt 1190016345, // Mask of the Great Hunt 1195800715, // Boots of the Great Hunt 1258342944, // Mask of the Great Hunt 1314563129, // Cloak of the Great Hunt 1432728945, // Hood of the Great Hunt 1444894250, // Strides of the Great Hunt 1477271933, // Bond of the Great Hunt 1646520469, // Cloak of the Great Hunt 1656835365, // Plate of the Great Hunt 1851777734, // Apex Predator 2112541750, // Cloak of the Great Hunt 2274520361, // Helm of the Great Hunt 2280287728, // Bond of the Great Hunt 2550116544, // Robes of the Great Hunt 2598685593, // Gloves of the Great Hunt 2868042232, // Vest of the Great Hunt 2884596447, // The Supremacy 2950533187, // Strides of the Great Hunt 3055836250, // Greaves of the Great Hunt 3119383537, // Grips of the Great Hunt 3143067364, // Plate of the Great Hunt 3208178411, // Gauntlets of the Great Hunt 3227674085, // Boots of the Great Hunt 3251351304, // Hood of the Great Hunt 3445296383, // Robes of the Great Hunt 3445582154, // Hood of the Great Hunt 3492720019, // Gloves of the Great Hunt 3494130310, // Strides of the Great Hunt 3591141932, // Techeun Force 3614211816, // Plate of the Great Hunt 3838639757, // Mask of the Great Hunt 3868637058, // Helm of the Great Hunt 3874578566, // Greaves of the Great Hunt 3885259140, // Transfiguration 4219088013, // Helm of the Great Hunt ], moon: [ 193805725, // Dreambane Cloak 272413517, // Dreambane Helm 310888006, // Dreambane Greaves 377813570, // Dreambane Strides 659922705, // Dreambane Cowl 682780965, // Dreambane Gloves 883769696, // Dreambane Vest 925079356, // Dreambane Gauntlets 1030110631, // Dreambane Boots 1528483180, // Dreambane Hood 2048903186, // Dreambane Bond 2568538788, // Dreambane Plate 3312368889, // Dreambane Mark 3571441640, // Dreambane Grips 3692187003, // Dreambane Robes ], nessus: [ 11686457, // Unethical Experiments Cloak 56157064, // Exodus Down Gauntlets 126418248, // Exodus Down Vest 177493699, // Exodus Down Plate 192377242, // Exodus Down Strides 320310250, // Unethical Experiments Bond 472691604, // Exodus Down Vest 527652447, // Exodus Down Mark 569251271, // Exodus Down Gloves 569678873, // Exodus Down Mark 582151075, // Exodus Down Helm 667921213, // Exodus Down Mark 853736709, // Exodus Down Cloak 874856664, // Exodus Down Bond 957928253, // Exodus Down Gauntlets 1010733668, // Exodus Down Helm 1096417434, // Shieldbreaker Robes 1156448694, // Exodus Down Plate 1157496418, // Exodus Down Greaves 1286488743, // Shieldbreaker Plate 1316205184, // Exodus Down Plate 1355771621, // Shieldbreaker Vest 1427620200, // Exodus Down Gloves 1439502385, // Exodus Down Helm 1539014368, // Exodus Down Grips 1640979177, // Exodus Down Cloak 1669675549, // Exodus Down Bond 1678216306, // Exodus Down Gauntlets 1810569868, // Exodus Down Bond 2029766091, // Exodus Down Gloves 2032811197, // Exodus Down Robes 2079454604, // Exodus Down Greaves 2172333833, // Exodus Down Mask 2218838661, // Exodus Down Robes 2252973221, // Exodus Down Cloak 2359639520, // Exodus Down Robes 2423003287, // Exodus Down Grips 2426340791, // Unethical Experiments Mark 2462524641, // Exodus Down Vest 2528959426, // Exodus Down Boots 2731698402, // Exodus Down Hood 2736812653, // Exodus Down Helm 2811068561, // Exodus Down Hood 2816760678, // Exodus Down Greaves 2947629004, // Exodus Down Grips 2953649850, // Exodus Down Strides 3026265798, // Exodus Down Mask 3323553887, // Exodus Down Greaves 3446606632, // Exodus Down Vest 3536375792, // Exodus Down Bond 3545981149, // Exodus Down Boots 3593464438, // Exodus Down Strides 3617024265, // Exodus Down Boots 3654781892, // Exodus Down Plate 3660228214, // Exodus Down Hood 3669590332, // Exodus Down Cloak 3742350309, // Exodus Down Boots 3754164794, // Exodus Down Mark 3807183801, // Exodus Down Strides 3855512540, // Exodus Down Gauntlets 3875829376, // Exodus Down Grips 3951684081, // Exodus Down Robes 3960258378, // Exodus Down Hood 4007396243, // Exodus Down Gloves 4060742749, // Exodus Down Mask 4130486121, // Exodus Down Mask ], nightfall: [ 40394833, // The Militia's Birthright 47772649, // THE SWARM 89693562, // Duty Bound 192784503, // Pre Astyanax IV 205225492, // Hung Jury SR4 267089201, // Warden's Law (Adept) 496556698, // Pre Astyanax IV (Adept) 555148853, // Wendigo GL3 (Adept) 672957262, // Undercurrent (Adept) 681067419, // Hung Jury SR4 (Adept) 772231794, // Hung Jury SR4 852228780, // Uzume RR4 (Adept) 912150785, // Mindbender's Ambition (Adept) 1018012078, // Horror's Least 1094005544, // Mindbender's Ambition 1151688091, // Undercurrent 1821529912, // Warden's Law 1891996599, // Uzume RR4 (Adept) 2065081837, // Uzume RR4 2147010335, // Shadow Price (Adept) 2378101424, // The Militia's Birthright (Adept) 2450917538, // Uzume RR4 2633186522, // Shadow Price 3183283212, // Wendigo GL3 3836861464, // THE SWARM (Adept) 4074251943, // Hung Jury SR4 (Adept) 4281371574, // Hung Jury SR4 ], nm: [ 25798127, // Sovereign Grips 106359434, // Coronation Mark 147165546, // Sovereign Legs 316745113, // Sovereign Hood 342618372, // Coronation Cloak 600401425, // Sovereign Boots 755928510, // Sovereign Mask 831738837, // Coronation Bond 1890693805, // Sovereign Gauntlets 2154427219, // Sovereign Plate 2436244536, // Sovereign Robes 2603069551, // Sovereign Greaves 3059968532, // Sovereign Helm 3323316553, // Sovereign Vest 4083497488, // Sovereign Gloves ], plunder: [ 912150785, // Mindbender's Ambition (Adept) 2378101424, // The Militia's Birthright (Adept) 2871264750, // Skeleton Key ], prophecy: [ 1773934241, // Judgment 1904170910, // A Sudden Death 2129814338, // Prosecutor 4097972038, // A Sudden Death ], psiops: [ 3358687360, // Synaptic Spear ], rahool: [ 50291571, // Speaker's Sight 90009855, // Arbor Warden 192896783, // Cyrtarachne's Facade 300502917, // Nothing Manacles 461841403, // Gyrfalcon's Hauberk 511888814, // Secant Filaments 1001356380, // Star-Eater Scales 1322544481, // Hoarfrost-Z 1443166262, // Second Chance 1453120846, // The Path of Burning Steps 1467044898, // Icefall Mantle 1619425569, // Mask of Bakris 1624882687, // Rain of Fire 1627691271, // Gifted Conviction 1702288800, // Radiant Dance Machines 1703551922, // Blight Ranger 1703598057, // Point-Contact Cannon Brace 1849149215, // Fallen Sunstar 1909305643, // Hazardous Propulsion 1935198785, // Omnioculus 1955548646, // Mataiodoxía 2066430310, // Pyrogale Gauntlets 2169905051, // Renewal Grasps 2316914168, // Dawn Chorus 2321120637, // Cuirass of the Falling Star 2374129871, // Cenotaph Mask 2390471904, // Speedloader Slacks 2415768376, // Athrys's Embrace 2463947681, // Swarmers 2780717641, // Necrotic Grip 3001449507, // Balance of Power 3045642045, // Boots of the Assembler 3093309525, // Triton Vice 3234692237, // Briarbinds 3259193988, // Osmiomancy Gloves 3267996858, // No Backup Plans 3301944824, // Mantle of Battle Harmony 3316517958, // Loreley Splendor Helm 3453042252, // Caliban's Hand 3534173884, // Mothkeeper's Wraps 3574051505, // Cadmus Ridge Lancecap 3637722482, // Abeyant Leap 3717431477, // Wishful Ignorance 3831935023, // Ballidorse Wrathweavers 3974038291, // Precious Scars ], raid: [ 4968701, // Greaves of the Great Hunt 11974904, // Greaves of Ascendancy 16387641, // Mark of the Great Hunt 17280095, // Shadow's Strides 30962015, // Boots of the Ace-Defiant 49280456, // Gloves of the Great Hunt 64543268, // Boots of the Emperor's Minister 64543269, // Boots of the Fulminator 65929376, // Gauntlets of the Great Hunt 70083888, // Nation of Beasts 146275556, // Vest of the Great Hunt 196235132, // Grips of the Great Hunt 223783885, // Insigne Shade Bond 239489770, // Bond of Sekris 253344425, // Mask of Feltroc 256904954, // Shadow's Grips 281660259, // Temptation's Mark 288406317, // Greaves of Rull 309687341, // Shadow's Greaves 311429765, // Mark of the Emperor's Champion 325125949, // Shadow's Helm 325434398, // Vest of the Ace-Defiant 325434399, // Vest of the Emperor's Agent 336656483, // Boots of the Emperor's Minister 340118991, // Boots of Sekris 350056552, // Bladesmith's Memory Mask 383742277, // Cloak of Feltroc 388999052, // Bulletsmith's Ire Mark 407863747, // Vest of the Ace-Defiant 424291879, // Age-Old Bond 455108040, // Helm of the Emperor's Champion 455108041, // Mask of Rull 501329015, // Chattering Bone 503773817, // Insigne Shade Gloves 519078295, // Helm of Righteousness 548581042, // Insigne Shade Boots 557676195, // Cowl of Righteousness 560455272, // Penumbral Mark 574137192, // Shadow's Mark 576683388, // Gauntlets of the Great Hunt 581908796, // Bond of the Emperor's Minister 588627781, // Bond of Sekris 608074492, // Robes of the Emperor's Minister 608074493, // Robes of the Fulminator 612065993, // Penumbral Mark 618662448, // Headpiece of the Emperor's Minister 641933203, // Mask of the Emperor's Agent 666883012, // Gauntlets of Nohr 726265506, // Boots of the Great Hunt 748485514, // Mask of the Fulminator 748485515, // Headpiece of the Emperor's Minister 754149842, // Wraps of the Emperor's Minister 754149843, // Wraps of the Fulminator 776723133, // Robes of the Great Hunt 778784376, // Mark of the Great Hunt 796914932, // Mask of Sekris 802557885, // Turris Shade Gauntlets 821841934, // Bond of the Great Hunt 845536715, // Vest of Feltroc 853543290, // Greaves of Rull 853543291, // Greaves of the Emperor's Champion 855363300, // Turris Shade Helm 874272413, // Shadow's Robes 917591018, // Grips of the Ace-Defiant 917591019, // Gloves of the Emperor's Agent 972689703, // Vest of the Great Hunt 974648224, // Shadow's Boots 1021341893, // Mark of the Great Hunt 1034660314, // Boots of Feltroc 1108389626, // Gloves of the Emperor's Agent 1127835600, // Grips of the Great Hunt 1156439528, // Insigne Shade Cover 1190016345, // Mask of the Great Hunt 1195800715, // Boots of the Great Hunt 1230192769, // Robes of the Emperor's Minister 1242139836, // Plate of Nohr 1256688732, // Mask of Feltroc 1258342944, // Mask of the Great Hunt 1296628624, // Insigne Shade Robes 1314563129, // Cloak of the Great Hunt 1339632007, // Turris Shade Helm 1354679721, // Cloak of the Emperor's Agent 1390282760, // Chassis of Rull 1390282761, // Cuirass of the Emperor's Champion 1413589586, // Mask of Rull 1432728945, // Hood of the Great Hunt 1434870610, // Shadow's Helm 1444894250, // Strides of the Great Hunt 1457195686, // Shadow's Gloves 1477271933, // Bond of the Great Hunt 1481751647, // Shadow's Mind 1624906371, // Gunsmith's Devotion Crown 1646520469, // Cloak of the Great Hunt 1653741426, // Grips of Exaltation 1656835365, // Plate of the Great Hunt 1675393889, // Insigne Shade Cover 1756558505, // Mask of Sekris 1793869832, // Turris Shade Greaves 1851777734, // Apex Predator 1862963733, // Shadow's Plate 1876645653, // Chassis of Rull 1879942843, // Gauntlets of Rull 1901223867, // Shadow's Gauntlets 1917693279, // Bladesmith's Memory Vest 1934647691, // Shadow's Mask 1937834292, // Shadow's Strides 1946621757, // Shadow's Grips 1960303677, // Grips of the Ace-Defiant 1991039861, // Mask of Nohr 1999427172, // Shadow's Mask 2013109092, // Helm of the Ace-Defiant 2015894615, // Gloves of Exaltation 2023695690, // Shadow's Robes 2054979724, // Strides of Ascendancy 2070062384, // Shadow's Bond 2070062385, // Bond of the Emperor's Minister 2112541750, // Cloak of the Great Hunt 2128823667, // Turris Shade Mark 2153222031, // Shadow's Gloves 2158603584, // Gauntlets of Rull 2158603585, // Gauntlets of the Emperor's Champion 2183861870, // Gauntlets of the Emperor's Champion 2193494688, // Boots of the Fulminator 2194479195, // Penumbral Bond 2232730708, // Vest of the Emperor's Agent 2274520361, // Helm of the Great Hunt 2280287728, // Bond of the Great Hunt 2320830625, // Robes of Transcendence 2329031091, // Robes of Sekris 2339720736, // Grips of Feltroc 2369496221, // Plate of Nohr 2480074702, // Forbearance 2513313400, // Insigne Shade Gloves 2530113265, // Bulletsmith's Ire Plate 2537874394, // Boots of Sekris 2550116544, // Robes of the Great Hunt 2552158692, // Equitis Shade Rig 2589473259, // Bladesmith's Memory Strides 2597529070, // Greaves of Nohr 2598685593, // Gloves of the Great Hunt 2620001759, // Insigne Shade Robes 2653039573, // Grips of Feltroc 2676042150, // Wraps of the Fulminator 2700598111, // Mask of the Fulminator 2710517999, // Equitis Shade Grips 2722103686, // Equitis Shade Boots 2758465168, // Greaves of the Emperor's Champion 2762445138, // Gunsmith's Devotion Gloves 2765688378, // Penumbral Cloak 2769298993, // Shadow's Boots 2868042232, // Vest of the Great Hunt 2878130185, // Bulletsmith's Ire Greaves 2884596447, // The Supremacy 2904930850, // Turris Shade Plate 2913992255, // Helm of the Emperor's Champion 2921334134, // Bulletsmith's Ire Helm 2933666377, // Equitis Shade Rig 2950533187, // Strides of the Great Hunt 2976612200, // Vest of Feltroc 2994007601, // Mark of Nohr 3001934726, // Mask of Righteousness 3055836250, // Greaves of the Great Hunt 3066613133, // Equitis Shade Cowl 3082625196, // Shadow's Gauntlets 3092380260, // Mark of the Emperor's Champion 3092380261, // Shadow's Mark 3099636805, // Greaves of Nohr 3103335676, // Temptation's Bond 3108321700, // Penumbral Bond 3119383537, // Grips of the Great Hunt 3143067364, // Plate of the Great Hunt 3163683564, // Gunsmith's Devotion Boots 3164851950, // Bladesmith's Memory Cloak 3168183519, // Turris Shade Greaves 3181497704, // Robes of Sekris 3208178411, // Gauntlets of the Great Hunt 3227674085, // Boots of the Great Hunt 3251351304, // Hood of the Great Hunt 3285121297, // Equitis Shade Boots 3292127944, // Cuirass of the Emperor's Champion 3349283422, // Shadow's Mind 3359121706, // Mask of Nohr 3364682867, // Gauntlets of Nohr 3388655311, // Tyranny of Heaven 3395856235, // Insigne Shade Boots 3416932282, // Turris Shade Mark 3440648382, // Equitis Shade Cowl 3445296383, // Robes of the Great Hunt 3445582154, // Hood of the Great Hunt 3483984579, // Shadow's Vest 3492720019, // Gloves of the Great Hunt 3494130310, // Strides of the Great Hunt 3497220322, // Cloak of Feltroc 3517729518, // Shadow's Vest 3518193943, // Penumbral Cloak 3530284425, // Wraps of the Emperor's Minister 3549177695, // Cloak of Temptation 3567761471, // Gunsmith's Devotion Bond 3581198350, // Turris Shade Gauntlets 3591141932, // Techeun Force 3592548938, // Robes of the Fulminator 3614211816, // Plate of the Great Hunt 3711700026, // Mask of the Emperor's Agent 3711700027, // Helm of the Ace-Defiant 3719175804, // Equitis Shade Grips 3720446265, // Equitis Shade Cloak 3759659288, // Shadow's Plate 3763332443, // Shadow's Bond 3824429433, // Boots of Ascendancy 3831484112, // Mark of Nohr 3838639757, // Mask of the Great Hunt 3842934816, // Wraps of Sekris 3853397100, // Boots of the Emperor's Agent 3867160430, // Insigne Shade Bond 3868637058, // Helm of the Great Hunt 3874578566, // Greaves of the Great Hunt 3885259140, // Transfiguration 3887559710, // Gauntlets of Exaltation 3939809874, // Plate of Transcendence 3950028838, // Cloak of the Emperor's Agent 3950028839, // Shadow's Cloak 3964287245, // Wraps of Sekris 3984534842, // Shadow's Cloak 3992358137, // Bladesmith's Memory Grips 4125324487, // Bulletsmith's Ire Gauntlets 4135228483, // Turris Shade Plate 4152814806, // Shadow's Greaves 4177973942, // Vest of Transcendence 4219088013, // Helm of the Great Hunt 4229161783, // Boots of Feltroc 4238134294, // Gunsmith's Devotion Robes 4247935492, // Equitis Shade Cloak 4251770244, // Boots of the Ace-Defiant 4251770245, // Boots of the Emperor's Agent ], rasputin: [ 555148853, // Wendigo GL3 (Adept) 681067419, // Hung Jury SR4 (Adept) 1631448645, // Seraph Cipher 4074251943, // Hung Jury SR4 (Adept) ], reclaim: [ 2979965244, // Romantic Death 2979965245, // Romantic Death 2979965246, // Romantic Death 2979965247, // Romantic Death 3184457500, // Folded Root 3184457501, // Folded Root 3184457502, // Folded Root 3184457503, // Folded Root ], renegades: [ 1872906663, // Modified B-7 Pistol 2023002233, // All or Nothing 2462965802, // Uncivil Discourse 2659286158, // Compact Defender 2770035786, // M-17 "Fast Talker" ], revenant: [ 239405325, // Spacewalk Strides 480133716, // Spacewalk Robes 498496285, // Spacewalk Cover 898451378, // Spacewalk Cowl 1265540521, // Spacewalk Bond 1364804507, // Spacewalk Grasps 2147583688, // Spacewalk Cloak 2155757770, // Spacewalk Mark 2867324653, // Spacewalk Gauntlets 3067211509, // Spacewalk Vest 3113666223, // Spacewalk Greaves 3255995532, // Spacewalk Gloves 3820841619, // Spacewalk Plate 3901727798, // Spacewalk Boots 4036496212, // Spacewalk Helm ], riteofthenine: [ 14929251, // Long Arm 492673102, // New Pacific Epitaph 749483159, // Prosecutor (Adept) 1050582210, // Greasy Luck (Adept) 1066598837, // Relentless (Adept) 1157220231, // No Survivors (Adept) 1685406703, // Greasy Luck 2126543269, // Cold Comfort (Adept) 2477408004, // Wilderflight (Adept) 2730671571, // Terminus Horizon 2764074355, // A Sudden Death (Adept) 3185151619, // New Pacific Epitaph (Adept) 3421639790, // Liminal Vigil (Adept) 3598944128, // Foretold 3598944129, // Seer 3598944130, // Esper 3598944131, // Calibrated 3598944132, // Immortality 3681280908, // Relentless 3692140710, // Long Arm (Adept) 4193602194, // No Survivors 4267192886, // Terminus Horizon (Adept) ], rotn: [ 14929251, // Long Arm 492673102, // New Pacific Epitaph 749483159, // Prosecutor (Adept) 1050582210, // Greasy Luck (Adept) 1066598837, // Relentless (Adept) 1157220231, // No Survivors (Adept) 1685406703, // Greasy Luck 2126543269, // Cold Comfort (Adept) 2477408004, // Wilderflight (Adept) 2730671571, // Terminus Horizon 2764074355, // A Sudden Death (Adept) 3185151619, // New Pacific Epitaph (Adept) 3421639790, // Liminal Vigil (Adept) 3598944128, // Foretold 3598944129, // Seer 3598944130, // Esper 3598944131, // Calibrated 3598944132, // Immortality 3681280908, // Relentless 3692140710, // Long Arm (Adept) 4193602194, // No Survivors 4267192886, // Terminus Horizon (Adept) ], saint14: [ 3360014173, // The Lantern of Osiris ], scourge: [ 350056552, // Bladesmith's Memory Mask 388999052, // Bulletsmith's Ire Mark 1624906371, // Gunsmith's Devotion Crown 1917693279, // Bladesmith's Memory Vest 2530113265, // Bulletsmith's Ire Plate 2589473259, // Bladesmith's Memory Strides 2762445138, // Gunsmith's Devotion Gloves 2878130185, // Bulletsmith's Ire Greaves 2921334134, // Bulletsmith's Ire Helm 3163683564, // Gunsmith's Devotion Boots 3164851950, // Bladesmith's Memory Cloak 3567761471, // Gunsmith's Devotion Bond 3992358137, // Bladesmith's Memory Grips 4125324487, // Bulletsmith's Ire Gauntlets 4238134294, // Gunsmith's Devotion Robes ], scourgeofthepast: [ 350056552, // Bladesmith's Memory Mask 388999052, // Bulletsmith's Ire Mark 1624906371, // Gunsmith's Devotion Crown 1917693279, // Bladesmith's Memory Vest 2530113265, // Bulletsmith's Ire Plate 2589473259, // Bladesmith's Memory Strides 2762445138, // Gunsmith's Devotion Gloves 2878130185, // Bulletsmith's Ire Greaves 2921334134, // Bulletsmith's Ire Helm 3163683564, // Gunsmith's Devotion Boots 3164851950, // Bladesmith's Memory Cloak 3567761471, // Gunsmith's Devotion Bond 3992358137, // Bladesmith's Memory Grips 4125324487, // Bulletsmith's Ire Gauntlets 4238134294, // Gunsmith's Devotion Robes ], seasonpass: [ 1387688628, // The Gate Lord's Eye 1631448645, // Seraph Cipher 2785855278, // NPA Repulsion Regulator 2871264750, // Skeleton Key 3358687360, // Synaptic Spear 3644991365, // Ascendant Scepter 3864896927, // Nightmare Harvester 4012642691, // Riptide ], servitor: [ 3380377210, // Paradrome Cube ], shaxx: [ 85800627, // Ankaa Seeker IV 98331691, // Binary Phoenix Mark 120859138, // Phoenix Strife Type 0 185853176, // Wing Discipline 252414402, // Swordflight 4.1 283188616, // Wing Contender 290136582, // Wing Theorem 315615761, // Ankaa Seeker IV 327530279, // Wing Theorem 328902054, // Swordflight 4.1 356269375, // Wing Theorem 388771599, // Phoenix Strife Type 0 419812559, // Ankaa Seeker IV 438224459, // Wing Discipline 449878234, // Phoenix Strife Type 0 468899627, // Binary Phoenix Mark 494475253, // Ossuary Boots 530558102, // Phoenix Strife Type 0 628604416, // Ossuary Bond 631191162, // Ossuary Cover 636679949, // Ankaa Seeker IV 657400178, // Swordflight 4.1 670877864, // Binary Phoenix Mark 727838174, // Swordflight 4.1 744199039, // Wing Contender 761953100, // Ankaa Seeker IV 820446170, // Phoenix Strife Type 0 849529384, // Phoenix Strife Type 0 874101646, // Wing Theorem 876608500, // Ankaa Seeker IV 920187221, // Wing Discipline 929917162, // Wing Theorem 944242985, // Ankaa Seeker IV 979782821, // Hinterland Cloak 987343638, // Ankaa Seeker IV 997903134, // Wing Theorem 1036467370, // Wing Theorem 1062166003, // Wing Contender 1063904165, // Wing Discipline 1069887756, // Wing Contender 1071350799, // Binary Phoenix Cloak 1084033161, // Wing Contender 1127237110, // Wing Contender 1167444103, // Biosphere Explorer Mark 1245115841, // Wing Theorem 1294217731, // Binary Phoenix Cloak 1307478991, // Ankaa Seeker IV 1323862250, // Riptide 1330581478, // Phoenix Strife Type 0 1333087155, // Ankaa Seeker IV 1381742107, // Biosphere Explorer Helm 1464207979, // Wing Discipline 1467590642, // Binary Phoenix Bond 1484937602, // Phoenix Strife Type 0 1497354980, // Biosphere Explorer Greaves 1548928853, // Phoenix Strife Type 0 1571781304, // Swordflight 4.1 1648675919, // Binary Phoenix Mark 1654427223, // Swordflight 4.1 1658896287, // Binary Phoenix Cloak 1673285051, // Wing Theorem 1716643851, // Wing Contender 1722623780, // Wing Discipline 1742680797, // Binary Phoenix Mark 1742940528, // Phoenix Strife Type 0 1764274932, // Ankaa Seeker IV 1801625827, // Swordflight 4.1 1828358334, // Swordflight 4.1 1830829330, // Swordflight 4.1 1837817086, // Biosphere Explorer Plate 1838158578, // Binary Phoenix Bond 1838273186, // Wing Contender 1852468615, // Ankaa Seeker IV 1904811766, // Swordflight 4.1 1929596421, // Ankaa Seeker IV 2048762125, // Ossuary Robes 2070517134, // Wing Contender 2124666626, // Wing Discipline 2191401041, // Phoenix Strife Type 0 2191437287, // Ankaa Seeker IV 2206581692, // Phoenix Strife Type 0 2231762285, // Phoenix Strife Type 0 2247740696, // Swordflight 4.1 2291226602, // Binary Phoenix Bond 2293476915, // Swordflight 4.1 2296560252, // Swordflight 4.1 2296691422, // Swordflight 4.1 2323865727, // Wing Theorem 2331227463, // Wing Contender 2339694345, // Hinterland Cowl 2402428483, // Ossuary Gloves 2415711886, // Wing Contender 2426070307, // Binary Phoenix Cloak 2466453881, // Wing Discipline 2473130418, // Swordflight 4.1 2496309431, // Wing Discipline 2511045676, // Binary Phoenix Bond 2525395257, // Wing Theorem 2543903638, // Phoenix Strife Type 0 2555965565, // Wing Discipline 2627852659, // Phoenix Strife Type 0 2670393359, // Phoenix Strife Type 0 2718495762, // Swordflight 4.1 2727890395, // Ankaa Seeker IV 2754844215, // Swordflight 4.1 2775298636, // Ankaa Seeker IV 2815422368, // Phoenix Strife Type 0 2841023690, // Biosphere Explorer Gauntlets 3089908066, // Wing Discipline 3098328572, // The Recluse 3098458331, // Ankaa Seeker IV 3119528729, // Wing Contender 3121010362, // Hinterland Strides 3140634552, // Swordflight 4.1 3148195144, // Hinterland Vest 3211001969, // Wing Contender 3223280471, // Swordflight 4.1 3298826188, // Swordflight 4.1 3313736739, // Binary Phoenix Cloak 3315265682, // Phoenix Strife Type 0 3483546829, // Wing Discipline 3522021318, // Wing Discipline 3538513130, // Binary Phoenix Bond 3724026171, // Wing Theorem 3756286064, // Phoenix Strife Type 0 3772194440, // Wing Contender 3781722107, // Phoenix Strife Type 0 3818803676, // Wing Discipline 3839561204, // Wing Theorem 4043189888, // Hinterland Grips 4043921923, // The Mountaintop 4043980813, // Ankaa Seeker IV 4123918087, // Wing Contender 4134090375, // Ankaa Seeker IV 4136212668, // Wing Discipline 4144133120, // Wing Theorem 4211218181, // Ankaa Seeker IV 4264096388, // Wing Theorem ], sonar: [ 2785855278, // NPA Repulsion Regulator ], sos: [ 223783885, // Insigne Shade Bond 503773817, // Insigne Shade Gloves 548581042, // Insigne Shade Boots 802557885, // Turris Shade Gauntlets 855363300, // Turris Shade Helm 1156439528, // Insigne Shade Cover 1296628624, // Insigne Shade Robes 1339632007, // Turris Shade Helm 1675393889, // Insigne Shade Cover 1793869832, // Turris Shade Greaves 2128823667, // Turris Shade Mark 2513313400, // Insigne Shade Gloves 2552158692, // Equitis Shade Rig 2620001759, // Insigne Shade Robes 2710517999, // Equitis Shade Grips 2722103686, // Equitis Shade Boots 2904930850, // Turris Shade Plate 2933666377, // Equitis Shade Rig 3066613133, // Equitis Shade Cowl 3168183519, // Turris Shade Greaves 3285121297, // Equitis Shade Boots 3395856235, // Insigne Shade Boots 3416932282, // Turris Shade Mark 3440648382, // Equitis Shade Cowl 3581198350, // Turris Shade Gauntlets 3719175804, // Equitis Shade Grips 3720446265, // Equitis Shade Cloak 3867160430, // Insigne Shade Bond 4135228483, // Turris Shade Plate 4247935492, // Equitis Shade Cloak ], sotp: [ 350056552, // Bladesmith's Memory Mask 388999052, // Bulletsmith's Ire Mark 1624906371, // Gunsmith's Devotion Crown 1917693279, // Bladesmith's Memory Vest 2530113265, // Bulletsmith's Ire Plate 2589473259, // Bladesmith's Memory Strides 2762445138, // Gunsmith's Devotion Gloves 2878130185, // Bulletsmith's Ire Greaves 2921334134, // Bulletsmith's Ire Helm 3163683564, // Gunsmith's Devotion Boots 3164851950, // Bladesmith's Memory Cloak 3567761471, // Gunsmith's Devotion Bond 3992358137, // Bladesmith's Memory Grips 4125324487, // Bulletsmith's Ire Gauntlets 4238134294, // Gunsmith's Devotion Robes ], sotw: [ 436695703, // TM-Cogburn Custom Plate 498918879, // TM-Earp Custom Grips 708921139, // TM-Cogburn Custom Legguards 1349399252, // TM-Earp Custom Cloaked Stetson 1460079227, // Liminal Vigil 2341879253, // TM-Moss Custom Bond 2565015142, // TM-Cogburn Custom Mark 2982006965, // Wilderflight 3344225390, // TM-Earp Custom Hood 3511740432, // TM-Moss Custom Gloves 3715136417, // TM-Earp Custom Chaps 3870375786, // TM-Moss Custom Pants 3933500353, // TM-Cogburn Custom Gauntlets 3946384952, // TM-Moss Custom Duster 4039955353, // TM-Moss Custom Hat 4177293424, // TM-Cogburn Custom Cover 4288623897, // TM-Earp Custom Vest ], spireofstars: [ 223783885, // Insigne Shade Bond 503773817, // Insigne Shade Gloves 548581042, // Insigne Shade Boots 802557885, // Turris Shade Gauntlets 855363300, // Turris Shade Helm 1156439528, // Insigne Shade Cover 1296628624, // Insigne Shade Robes 1339632007, // Turris Shade Helm 1675393889, // Insigne Shade Cover 1793869832, // Turris Shade Greaves 2128823667, // Turris Shade Mark 2513313400, // Insigne Shade Gloves 2552158692, // Equitis Shade Rig 2620001759, // Insigne Shade Robes 2710517999, // Equitis Shade Grips 2722103686, // Equitis Shade Boots 2904930850, // Turris Shade Plate 2933666377, // Equitis Shade Rig 3066613133, // Equitis Shade Cowl 3168183519, // Turris Shade Greaves 3285121297, // Equitis Shade Boots 3395856235, // Insigne Shade Boots 3416932282, // Turris Shade Mark 3440648382, // Equitis Shade Cowl 3581198350, // Turris Shade Gauntlets 3719175804, // Equitis Shade Grips 3720446265, // Equitis Shade Cloak 3867160430, // Insigne Shade Bond 4135228483, // Turris Shade Plate 4247935492, // Equitis Shade Cloak ], spireofthewatcher: [ 436695703, // TM-Cogburn Custom Plate 498918879, // TM-Earp Custom Grips 708921139, // TM-Cogburn Custom Legguards 1349399252, // TM-Earp Custom Cloaked Stetson 1460079227, // Liminal Vigil 2341879253, // TM-Moss Custom Bond 2565015142, // TM-Cogburn Custom Mark 2982006965, // Wilderflight 3344225390, // TM-Earp Custom Hood 3511740432, // TM-Moss Custom Gloves 3715136417, // TM-Earp Custom Chaps 3870375786, // TM-Moss Custom Pants 3933500353, // TM-Cogburn Custom Gauntlets 3946384952, // TM-Moss Custom Duster 4039955353, // TM-Moss Custom Hat 4177293424, // TM-Cogburn Custom Cover 4288623897, // TM-Earp Custom Vest ], strikes: [ 24244626, // Mark of Shelter 34846448, // Xenos Vale IV 335317194, // Vigil of Heroes 358599471, // Vigil of Heroes 406401261, // The Took Offense 413460498, // Xenos Vale IV 417061387, // Xenos Vale IV 420247988, // Xenos Vale IV 432360904, // Vigil of Heroes 494475253, // Ossuary Boots 506100699, // Vigil of Heroes 508642129, // Vigil of Heroes 575676771, // Vigil of Heroes 628604416, // Ossuary Bond 631191162, // Ossuary Cover 758026143, // Vigil of Heroes 799187478, // Vigil of Heroes 979782821, // Hinterland Cloak 986111044, // Vigil of Heroes 1003941622, // Vigil of Heroes 1007759904, // Vigil of Heroes 1054960580, // Vigil of Heroes 1099472035, // The Took Offense 1130203390, // Vigil of Heroes 1167444103, // Biosphere Explorer Mark 1188816597, // The Took Offense 1247181362, // Vigil of Heroes 1320081419, // The Shelter in Place 1381742107, // Biosphere Explorer Helm 1405063395, // Vigil of Heroes 1490307366, // Vigil of Heroes 1497354980, // Biosphere Explorer Greaves 1514841742, // Mark of Shelter 1514863327, // Vigil of Heroes 1538362007, // Vigil of Heroes 1540376513, // Xenos Vale IV 1667528443, // The Shelter in Place 1699493316, // The Last Dance 1825880546, // The Took Offense 1837817086, // Biosphere Explorer Plate 2011569904, // Vigil of Heroes 2048762125, // Ossuary Robes 2060516289, // Vigil of Heroes 2072877132, // Vigil of Heroes 2076567986, // Vigil of Heroes 2304309360, // Vigil of Heroes 2337221567, // Vigil of Heroes 2339694345, // Hinterland Cowl 2378296024, // Xenos Vale IV 2402428483, // Ossuary Gloves 2422319309, // Vigil of Heroes 2442309039, // Vigil of Heroes 2460793798, // Vigil of Heroes 2592351697, // Vigil of Heroes 2671880779, // Vigil of Heroes 2722966297, // The Shelter in Place 2764938807, // The Took Offense 2841023690, // Biosphere Explorer Gauntlets 2902263756, // Vigil of Heroes 2939022735, // Vigil of Heroes 3027732901, // The Shelter in Place 3034285946, // Xenos Vale IV 3074985148, // Vigil of Heroes 3121010362, // Hinterland Strides 3130904371, // Vigil of Heroes 3148195144, // Hinterland Vest 3198744410, // The Took Offense 3213912958, // Vigil of Heroes 3215392301, // Xenos Vale Bond 3221304270, // Xenos Vale IV 3281314016, // The Took Offense 3375062567, // The Shelter in Place 3375632008, // The Shelter in Place 3469164235, // The Took Offense 3486485973, // The Took Offense 3499839403, // Vigil of Heroes 3500775049, // Vigil of Heroes 3544662820, // Vigil of Heroes 3569624585, // Vigil of Heroes 3584380110, // Vigil of Heroes 3666681446, // Vigil of Heroes 3670149407, // Vigil of Heroes 3851385946, // Vigil of Heroes 3873435116, // The Shelter in Place 3916064886, // Vigil of Heroes 3963753111, // Xenos Vale Bond 4024037919, // Origin Story 4043189888, // Hinterland Grips 4074662489, // Vigil of Heroes 4087433052, // The Took Offense 4138296191, // The Shelter in Place 4288492921, // Vigil of Heroes ], sundered: [ 267671509, // Skull of the Flain 571745874, // Hooks of the Flain 608948636, // Carapace of the Flain 874160718, // Claws of the Flain 1274101249, // Mask of the Flain 1930656621, // Husk's Cloak 2112020760, // Grasps of the Flain 2299285295, // Adornment of the Flain 2319342865, // Attendant's Mark 2501618648, // Visage of the Flain 2578940720, // Scales of the Flain 2791329915, // Talons of the Flain 2965319081, // Reach of the Flain 3238482084, // Grips of the Flain 4012478142, // Weaver's Bond ], sundereddoctrine: [ 267671509, // Skull of the Flain 571745874, // Hooks of the Flain 608948636, // Carapace of the Flain 874160718, // Claws of the Flain 1274101249, // Mask of the Flain 1930656621, // Husk's Cloak 2112020760, // Grasps of the Flain 2299285295, // Adornment of the Flain 2319342865, // Attendant's Mark 2501618648, // Visage of the Flain 2578940720, // Scales of the Flain 2791329915, // Talons of the Flain 2965319081, // Reach of the Flain 3238482084, // Grips of the Flain 4012478142, // Weaver's Bond ], tangled: [ 177829853, // Scatterhorn Bond 218523139, // Scatterhorn Grasps 307138509, // Scatterhorn Vest 411850804, // Scatterhorn Wraps 694120634, // Scatterhorn Mark 699589438, // Scatterhorn Boots 902989307, // Scorned Baron Vest 1069453608, // Scatterhorn Wraps 1094005544, // Mindbender's Ambition 1250571424, // Scatterhorn Robe 1347463276, // Scatterhorn Mark 1349281425, // Scorned Baron Plate 1407026808, // Torobatl Celebration Mask 1412416835, // Scatterhorn Plate 1467355683, // Scatterhorn Strides 1566911695, // Scorned Baron Plate 1636205905, // Scatterhorn Grasps 1704861826, // Scatterhorn Boots 1862088022, // Scatterhorn Helm 1863170823, // Scatterhorn Vest 1928007477, // Scorned Baron Vest 1989103583, // Scatterhorn Greaves 2007698582, // Torobatl Celebration Mask 2243444841, // Scatterhorn Greaves 2276115770, // Scatterhorn Mask 2411325265, // Scatterhorn Hood 2563857333, // Scatterhorn Strides 2571396481, // Scatterhorn Bond 2757593792, // Scatterhorn Cloak 2932919026, // Nea-Thonis Breather 2944336620, // Nea-Thonis Breather 3044599574, // Scatterhorn Cloak 3066181671, // Scatterhorn Gauntlets 3183089352, // Scorned Baron Robes 3523809305, // Eimin-Tin Ritual Mask 3858472841, // Eimin-Tin Ritual Mask 3871458129, // Scatterhorn Plate 3918445245, // Scatterhorn Gauntlets 3926141285, // Scatterhorn Hood 3971250660, // Scatterhorn Helm 4070132608, // Scatterhorn Mask 4167605324, // Scatterhorn Robe 4245441464, // Scorned Baron Robes ], titan: [ 89693562, // Duty Bound 1701005142, // Songbreaker Gloves 2486041713, // Songbreaker Gauntlets 3706457515, // Songbreaker Grips ], trials: [ 2307365, // The Inquisitor (Adept) 72827962, // Focusing Robes 142864314, // Bond of the Exile 150551028, // Boots of the Exile 155955678, // Mark Relentless 272735286, // Greaves of the Exile 421771594, // Cloak Relentless 442736573, // Gloves of the Exile 495541988, // Hood of the Exile 532746994, // Astral Horizon (Adept) 571925067, // Cover of the Exile 686607149, // Focusing Cowl 711889599, // Whistler's Whim (Adept) 773318267, // Floating Vest 784751927, // Annihilating Plate 825554997, // The Inquisitor (Adept) 861160515, // Robe of the Exile 875395086, // Vest of the Exile 945907383, // Floating Grips 1164471069, // Helm of the Exile 1193489623, // Cloak of the Exile 1401300690, // Eye of Sol 1526650446, // Trials Engram 1574601402, // Whistler's Whim 1697682876, // Astral Horizon 1929400866, // Annihilating Helm 2059255495, // Eye of Sol (Adept) 2158289681, // Floating Boots 2185327324, // The Inquisitor 2421180981, // Incisor (Adept) 2579999316, // Plate of the Exile 2653171212, // The Inquisitor 2759251821, // Unwavering Duty (Adept) 2764588986, // Grips of the Exile 2808362207, // Legs of the Exile 3025466099, // Annihilating Guard 3102421004, // Exalted Truth 3127319342, // Floating Cowl 3149072083, // Bond Relentless 3365406121, // Mark of the Exile 3426704397, // Annihilating Greaves 3682803680, // Shayura's Wrath 3920882229, // Exalted Truth (Adept) 3921970316, // Gauntlets of the Exile 4023807721, // Shayura's Wrath (Adept) 4100217958, // Focusing Boots 4177448932, // Focusing Wraps 4248997900, // Incisor ], vesper: [ 239405325, // Spacewalk Strides 480133716, // Spacewalk Robes 498496285, // Spacewalk Cover 898451378, // Spacewalk Cowl 1265540521, // Spacewalk Bond 1364804507, // Spacewalk Grasps 2147583688, // Spacewalk Cloak 2155757770, // Spacewalk Mark 2867324653, // Spacewalk Gauntlets 3067211509, // Spacewalk Vest 3113666223, // Spacewalk Greaves 3255995532, // Spacewalk Gloves 3820841619, // Spacewalk Plate 3901727798, // Spacewalk Boots 4036496212, // Spacewalk Helm ], vespershost: [ 239405325, // Spacewalk Strides 480133716, // Spacewalk Robes 498496285, // Spacewalk Cover 898451378, // Spacewalk Cowl 1265540521, // Spacewalk Bond 1364804507, // Spacewalk Grasps 2147583688, // Spacewalk Cloak 2155757770, // Spacewalk Mark 2867324653, // Spacewalk Gauntlets 3067211509, // Spacewalk Vest 3113666223, // Spacewalk Greaves 3255995532, // Spacewalk Gloves 3820841619, // Spacewalk Plate 3901727798, // Spacewalk Boots 4036496212, // Spacewalk Helm ], votd: [ 2480074702, // Forbearance ], vow: [ 2480074702, // Forbearance ], vowofthedisciple: [ 2480074702, // Forbearance ], warlordsruin: [ 557092665, // Dark Age Cloak 632989816, // Dark Age Gauntlets 806004493, // Dark Age Gloves 851401651, // Dark Age Overcoat 1476803535, // Dark Age Legbraces 1933599476, // Dark Age Visor 2426502022, // Dark Age Strides 2662590925, // Dark Age Mark 2771011469, // Dark Age Mask 2963224754, // Dark Age Sabatons 3056827626, // Dark Age Bond 3423574140, // Dark Age Grips 3683772388, // Dark Age Harness 3735435664, // Dark Age Chestrig 4090037601, // Dark Age Helm ], watcher: [ 436695703, // TM-Cogburn Custom Plate 498918879, // TM-Earp Custom Grips 708921139, // TM-Cogburn Custom Legguards 1349399252, // TM-Earp Custom Cloaked Stetson 1460079227, // Liminal Vigil 2341879253, // TM-Moss Custom Bond 2565015142, // TM-Cogburn Custom Mark 2982006965, // Wilderflight 3344225390, // TM-Earp Custom Hood 3511740432, // TM-Moss Custom Gloves 3715136417, // TM-Earp Custom Chaps 3870375786, // TM-Moss Custom Pants 3933500353, // TM-Cogburn Custom Gauntlets 3946384952, // TM-Moss Custom Duster 4039955353, // TM-Moss Custom Hat 4177293424, // TM-Cogburn Custom Cover 4288623897, // TM-Earp Custom Vest ], zavala: [ 24244626, // Mark of Shelter 34846448, // Xenos Vale IV 335317194, // Vigil of Heroes 358599471, // Vigil of Heroes 406401261, // The Took Offense 413460498, // Xenos Vale IV 417061387, // Xenos Vale IV 420247988, // Xenos Vale IV 432360904, // Vigil of Heroes 494475253, // Ossuary Boots 506100699, // Vigil of Heroes 508642129, // Vigil of Heroes 575676771, // Vigil of Heroes 628604416, // Ossuary Bond 631191162, // Ossuary Cover 758026143, // Vigil of Heroes 799187478, // Vigil of Heroes 979782821, // Hinterland Cloak 986111044, // Vigil of Heroes 1003941622, // Vigil of Heroes 1007759904, // Vigil of Heroes 1054960580, // Vigil of Heroes 1099472035, // The Took Offense 1130203390, // Vigil of Heroes 1167444103, // Biosphere Explorer Mark 1188816597, // The Took Offense 1247181362, // Vigil of Heroes 1320081419, // The Shelter in Place 1381742107, // Biosphere Explorer Helm 1405063395, // Vigil of Heroes 1490307366, // Vigil of Heroes 1497354980, // Biosphere Explorer Greaves 1514841742, // Mark of Shelter 1514863327, // Vigil of Heroes 1538362007, // Vigil of Heroes 1540376513, // Xenos Vale IV 1667528443, // The Shelter in Place 1699493316, // The Last Dance 1825880546, // The Took Offense 1837817086, // Biosphere Explorer Plate 2011569904, // Vigil of Heroes 2048762125, // Ossuary Robes 2060516289, // Vigil of Heroes 2072877132, // Vigil of Heroes 2076567986, // Vigil of Heroes 2304309360, // Vigil of Heroes 2337221567, // Vigil of Heroes 2339694345, // Hinterland Cowl 2378296024, // Xenos Vale IV 2402428483, // Ossuary Gloves 2422319309, // Vigil of Heroes 2442309039, // Vigil of Heroes 2460793798, // Vigil of Heroes 2592351697, // Vigil of Heroes 2671880779, // Vigil of Heroes 2722966297, // The Shelter in Place 2764938807, // The Took Offense 2841023690, // Biosphere Explorer Gauntlets 2902263756, // Vigil of Heroes 2939022735, // Vigil of Heroes 3027732901, // The Shelter in Place 3034285946, // Xenos Vale IV 3074985148, // Vigil of Heroes 3121010362, // Hinterland Strides 3130904371, // Vigil of Heroes 3148195144, // Hinterland Vest 3198744410, // The Took Offense 3213912958, // Vigil of Heroes 3215392301, // Xenos Vale Bond 3221304270, // Xenos Vale IV 3281314016, // The Took Offense 3375062567, // The Shelter in Place 3375632008, // The Shelter in Place 3469164235, // The Took Offense 3486485973, // The Took Offense 3499839403, // Vigil of Heroes 3500775049, // Vigil of Heroes 3544662820, // Vigil of Heroes 3569624585, // Vigil of Heroes 3584380110, // Vigil of Heroes 3666681446, // Vigil of Heroes 3670149407, // Vigil of Heroes 3851385946, // Vigil of Heroes 3873435116, // The Shelter in Place 3916064886, // Vigil of Heroes 3963753111, // Xenos Vale Bond 4024037919, // Origin Story 4043189888, // Hinterland Grips 4074662489, // Vigil of Heroes 4087433052, // The Took Offense 4138296191, // The Shelter in Place 4288492921, // Vigil of Heroes ], }; export default missingSources; ================================================ FILE: src/data/d2/mods-with-bad-descriptions.json ================================================ { "Harmonic": [ 293178904, 877723168, 1103878128, 1293710444, 1301391064, 1305536863, 1677180919, 1702273159, 1891463783, 1971149752, 2479297167, 2657604783, 3094620656, 3832366019, 3969361392 ] } ================================================ FILE: src/data/d2/mutually-exclusive-mods.json ================================================ { "84503918": "finisher", "1125986156": "finisher", "1170405455": "finisher", "1208761894": "finisher", "2649291407": "fastball", "3064687909": "finisher", "4004774872": "finisher", "4004774873": "finisher", "4004774874": "finisher", "4004774875": "finisher", "4004774876": "finisher", "4004774877": "finisher" } ================================================ FILE: src/data/d2/objective-richTexts.ts ================================================ const richTextManifestSourceData = { '[###DestinyNamedSubstitutions.ui_player_action_jump_button###]': ['SandboxPerk', 738782202], '[Aim Down Sights]': ['SandboxPerk', 104297121], '[Air Dodge]': ['SandboxPerk', 1000136554], '[Air Move]': ['SandboxPerk', 3605775441], '[Alternate Weapon Action]': ['SandboxPerk', 156121479], '[Arc]': ['Objective', 85535852], '[Auto Rifle]': ['Objective', 49530695], '[Block]': ['SandboxPerk', 2236497009], '[Boost]': ['SandboxPerk', 3104860955], '[Bow]': ['Objective', 1368601876], '[Brake]': ['SandboxPerk', 4189070124], '[Disruption]': ['SandboxPerk', 136649446], '[Fusion Rifle]': ['Objective', 215999859], '[Glaive]': ['Objective', 1351954994], '[Grenade Launcher]': ['Objective', 43313268], '[Grenade]': ['Objective', 45245118], '[Hand Cannon]': ['Objective', 563593850], '[Headshot]': ['Objective', 30510483], '[Heavy Attack]': ['SandboxPerk', 390604159], '[Insert Medal Here]': ['Objective', 2254471463], '[Light Attack]': ['SandboxPerk', 355406666], '[Linear Fusion Rifle]': ['Objective', 1476676901], '[Machine Gun]': ['Objective', 172143731], '[Melee]': ['Objective', 429378555], '[Pulse Rifle]': ['Objective', 189060104], '[Quest]': ['Objective', 119206183], '[Reload]': ['SandboxPerk', 2835443516], '[Rocket Launcher]': ['Objective', 13215836], '[SMG]': ['Objective', 102976778], '[Scout Rifle]': ['Objective', 75057024], '[Shield-Piercing]': ['SandboxPerk', 200616812], '[Shoot]': ['SandboxPerk', 909538239], '[Shotgun]': ['Objective', 212380697], '[Sidearm]': ['Objective', 141911950], '[Sniper Rifle]': ['Objective', 273389628], '[Solar]': ['Objective', 85535853], '[Special Grenade Launcher]': ['Objective', 1217177904], '[Sprint]': ['SandboxPerk', 1330059513], '[Stagger]': ['SandboxPerk', 72139184], '[Stasis: Glyph 0]': ['Objective', 1397002441], '[Stasis: Glyph 1 Locked]': ['Objective', 1851419582], '[Stasis: Glyph 2 Locked]': ['Objective', 1857129539], '[Stasis: Glyph 3 Locked]': ['Objective', 1481711502], '[Stasis]': ['Objective', 1097409723], '[Strand]': ['Objective', 289019406], '[Super]': ['SandboxPerk', 608022904], '[Sword]': ['Objective', 1260068656], '[Trace Rifle]': ['Objective', 554293431], '[Void]': ['Objective', 33657378], '[afflicted|burdened|cursed]': ['SandboxPerk', 2208422697], '': ['Objective', 1080160166], } as const; export type StringsNeedingReplacement = keyof typeof richTextManifestSourceData; const richTextManifestExamples: Record< StringsNeedingReplacement, readonly [tableName: 'Objective' | 'SandboxPerk', hash: number] > = richTextManifestSourceData; export default richTextManifestExamples; ================================================ FILE: src/data/d2/objective-triumph.json ================================================ {} ================================================ FILE: src/data/d2/powerful-rewards.json ================================================ [ 66825403, 226330008, 506041342, 646467119, 782362349, 914894006, 993006552, 1204101093, 2223145359, 2484791497, 2646629159, 3308857600, 3529617614, 3879752304 ] ================================================ FILE: src/data/d2/pursuits.json ================================================ { "8928990": { "Destination": [697502628] }, "16009420": { "Destination": [2481646875] }, "27825000": { "ActivityMode": [608898761] }, "35303604": { "Destination": [1729879943] }, "35303605": { "Destination": [1729879943] }, "42252934": { "ActivityMode": [1673724806] }, "45262603": { "ActivityMode": [1673724806] }, "58287472": { "ActivityMode": [1673724806], "KillType": [4] }, "74310575": { "ActivityMode": [1164760504] }, "82312451": { "KillType": [1, 5] }, "88202002": { "Destination": [677774031] }, "90741727": { "Destination": [677774031] }, "99218696": { "Destination": [677774031] }, "109741767": { "Destination": [697502628] }, "113061891": { "Destination": [1729879943] }, "116822325": { "ItemCategory": [9, 1504945536] }, "128980839": { "Destination": [1416096592] }, "169598011": { "ActivityMode": [1164760504] }, "183125260": { "ActivityMode": [1848252830, 1164760504], "ItemCategory": [11], "KillType": [4] }, "186303403": { "Destination": [1729879943] }, "191616380": { "ActivityMode": [3789021730] }, "191616381": { "ActivityMode": [3789021730] }, "191616382": { "ActivityMode": [3789021730] }, "191616383": { "ActivityMode": [3789021730] }, "204755896": { "ActivityMode": [332181804], "KillType": [4] }, "204755897": { "ActivityMode": [332181804], "ItemCategory": [9] }, "204755898": { "ActivityMode": [332181804], "ItemCategory": [5] }, "204755899": { "ActivityMode": [332181804], "ItemCategory": [3954685534] }, "204755900": { "ActivityMode": [332181804], "KillType": [4] }, "204755901": { "ActivityMode": [332181804], "ItemCategory": [6] }, "204755902": { "ActivityMode": [332181804], "ItemCategory": [3317538576] }, "209894088": { "ActivityMode": [3789021730] }, "209894091": { "ActivityMode": [608898761] }, "211068288": { "ActivityMode": [1673724806], "DamageType": [1847026933] }, "217349965": { "ItemCategory": [3317538576] }, "221921328": { "Destination": [1729879943], "KillType": [4] }, "221921330": { "Destination": [1729879943] }, "221921331": { "Destination": [1729879943] }, "225464052": { "ActivityMode": [1673724806], "DamageType": [2303181850] }, "227508940": { "Destination": [1729879943] }, "227508941": { "Destination": [1729879943] }, "227508942": { "Destination": [1729879943] }, "227508943": { "Destination": [1729879943] }, "232000212": { "DamageType": [2303181850] }, "235749523": { "Destination": [2481646875] }, "242688673": { "ActivityMode": [2294590554], "ItemCategory": [9, 3954685534, 11, 14] }, "242688675": { "ActivityMode": [2294590554], "ItemCategory": [153950757, -153950757, 1504945536, 13, 10] }, "242688677": { "ActivityMode": [2294590554], "DamageType": [2303181850, 1847026933, 3454344768] }, "242688678": { "ActivityMode": [2294590554] }, "242688679": { "ActivityMode": [2294590554], "DamageType": [1847026933, 2303181850, 3454344768] }, "242691748": { "Destination": [677774031] }, "261410846": { "ActivityMode": [608898761] }, "262952568": { "Destination": [697502628] }, "267578800": { "Destination": [1729879943] }, "267578801": { "Destination": [1729879943] }, "282753691": { "Destination": [3990611421] }, "283507733": { "ActivityMode": [1164760504] }, "283938165": { "Destination": [1729879943] }, "285715020": { "Destination": [1729879943] }, "285715021": { "Destination": [1729879943] }, "285715022": { "DamageType": [151347233] }, "285715023": { "Destination": [1729879943], "DamageType": [151347233] }, "286192928": { "ActivityMode": [2394616003] }, "286561523": { "Destination": [1729879943], "ItemCategory": [2489664120] }, "299576120": { "ActivityMode": [2294590554], "ItemCategory": [153950757, -153950757], "KillType": [2] }, "299576121": { "ActivityMode": [2294590554], "KillType": [1] }, "299576122": { "ActivityMode": [2294590554], "KillType": [2] }, "299576123": { "ActivityMode": [2294590554], "DamageType": [3373582085], "KillType": [0] }, "299576126": { "ActivityMode": [2294590554], "ItemCategory": [6], "KillType": [4] }, "299576127": { "ActivityMode": [2294590554], "DamageType": [2303181850], "ItemCategory": [54] }, "303384168": { "ActivityMode": [1673724806], "ItemCategory": [11] }, "316370328": { "DamageType": [1847026933] }, "316370331": { "KillType": [0] }, "316370334": { "ItemCategory": [3317538576] }, "327090183": { "Destination": [677774031], "ItemCategory": [3954685534] }, "328725000": { "Destination": [1416096592] }, "339159260": { "ActivityMode": [2394616003] }, "346263927": { "ActivityMode": [608898761] }, "352390187": { "ItemCategory": [3317538576] }, "364003008": { "DamageType": [2303181850], "KillType": [4] }, "379746735": { "Destination": [1416096592] }, "400298576": { "ActivityMode": [332181804], "KillType": [3] }, "400298584": { "ActivityMode": [332181804], "DamageType": [1847026933] }, "400298585": { "ActivityMode": [332181804], "DamageType": [2303181850] }, "400298586": { "ActivityMode": [332181804], "DamageType": [3454344768] }, "400298587": { "ActivityMode": [332181804], "KillType": [2] }, "400298588": { "ActivityMode": [332181804], "KillType": [0] }, "400298589": { "ActivityMode": [332181804] }, "400298591": { "DamageType": [2303181850] }, "420228109": { "ActivityMode": [1164760504] }, "422583190": { "Destination": [1729879943] }, "425910192": { "ActivityMode": [1164760504] }, "431984069": { "DamageType": [151347233] }, "431984070": { "DamageType": [151347233] }, "435145886": { "ActivityMode": [1848252830], "DamageType": [151347233], "KillType": [0] }, "455233900": { "ActivityMode": [2394616003] }, "463685197": { "Destination": [1729879943], "ItemCategory": [11] }, "484414765": { "Destination": [2481646875] }, "484780086": { "ActivityMode": [1673724806] }, "523748088": { "Destination": [1729879943] }, "541603424": { "Destination": [1729879943] }, "542328999": { "Destination": [1416096592] }, "550008052": { "ActivityMode": [1673724806], "ItemCategory": [10] }, "578566109": { "ActivityMode": [332181804], "Destination": [677774031] }, "581594991": { "ActivityMode": [1826469369] }, "582968308": { "ItemCategory": [54] }, "582968309": { "ItemCategory": [54, 3871742104], "KillType": [0] }, "582968310": { "ActivityMode": [1164760504] }, "582968311": { "ActivityMode": [1164760504] }, "582968312": { "DamageType": [3373582085, 2303181850, 1847026933, 3454344768] }, "582968313": { "ActivityMode": [1164760504] }, "595255798": { "Destination": [1729879943] }, "595255799": { "Destination": [1729879943] }, "598616135": { "Destination": [677774031] }, "607910367": { "ItemCategory": [9, 6, 54] }, "609421552": { "Destination": [2244580325] }, "609421553": { "Destination": [2244580325] }, "609421554": { "Destination": [2244580325] }, "609421555": { "Destination": [2244580325] }, "609421556": { "Destination": [2244580325], "DamageType": [3454344768, 151347233] }, "609421557": { "Destination": [2244580325], "DamageType": [2303181850, 151347233] }, "609421558": { "Destination": [2244580325], "DamageType": [1847026933, 151347233] }, "609421566": { "Destination": [2244580325] }, "609421567": { "Destination": [2244580325], "KillType": [1] }, "610104115": { "ActivityMode": [2394616003] }, "613601972": { "Destination": [677774031] }, "648317971": { "ActivityMode": [910991990] }, "658526296": { "Destination": [1729879943], "DamageType": [151347233] }, "679437494": { "Destination": [1729879943], "ItemCategory": [8] }, "686467364": { "Destination": [677774031] }, "692805471": { "ActivityMode": [1164760504] }, "695153738": { "Destination": [1729879943] }, "695153739": { "Destination": [1729879943], "KillType": [3] }, "695180421": { "Destination": [677774031] }, "699674453": { "ActivityMode": [608898761], "Destination": [697502628] }, "709535744": { "Destination": [677774031], "DamageType": [2303181850, 1847026933, 3454344768] }, "709535745": { "Destination": [677774031], "DamageType": [3373582085] }, "709535746": { "Destination": [677774031], "ItemCategory": [5] }, "709535747": { "Destination": [677774031] }, "709535748": { "Destination": [677774031], "ItemCategory": [9] }, "709535749": { "Destination": [677774031], "ItemCategory": [3954685534] }, "709535750": { "Destination": [677774031], "ItemCategory": [6] }, "709535751": { "Destination": [677774031] }, "712137159": { "ActivityMode": [1673724806], "ItemCategory": [3954685534] }, "736610326": { "Destination": [1729879943], "DamageType": [3373582085, 2303181850, 1847026933, 3454344768] }, "736610327": { "Destination": [1729879943] }, "745565840": { "Destination": [1729879943] }, "745565841": { "Destination": [1729879943] }, "748013106": { "DamageType": [2303181850] }, "768824550": { "Destination": [1729879943], "ItemCategory": [12] }, "785647193": { "Destination": [1729879943] }, "802552739": { "Destination": [1729879943] }, "811475414": { "DamageType": [3949783978] }, "836278577": { "KillType": [5] }, "836380479": { "ActivityMode": [1673724806], "ItemCategory": [8] }, "848944256": { "ActivityMode": [2294590554], "KillType": [4] }, "848944257": { "ActivityMode": [2294590554], "KillType": [3] }, "848944258": { "ActivityMode": [2294590554], "DamageType": [2303181850, 1847026933, 3454344768] }, "848944259": { "ActivityMode": [2294590554] }, "848944260": { "ActivityMode": [2294590554] }, "848944261": { "ActivityMode": [2294590554], "DamageType": [3373582085] }, "848944262": { "ActivityMode": [2294590554], "DamageType": [2303181850] }, "848944263": { "ActivityMode": [2294590554], "DamageType": [3454344768] }, "871673916": { "Destination": [1416096592] }, "877925542": { "Destination": [1729879943], "DamageType": [3373582085] }, "890416150": { "Destination": [1729879943] }, "890416151": { "Destination": [1729879943], "KillType": [0, 4] }, "901575776": { "Destination": [2481646875], "KillType": [4] }, "901575777": { "Destination": [2481646875] }, "901575778": { "Destination": [2481646875], "DamageType": [151347233] }, "901575779": { "Destination": [2481646875], "DamageType": [3454344768] }, "901575780": { "Destination": [2481646875], "DamageType": [2303181850] }, "901575781": { "Destination": [2481646875], "DamageType": [1847026933] }, "901575782": { "Destination": [2481646875], "KillType": [4] }, "901575783": { "Destination": [2481646875] }, "901575790": { "Destination": [2481646875], "DamageType": [2303181850] }, "901575791": { "Destination": [2481646875], "DamageType": [1847026933] }, "903424492": { "ActivityMode": [1673724806], "KillType": [2] }, "904478927": { "ActivityMode": [2394616003], "DamageType": [1847026933, 151347233] }, "908878819": { "DamageType": [151347233] }, "913332794": { "DamageType": [1847026933], "KillType": [1] }, "916853149": { "ActivityMode": [1826469369] }, "926942410": { "ActivityMode": [2394616003] }, "938102884": { "ActivityMode": [910991990] }, "938102885": { "ActivityMode": [910991990] }, "938102886": { "ActivityMode": [910991990] }, "938102887": { "ActivityMode": [910991990] }, "942111489": { "Destination": [1416096592] }, "956734255": { "Destination": [1729879943], "ItemCategory": [7] }, "972929708": { "Destination": [2244580325] }, "972929711": { "Destination": [2244580325] }, "973983247": { "Destination": [1416096592] }, "973983256": { "Destination": [1416096592] }, "999548402": { "Destination": [1729879943] }, "999548403": { "Destination": [1729879943] }, "1001399024": { "Destination": [1416096592] }, "1001741317": { "Destination": [677774031] }, "1009165881": { "ItemCategory": [5, 12, 3954685534] }, "1009165882": { "ItemCategory": [6, 14], "KillType": [4] }, "1009165883": { "ItemCategory": [7, 8] }, "1009165884": { "ItemCategory": [10, 3871742104] }, "1013534599": { "Destination": [1729879943] }, "1022687405": { "Destination": [1729879943] }, "1025057518": { "Destination": [677774031] }, "1043681923": { "ItemCategory": [54] }, "1055369100": { "Destination": [1729879943], "DamageType": [1847026933] }, "1056064029": { "Destination": [2481646875], "ItemCategory": [5, 153950757, -153950757], "KillType": [2] }, "1069076942": { "Destination": [1416096592] }, "1077245329": { "ActivityMode": [1848252830], "KillType": [4] }, "1077245330": { "ActivityMode": [1848252830] }, "1077245331": { "ActivityMode": [1848252830] }, "1080549749": { "Destination": [677774031] }, "1083494845": { "ActivityMode": [1673724806], "DamageType": [3373582085] }, "1092992613": { "Destination": [3990611421] }, "1098618397": { "ActivityMode": [2394616003], "DamageType": [2303181850], "KillType": [3, 0] }, "1098618398": { "ActivityMode": [2394616003], "DamageType": [2303181850, 3454344768], "KillType": [2] }, "1098618399": { "DamageType": [3454344768], "KillType": [1, 4] }, "1099249219": { "ActivityMode": [2394616003] }, "1099446506": { "ActivityMode": [1673724806] }, "1113229786": { "ActivityMode": [1164760504] }, "1123124244": { "Destination": [1416096592] }, "1127960548": { "Destination": [1729879943] }, "1127960549": { "Destination": [1729879943] }, "1131956134": { "Destination": [1729879943], "ItemCategory": [12] }, "1131956135": { "Destination": [1729879943], "ItemCategory": [10], "KillType": [4] }, "1131956136": { "Destination": [1729879943], "ItemCategory": [11] }, "1131956137": { "Destination": [1729879943], "ItemCategory": [3954685534] }, "1131956138": { "Destination": [1729879943], "ItemCategory": [3317538576], "KillType": [4] }, "1131956139": { "Destination": [1729879943], "ItemCategory": [14], "KillType": [4] }, "1131956140": { "Destination": [1729879943], "ItemCategory": [6], "KillType": [4] }, "1131956141": { "Destination": [1729879943], "ItemCategory": [5] }, "1131956142": { "Destination": [1729879943], "ItemCategory": [7], "KillType": [4] }, "1131956143": { "Destination": [1729879943], "ItemCategory": [8], "KillType": [4] }, "1136139768": { "ActivityMode": [2294590554], "ItemCategory": [2489664120] }, "1136139769": { "ActivityMode": [2294590554], "ItemCategory": [11] }, "1136139770": { "ActivityMode": [2294590554], "ItemCategory": [7] }, "1136139771": { "ActivityMode": [2294590554], "ItemCategory": [5] }, "1136139774": { "ActivityMode": [2294590554], "ItemCategory": [10] }, "1136139775": { "ActivityMode": [2294590554], "ItemCategory": [6] }, "1143110718": { "Destination": [677774031] }, "1147672297": { "Destination": [1416096592] }, "1148638263": { "ActivityMode": [1673724806], "DamageType": [1847026933] }, "1163649882": { "Destination": [677774031] }, "1163649883": { "Destination": [677774031] }, "1164306016": { "Destination": [2244580325], "DamageType": [1847026933] }, "1164306017": { "Destination": [2244580325], "KillType": [4] }, "1164306024": { "Destination": [2244580325] }, "1164306026": { "Destination": [2244580325], "DamageType": [1847026933] }, "1164306027": { "Destination": [2244580325], "KillType": [4] }, "1164306028": { "Destination": [2244580325], "DamageType": [3454344768] }, "1164306029": { "Destination": [2244580325], "DamageType": [2303181850] }, "1164306030": { "Destination": [2244580325] }, "1164306031": { "Destination": [2244580325], "DamageType": [151347233] }, "1181188552": { "DamageType": [3949783978] }, "1183240368": { "ActivityMode": [332181804] }, "1183240369": { "Destination": [677774031] }, "1190558120": { "Destination": [1729879943], "DamageType": [2303181850] }, "1190558122": { "Destination": [1729879943], "DamageType": [3454344768] }, "1190558123": { "Destination": [1729879943], "DamageType": [1847026933] }, "1190558124": { "Destination": [1729879943], "KillType": [3] }, "1190558125": { "Destination": [1729879943], "KillType": [4] }, "1190558127": { "Destination": [1729879943] }, "1202973617": { "Destination": [1416096592] }, "1214796460": { "DamageType": [3454344768] }, "1225647738": { "Destination": [1416096592] }, "1231721206": { "DamageType": [3454344768] }, "1238879616": { "DamageType": [1847026933], "KillType": [1] }, "1239957059": { "Destination": [1729879943] }, "1239957062": { "Destination": [1729879943] }, "1239957064": { "Destination": [1729879943] }, "1244906310": { "ActivityMode": [1673724806], "DamageType": [3454344768] }, "1246673684": { "ActivityMode": [608898761] }, "1249119257": { "KillType": [4] }, "1257909267": { "Destination": [677774031] }, "1264358026": { "ActivityMode": [608898761] }, "1264945960": { "ActivityMode": [1686739444], "Destination": [677774031] }, "1280324466": { "DamageType": [3454344768] }, "1285862604": { "Destination": [677774031] }, "1285862605": { "DamageType": [1847026933] }, "1295283020": { "Destination": [677774031] }, "1299795970": { "DamageType": [151347233] }, "1319723906": { "Destination": [1416096592] }, "1322767807": { "DamageType": [1847026933] }, "1322836372": { "Destination": [677774031] }, "1322836373": { "Destination": [677774031] }, "1325063399": { "Destination": [677774031] }, "1326233664": { "Destination": [2244580325], "ItemCategory": [13], "KillType": [2] }, "1326233665": { "Destination": [2244580325], "ItemCategory": [5, 12, 7] }, "1326233666": { "Destination": [2244580325] }, "1326233667": { "Destination": [2244580325] }, "1326233668": { "Destination": [2244580325] }, "1326233669": { "Destination": [2244580325] }, "1326233670": { "Destination": [2244580325] }, "1326233678": { "Destination": [2244580325] }, "1326233679": { "Destination": [2244580325] }, "1330233961": { "Destination": [677774031], "ItemCategory": [6] }, "1334385022": { "Destination": [1729879943] }, "1334385023": { "Destination": [1729879943], "ItemCategory": [13] }, "1335967964": { "Destination": [677774031], "ItemCategory": [9] }, "1339692425": { "KillType": [2] }, "1340580577": { "Destination": [677774031], "DamageType": [2303181850] }, "1340580578": { "Destination": [677774031] }, "1352027002": { "ItemCategory": [9, 1504945536] }, "1359460005": { "ActivityMode": [1164760504, 1673724806] }, "1359460007": { "ActivityMode": [1673724806] }, "1359658752": { "ActivityMode": [910991990, 2239249083, 1826469369], "DamageType": [1847026933, 2303181850, 3454344768, 151347233, 3949783978] }, "1359658753": { "ActivityMode": [910991990, 2239249083, 1826469369] }, "1359658754": { "ActivityMode": [910991990, 2239249083, 1826469369], "KillType": [0, 4] }, "1359658755": { "ActivityMode": [910991990, 2239249083, 1826469369], "KillType": [1, 2] }, "1360203916": { "ActivityMode": [1848252830], "DamageType": [151347233], "KillType": [2, 0] }, "1375475507": { "Destination": [697502628] }, "1380868249": { "Destination": [677774031] }, "1388623635": { "DamageType": [3454344768] }, "1390863957": { "DamageType": [1847026933] }, "1391363409": { "Destination": [1416096592] }, "1391563758": { "Destination": [1416096592] }, "1399678798": { "Destination": [1729879943] }, "1403685364": { "Destination": [2481646875] }, "1403685365": { "Destination": [2481646875] }, "1403685367": { "Destination": [2481646875] }, "1405737712": { "ItemCategory": [3317538576], "KillType": [4] }, "1405737713": { "ItemCategory": [2489664120] }, "1405737714": { "DamageType": [1847026933] }, "1405737715": { "DamageType": [3949783978] }, "1405737716": { "DamageType": [151347233] }, "1405737717": { "DamageType": [3454344768] }, "1405737718": { "DamageType": [2303181850] }, "1405737719": { "ItemCategory": [5] }, "1405737720": { "ItemCategory": [7], "KillType": [4] }, "1405737721": { "KillType": [2] }, "1407750585": { "Destination": [2481646875], "DamageType": [3949783978] }, "1407788735": { "DamageType": [1847026933] }, "1416287687": { "Destination": [697502628] }, "1422515306": { "ActivityMode": [1164760504] }, "1424280353": { "Destination": [1416096592] }, "1429228592": { "Destination": [1729879943] }, "1429228593": { "Destination": [1729879943], "KillType": [4] }, "1429228594": { "Destination": [1729879943], "KillType": [4] }, "1429228595": { "Destination": [1729879943], "KillType": [4] }, "1429553682": { "Destination": [2481646875] }, "1438363709": { "Destination": [1729879943] }, "1444985899": { "Destination": [677774031] }, "1456070464": { "ItemCategory": [1504945536], "KillType": [4] }, "1456070465": { "ItemCategory": [3871742104] }, "1456070472": { "ItemCategory": [3954685534] }, "1456070473": { "ItemCategory": [14], "KillType": [4] }, "1456070474": { "ItemCategory": [12] }, "1456070475": { "ItemCategory": [153950757, -153950757] }, "1456070476": { "ItemCategory": [8], "KillType": [4] }, "1456070477": { "ItemCategory": [13] }, "1456070478": { "ItemCategory": [54] }, "1456070479": { "ItemCategory": [10], "KillType": [4] }, "1467183958": { "Destination": [1729879943], "ItemCategory": [1504945536] }, "1475436128": { "ActivityMode": [2294590554] }, "1475436129": { "ActivityMode": [2294590554] }, "1475436130": { "ActivityMode": [2294590554] }, "1475436132": { "ActivityMode": [2294590554], "ItemCategory": [7] }, "1475436133": { "ActivityMode": [2294590554], "ItemCategory": [2489664120] }, "1475436134": { "ActivityMode": [2294590554], "ItemCategory": [6] }, "1475436135": { "ActivityMode": [2294590554], "ItemCategory": [54] }, "1475436138": { "ActivityMode": [2294590554], "ItemCategory": [11] }, "1475436139": { "ActivityMode": [2294590554], "ItemCategory": [153950757, -153950757] }, "1479328354": { "Destination": [677774031] }, "1490203679": { "ActivityMode": [1164760504], "DamageType": [151347233], "ItemCategory": [3317538576, 1504945536, 10] }, "1495419808": { "ActivityMode": [2394616003], "Destination": [1729879943] }, "1505040824": { "ActivityMode": [3789021730] }, "1505040825": { "ActivityMode": [3789021730] }, "1505040826": { "DamageType": [3454344768] }, "1505040827": { "DamageType": [2303181850] }, "1505040830": { "DamageType": [1847026933] }, "1505040831": { "ActivityMode": [3789021730] }, "1506403330": { "ActivityMode": [1164760504] }, "1506403331": { "ActivityMode": [1164760504] }, "1524355568": { "Destination": [677774031], "ItemCategory": [7] }, "1553536199": { "Destination": [677774031] }, "1589407358": { "Destination": [1729879943] }, "1606160568": { "Destination": [1729879943] }, "1606160570": { "Destination": [1729879943], "KillType": [4] }, "1606160571": { "Destination": [1729879943], "KillType": [3] }, "1606160572": { "Destination": [1729879943], "DamageType": [1847026933] }, "1606160573": { "Destination": [1729879943], "DamageType": [3454344768] }, "1606160575": { "Destination": [1729879943], "DamageType": [2303181850] }, "1647306237": { "Destination": [677774031], "ItemCategory": [12] }, "1649087790": { "ActivityMode": [1673724806], "KillType": [0] }, "1649477996": { "Destination": [2481646875], "KillType": [1] }, "1666445095": { "DamageType": [151347233] }, "1672479016": { "Destination": [1729879943] }, "1672479018": { "Destination": [1729879943] }, "1672479019": { "Destination": [1729879943] }, "1709644011": { "KillType": [4] }, "1711768439": { "ActivityMode": [2394616003], "Destination": [3607432451] }, "1716679237": { "ItemCategory": [6], "KillType": [4] }, "1716679242": { "ItemCategory": [9] }, "1716679243": { "ItemCategory": [11] }, "1719082278": { "Destination": [677774031] }, "1721560654": { "Destination": [1729879943], "ItemCategory": [14] }, "1730090752": { "Destination": [1729879943] }, "1730090753": { "Destination": [1729879943] }, "1730090754": { "Destination": [1729879943] }, "1730090755": { "Destination": [1729879943] }, "1740492685": { "Destination": [677774031] }, "1761112788": { "ActivityMode": [1673724806], "DamageType": [2303181850] }, "1768818273": { "Destination": [677774031] }, "1774447862": { "ActivityMode": [1673724806], "KillType": [4] }, "1777618773": { "ActivityMode": [1673724806] }, "1781597160": { "Destination": [1729879943] }, "1784552506": { "Destination": [1729879943] }, "1799098212": { "DamageType": [2303181850, 3454344768] }, "1799098213": { "DamageType": [3454344768], "KillType": [4] }, "1799098215": { "DamageType": [2303181850], "KillType": [3] }, "1807819922": { "ActivityMode": [1673724806] }, "1811415984": { "Destination": [1729879943] }, "1811415985": { "Destination": [1729879943] }, "1811415987": { "Destination": [1729879943], "KillType": [4] }, "1868886741": { "ActivityMode": [910991990] }, "1868886744": { "ActivityMode": [910991990] }, "1868886746": { "ActivityMode": [910991990] }, "1868886747": { "ActivityMode": [910991990] }, "1868886748": { "ActivityMode": [910991990] }, "1868886749": { "ActivityMode": [910991990] }, "1868886750": { "ActivityMode": [910991990] }, "1868886751": { "ActivityMode": [910991990] }, "1891580900": { "ActivityMode": [3789021730], "DamageType": [151347233] }, "1891580901": { "Destination": [1729879943], "DamageType": [151347233] }, "1901543625": { "Destination": [677774031] }, "1907272938": { "Destination": [3990611421] }, "1912981534": { "Destination": [1416096592] }, "1914390712": { "Destination": [1729879943] }, "1916728396": { "Destination": [1729879943] }, "1916728397": { "Destination": [1729879943] }, "1916728399": { "Destination": [1729879943] }, "1919833088": { "ActivityMode": [910991990] }, "1921694150": { "Destination": [1729879943] }, "1921694151": { "Destination": [1729879943] }, "1930571866": { "ActivityMode": [1164760504], "DamageType": [151347233], "ItemCategory": [54], "KillType": [0] }, "1931939585": { "ActivityMode": [1164760504] }, "1937694075": { "Destination": [1729879943], "ItemCategory": [3317538576] }, "1938005548": { "Destination": [1729879943] }, "1938005549": { "Destination": [1729879943], "ItemCategory": [5] }, "1940788333": { "Destination": [1416096592] }, "1941367117": { "DamageType": [3949783978] }, "1987856996": { "Destination": [1729879943] }, "1987856997": { "Destination": [1729879943] }, "2003401139": { "DamageType": [3373582085, 2303181850, 1847026933, 3454344768], "ItemCategory": [-153950757] }, "2032932188": { "Destination": [677774031], "ItemCategory": [54] }, "2035309547": { "Destination": [2244580325] }, "2038129348": { "ActivityMode": [2394616003], "Destination": [697502628] }, "2057293080": { "Destination": [1729879943], "ItemCategory": [54] }, "2057304092": { "DamageType": [1847026933, 2303181850, 3454344768] }, "2057304093": { "Destination": [1729879943] }, "2057304094": { "Destination": [1729879943] }, "2057304095": { "Destination": [1729879943] }, "2095884141": { "ActivityMode": [608898761] }, "2098370169": { "KillType": [4] }, "2104674665": { "Destination": [1416096592] }, "2111258436": { "Destination": [1729879943] }, "2111258437": { "Destination": [1729879943] }, "2111258439": { "Destination": [1729879943], "KillType": [4] }, "2158533271": { "Destination": [697502628] }, "2169515393": { "ActivityMode": [1848252830] }, "2169515412": { "ActivityMode": [1848252830] }, "2173508794": { "Destination": [1416096592] }, "2174686652": { "KillType": [1] }, "2174686653": { "ActivityMode": [1848252830, 1164760504], "ItemCategory": [3317538576, 10], "KillType": [4] }, "2174686654": { "Destination": [677774031] }, "2178015352": { "Destination": [677774031] }, "2179804365": { "Destination": [3990611421] }, "2192699203": { "Destination": [1416096592] }, "2193695127": { "KillType": [4] }, "2196850110": { "ActivityMode": [1164760504] }, "2205014136": { "Destination": [1729879943], "DamageType": [151347233] }, "2205014137": { "ActivityMode": [1848252830], "DamageType": [151347233] }, "2205014139": { "Destination": [1729879943], "DamageType": [3454344768] }, "2205125592": { "ActivityMode": [1164760504], "KillType": [1, 2, 0] }, "2205125593": { "ActivityMode": [1164760504], "DamageType": [1847026933, 2303181850, 3454344768, 151347233, 3949783978] }, "2205125594": { "ActivityMode": [1164760504], "KillType": [4] }, "2211737354": { "Destination": [1729879943], "KillType": [4] }, "2211737355": { "Destination": [1729879943] }, "2220926672": { "ItemCategory": [11], "KillType": [3, 0] }, "2220926673": { "ItemCategory": [153950757, 13] }, "2220926681": { "ItemCategory": [2489664120] }, "2220926682": { "ItemCategory": [3871742104], "KillType": [0] }, "2220926683": { "ItemCategory": [3317538576, 54] }, "2220926684": { "DamageType": [1847026933] }, "2220926685": { "KillType": [4] }, "2220926686": { "DamageType": [2303181850] }, "2220926687": { "DamageType": [3454344768] }, "2231267738": { "ActivityMode": [1848252830] }, "2231267739": { "ActivityMode": [1848252830], "DamageType": [2303181850] }, "2231267740": { "ActivityMode": [1848252830] }, "2231267741": { "ActivityMode": [1848252830] }, "2231267742": { "ActivityMode": [1848252830], "DamageType": [1847026933] }, "2231267743": { "ActivityMode": [1848252830], "DamageType": [3454344768] }, "2264047518": { "Destination": [1729879943], "ItemCategory": [7] }, "2264047519": { "Destination": [1729879943] }, "2271002093": { "Destination": [1416096592] }, "2299999154": { "Destination": [1729879943], "ItemCategory": [11] }, "2299999155": { "Destination": [1729879943] }, "2300040094": { "ActivityMode": [1164760504], "ItemCategory": [153950757, -153950757] }, "2305643258": { "Destination": [3990611421] }, "2308577369": { "ActivityMode": [1848252830], "DamageType": [151347233], "ItemCategory": [153950757], "KillType": [1, 2] }, "2338128705": { "Destination": [1416096592] }, "2348066127": { "Destination": [1729879943], "KillType": [4] }, "2366292847": { "ItemCategory": [3317538576] }, "2367271818": { "Destination": [2244580325] }, "2369227171": { "ActivityMode": [2394616003] }, "2376985818": { "ActivityMode": [1164760504] }, "2390836807": { "Destination": [1729879943], "DamageType": [2303181850] }, "2391068514": { "Destination": [1729879943], "ItemCategory": [3954685534] }, "2392484247": { "ActivityMode": [1164760504] }, "2398495226": { "Destination": [1729879943], "ItemCategory": [13] }, "2398495227": { "Destination": [1729879943] }, "2398816281": { "ActivityMode": [1673724806], "ItemCategory": [9] }, "2420567618": { "ActivityMode": [2394616003], "Destination": [1416096592] }, "2423789933": { "ActivityMode": [2394616003] }, "2433772164": { "Destination": [697502628] }, "2437268361": { "ActivityMode": [608898761] }, "2462005227": { "ActivityMode": [1673724806], "DamageType": [3454344768] }, "2468543640": { "Destination": [1729879943], "ItemCategory": [14], "KillType": [4] }, "2468543641": { "Destination": [1729879943] }, "2469435739": { "ActivityMode": [1673724806], "KillType": [1] }, "2471210438": { "DamageType": [151347233] }, "2494871935": { "Destination": [1416096592] }, "2498699770": { "KillType": [1] }, "2510021888": { "Destination": [1729879943] }, "2522697935": { "Destination": [1729879943] }, "2529410397": { "ActivityMode": [608898761] }, "2530201259": { "Destination": [3990611421] }, "2543372992": { "Destination": [1729879943], "KillType": [3] }, "2543372993": { "Destination": [1729879943] }, "2545852114": { "Destination": [1729879943] }, "2557879920": { "Destination": [2481646875] }, "2561829998": { "KillType": [1] }, "2564508104": { "Destination": [1729879943] }, "2564508105": { "Destination": [1729879943], "DamageType": [151347233] }, "2569392434": { "Destination": [1729879943] }, "2569392435": { "Destination": [1729879943], "ItemCategory": [7] }, "2572296686": { "Destination": [1729879943] }, "2572296687": { "Destination": [1729879943] }, "2577006690": { "DamageType": [151347233] }, "2579262077": { "ActivityMode": [2394616003], "Destination": [677774031] }, "2591382989": { "DamageType": [2303181850], "KillType": [1] }, "2593817224": { "Destination": [1729879943] }, "2593817225": { "Destination": [1729879943] }, "2597966745": { "Destination": [1729879943], "KillType": [2] }, "2604394056": { "Destination": [697502628] }, "2609952321": { "Destination": [2244580325] }, "2617538380": { "Destination": [1729879943] }, "2617538381": { "Destination": [1729879943], "ItemCategory": [11] }, "2631989274": { "Destination": [677774031] }, "2632313001": { "DamageType": [3949783978] }, "2642052560": { "Destination": [2481646875] }, "2642052561": { "Destination": [2481646875], "DamageType": [151347233] }, "2642052564": { "Destination": [2481646875], "DamageType": [1847026933] }, "2642052565": { "Destination": [2481646875], "ItemCategory": [1504945536, 8, 14, 10], "KillType": [4] }, "2642052566": { "Destination": [2481646875], "DamageType": [3454344768] }, "2642052567": { "Destination": [2481646875], "DamageType": [2303181850] }, "2652352679": { "ActivityMode": [1164760504] }, "2653391362": { "Destination": [677774031] }, "2655190752": { "Destination": [1729879943] }, "2655190753": { "Destination": [1729879943] }, "2655190754": { "Destination": [1729879943], "KillType": [3] }, "2661223204": { "Destination": [1416096592] }, "2663541547": { "ActivityMode": [608898761] }, "2665385178": { "Destination": [697502628] }, "2682789196": { "Destination": [1729879943], "ItemCategory": [13] }, "2682863308": { "ActivityMode": [1164760504] }, "2683598861": { "Destination": [1729879943] }, "2697214271": { "Destination": [1729879943], "KillType": [0] }, "2702385474": { "DamageType": [2303181850], "KillType": [1] }, "2702385475": { "DamageType": [1847026933], "KillType": [1] }, "2702385476": { "DamageType": [3454344768], "KillType": [1] }, "2702385480": { "ItemCategory": [5, 12, 3954685534] }, "2702385481": { "ItemCategory": [11], "KillType": [0] }, "2719669554": { "ActivityMode": [1673724806], "KillType": [4] }, "2719903032": { "ActivityMode": [1164760504], "DamageType": [151347233] }, "2723555752": { "ItemCategory": [12] }, "2729195975": { "ActivityMode": [608898761] }, "2736452412": { "ActivityMode": [2043403989] }, "2739557089": { "DamageType": [1847026933, 2303181850, 3454344768] }, "2740092152": { "KillType": [3] }, "2744299504": { "DamageType": [2303181850] }, "2761262388": { "ActivityMode": [2394616003], "Destination": [3990611421] }, "2780182450": { "ActivityMode": [1673724806], "DamageType": [3454344768] }, "2798544374": { "Destination": [1729879943] }, "2798544375": { "Destination": [1729879943] }, "2806019770": { "Destination": [697502628] }, "2808722824": { "Destination": [2244580325], "DamageType": [2303181850] }, "2808722825": { "Destination": [2244580325], "DamageType": [3454344768] }, "2808722826": { "Destination": [2244580325], "DamageType": [151347233] }, "2808722827": { "Destination": [2244580325], "KillType": [3] }, "2808722828": { "Destination": [2244580325], "KillType": [1] }, "2809262148": { "ActivityMode": [1164760504], "DamageType": [1847026933, 2303181850, 3454344768] }, "2809262149": { "ActivityMode": [1164760504], "DamageType": [3373582085, 151347233, 3949783978] }, "2809262150": { "ActivityMode": [1164760504] }, "2829320432": { "DamageType": [2303181850] }, "2833790084": { "Destination": [1729879943] }, "2833790085": { "Destination": [1729879943] }, "2833790092": { "Destination": [1729879943] }, "2833790093": { "Destination": [1729879943] }, "2833790094": { "Destination": [1729879943] }, "2833790095": { "Destination": [1729879943] }, "2836688212": { "Destination": [677774031] }, "2849007504": { "ActivityMode": [1673724806], "ItemCategory": [5] }, "2850552048": { "ActivityMode": [608898761] }, "2850552049": { "ActivityMode": [608898761], "DamageType": [1847026933, 2303181850, 3454344768] }, "2850552050": { "ActivityMode": [608898761] }, "2850552051": { "ActivityMode": [608898761] }, "2850552055": { "ActivityMode": [608898761] }, "2855966190": { "ItemCategory": [3954685534] }, "2856178253": { "Destination": [1729879943], "DamageType": [2303181850, 1847026933, 3454344768] }, "2857343851": { "DamageType": [151347233] }, "2859321092": { "Destination": [1729879943] }, "2859321093": { "Destination": [1729879943] }, "2864963410": { "ActivityMode": [1164760504], "DamageType": [1847026933] }, "2864963411": { "ActivityMode": [1164760504] }, "2864963412": { "ActivityMode": [1164760504] }, "2864963413": { "ActivityMode": [1164760504], "DamageType": [3454344768] }, "2864963414": { "ActivityMode": [1164760504], "DamageType": [2303181850] }, "2864963415": { "ActivityMode": [1164760504, 3199098480] }, "2880619157": { "ActivityMode": [2394616003], "DamageType": [151347233] }, "2881153063": { "Destination": [3990611421] }, "2890914464": { "DamageType": [151347233] }, "2893357936": { "Destination": [2244580325], "ItemCategory": [1504945536, 13, 8, 10] }, "2893357937": { "Destination": [2244580325], "DamageType": [3373582085] }, "2893357938": { "Destination": [2244580325], "DamageType": [2303181850, 1847026933, 3454344768] }, "2893357939": { "Destination": [2244580325] }, "2893357941": { "Destination": [2244580325], "ItemCategory": [11, 14, 54] }, "2893357942": { "Destination": [2244580325], "ItemCategory": [5, 9, 6, 12] }, "2893357943": { "Destination": [2244580325], "ItemCategory": [3317538576, 153950757, -153950757, 7, 2489664120] }, "2893357948": { "Destination": [2244580325], "ItemCategory": [6, 11, 54, 2489664120] }, "2893357949": { "Destination": [2244580325], "ItemCategory": [5, 12, 7] }, "2897490274": { "Destination": [1729879943], "KillType": [1] }, "2898442314": { "Destination": [1729879943] }, "2898442315": { "Destination": [1729879943], "DamageType": [3373582085, 2303181850, 1847026933, 3454344768] }, "2913208495": { "DamageType": [2303181850], "KillType": [1, 2] }, "2915388915": { "Destination": [1729879943], "ItemCategory": [10] }, "2922106190": { "Destination": [1729879943], "ItemCategory": [6] }, "2927847132": { "DamageType": [2303181850, 1847026933, 3454344768] }, "2930833968": { "ActivityMode": [2294590554], "ItemCategory": [7] }, "2930833969": { "ActivityMode": [2294590554], "ItemCategory": [2489664120] }, "2930833970": { "ActivityMode": [2294590554], "ItemCategory": [6] }, "2930833971": { "ActivityMode": [2294590554], "ItemCategory": [54] }, "2930833972": { "ActivityMode": [2294590554], "ItemCategory": [10] }, "2930833973": { "ActivityMode": [2294590554], "ItemCategory": [13] }, "2930833974": { "ActivityMode": [2294590554], "ItemCategory": [11] }, "2930833975": { "ActivityMode": [2294590554], "ItemCategory": [153950757, -153950757] }, "2930833978": { "ActivityMode": [2294590554], "DamageType": [1847026933] }, "2938914564": { "Destination": [1729879943] }, "2938914565": { "Destination": [1729879943], "ItemCategory": [14] }, "2940269474": { "ActivityMode": [1673724806], "KillType": [0] }, "2943602311": { "Destination": [1729879943] }, "2943701307": { "ActivityMode": [1673724806], "DamageType": [1847026933] }, "2975357706": { "Destination": [1729879943], "DamageType": [151347233], "KillType": [0] }, "2975357707": { "DamageType": [151347233] }, "2982138514": { "Destination": [1729879943], "ItemCategory": [5] }, "2982138515": { "Destination": [1729879943] }, "3039004941": { "Destination": [3990611421] }, "3051422226": { "ActivityMode": [1673724806] }, "3061679979": { "Destination": [677774031] }, "3068019720": { "Destination": [2244580325] }, "3070629378": { "Destination": [2244580325] }, "3070629379": { "Destination": [2244580325], "KillType": [1] }, "3072532148": { "DamageType": [3949783978] }, "3077591736": { "ActivityMode": [332181804], "KillType": [0] }, "3077591737": { "ActivityMode": [1686739444], "Destination": [677774031] }, "3078568019": { "ActivityMode": [608898761] }, "3083700040": { "ActivityMode": [1164760504] }, "3091479969": { "Destination": [677774031] }, "3099386391": { "ActivityMode": [1673724806], "KillType": [1] }, "3106430225": { "Destination": [1729879943], "KillType": [3] }, "3109359396": { "Destination": [677774031], "ItemCategory": [6] }, "3109359397": { "Destination": [677774031], "KillType": [4] }, "3109359400": { "Destination": [677774031] }, "3109359401": { "Destination": [677774031], "KillType": [0] }, "3109359402": { "Destination": [677774031], "ItemCategory": [3954685534] }, "3109359403": { "Destination": [677774031], "ItemCategory": [5] }, "3109359404": { "Destination": [677774031], "DamageType": [1847026933] }, "3109359405": { "Destination": [677774031], "DamageType": [2303181850] }, "3109359406": { "Destination": [677774031], "KillType": [2] }, "3109359407": { "Destination": [677774031], "DamageType": [3454344768] }, "3117358110": { "DamageType": [3949783978] }, "3127215872": { "DamageType": [3373582085] }, "3127215876": { "ItemCategory": [5, 12, 3954685534, 2489664120] }, "3127215877": { "ItemCategory": [54, 3871742104], "KillType": [0] }, "3127215878": { "DamageType": [1847026933, 2303181850, 3454344768] }, "3127215879": { "DamageType": [151347233, 3949783978] }, "3127215882": { "ItemCategory": [1504945536, 8, 10], "KillType": [4] }, "3127215883": { "ItemCategory": [153950757, -153950757, 13], "KillType": [2] }, "3127309054": { "Destination": [3990611421] }, "3136133748": { "Destination": [1729879943] }, "3143993585": { "KillType": [1] }, "3143993588": { "KillType": [1] }, "3147591220": { "KillType": [4] }, "3149086433": { "KillType": [4] }, "3160771168": { "ItemCategory": [9, 6, 11], "KillType": [0] }, "3160771177": { "ActivityMode": [1164760504] }, "3160771178": { "KillType": [4] }, "3160771180": { "ActivityMode": [1164760504] }, "3160771182": { "KillType": [3] }, "3160771183": { "KillType": [1] }, "3161484303": { "Destination": [2481646875], "DamageType": [3949783978] }, "3166523986": { "ItemCategory": [7, 2489664120], "KillType": [4] }, "3166523987": { "ItemCategory": [12, 3954685534] }, "3169161323": { "ActivityMode": [1848252830, 1164760504] }, "3169276698": { "Destination": [1729879943] }, "3169276699": { "Destination": [1729879943] }, "3177548756": { "ItemCategory": [14] }, "3177548757": { "ItemCategory": [3954685534] }, "3177548760": { "DamageType": [151347233, 3949783978] }, "3177548761": { "ItemCategory": [6] }, "3177548762": { "ItemCategory": [9] }, "3177548763": { "ItemCategory": [11] }, "3177548765": { "DamageType": [1847026933, 2303181850, 3454344768] }, "3177548766": { "DamageType": [151347233, 3949783978] }, "3177548767": { "DamageType": [1847026933, 2303181850, 3454344768] }, "3186325461": { "ActivityMode": [1686739444] }, "3194326342": { "ItemCategory": [2489664120] }, "3194326343": { "ItemCategory": [3317538576] }, "3194326344": { "ItemCategory": [1504945536] }, "3194326345": { "ItemCategory": [3871742104] }, "3194326346": { "ItemCategory": [54] }, "3194326347": { "ItemCategory": [10] }, "3194326348": { "ItemCategory": [8] }, "3194326349": { "ItemCategory": [13] }, "3194326350": { "ItemCategory": [12] }, "3194326351": { "ItemCategory": [153950757, -153950757] }, "3202230444": { "ItemCategory": [3871742104] }, "3207732940": { "Destination": [1416096592] }, "3211104032": { "DamageType": [151347233] }, "3211104033": { "DamageType": [3454344768] }, "3211104034": { "DamageType": [1847026933] }, "3211104035": { "DamageType": [3949783978] }, "3211104036": { "ActivityMode": [1164760504] }, "3211104037": { "ActivityMode": [1164760504] }, "3211104038": { "DamageType": [2303181850] }, "3211104039": { "ActivityMode": [1164760504] }, "3211104042": { "ActivityMode": [1164760504] }, "3211104043": { "ActivityMode": [1164760504] }, "3216117824": { "ActivityMode": [608898761] }, "3227881617": { "ItemCategory": [7] }, "3227881620": { "ActivityMode": [1164760504] }, "3227881621": { "ActivityMode": [1164760504] }, "3227881622": { "ItemCategory": [5] }, "3227881623": { "ActivityMode": [1164760504] }, "3236557427": { "Destination": [1416096592] }, "3238546247": { "ActivityMode": [1673724806], "ItemCategory": [6] }, "3250989939": { "ItemCategory": [12], "KillType": [4] }, "3257985301": { "DamageType": [151347233] }, "3261415715": { "DamageType": [3454344768], "KillType": [1, 2] }, "3265116853": { "KillType": [1] }, "3273285996": { "KillType": [4] }, "3294953102": { "Destination": [2244580325] }, "3311748604": { "DamageType": [3454344768] }, "3312871394": { "ActivityMode": [1164760504] }, "3323206616": { "Destination": [1729879943] }, "3337739523": { "Destination": [1416096592] }, "3339057603": { "ActivityMode": [1848252830], "DamageType": [151347233] }, "3354680137": { "KillType": [4] }, "3383073000": { "ActivityMode": [2394616003] }, "3399113824": { "Destination": [1729879943], "ItemCategory": [5] }, "3399113825": { "Destination": [1729879943] }, "3400121653": { "ActivityMode": [2394616003], "Destination": [3990611421] }, "3401935316": { "Destination": [1729879943] }, "3405012994": { "Destination": [677774031] }, "3406224904": { "Destination": [677774031] }, "3413453049": { "Destination": [2481646875] }, "3424456965": { "ActivityMode": [1848252830, 1164760504], "ItemCategory": [153950757, -153950757] }, "3427598700": { "DamageType": [151347233, 3949783978] }, "3449625264": { "Destination": [2481646875], "ItemCategory": [5, 12, 7, 3954685534] }, "3449625265": { "Destination": [2481646875] }, "3449625266": { "Destination": [2481646875] }, "3449625267": { "Destination": [2481646875], "ItemCategory": [153950757, -153950757, 13], "KillType": [2] }, "3449625268": { "Destination": [2481646875] }, "3449625269": { "Destination": [2481646875] }, "3449625270": { "Destination": [2481646875] }, "3449625271": { "Destination": [2481646875] }, "3449625276": { "Destination": [2481646875] }, "3449625277": { "Destination": [2481646875] }, "3452090911": { "ActivityMode": [1164760504] }, "3459548846": { "Destination": [1729879943] }, "3486089701": { "Destination": [3607432451] }, "3486486983": { "Destination": [1729879943], "DamageType": [3454344768] }, "3494281470": { "Destination": [2244580325] }, "3497386880": { "ActivityMode": [1673724806] }, "3513278115": { "DamageType": [151347233] }, "3519390048": { "Destination": [1729879943] }, "3519390049": { "Destination": [1729879943] }, "3519390050": { "Destination": [1729879943] }, "3519390051": { "DamageType": [3373582085, 2303181850, 1847026933, 3454344768] }, "3522663041": { "ActivityMode": [2294590554], "DamageType": [3454344768] }, "3522663042": { "ActivityMode": [2294590554], "DamageType": [1847026933], "KillType": [2] }, "3522663043": { "ActivityMode": [2294590554], "DamageType": [2303181850], "KillType": [0] }, "3525138784": { "ItemCategory": [153950757, -153950757, 13] }, "3525138786": { "ItemCategory": [6, 14] }, "3525138787": { "ItemCategory": [3317538576, 7, 8] }, "3525138791": { "ItemCategory": [9, 1504945536, 2489664120] }, "3531980021": { "Destination": [1416096592] }, "3533425632": { "ItemCategory": [5] }, "3535843838": { "KillType": [3] }, "3536491683": { "Destination": [677774031], "ItemCategory": [5] }, "3537310563": { "Destination": [2244580325] }, "3539440752": { "ActivityMode": [2294590554] }, "3539440753": { "ActivityMode": [2294590554] }, "3539440754": { "ActivityMode": [2294590554], "DamageType": [3373582085] }, "3539440755": { "ActivityMode": [2294590554] }, "3539440756": { "ActivityMode": [2294590554] }, "3539440757": { "ActivityMode": [2294590554], "DamageType": [2303181850, 1847026933, 3454344768] }, "3539440758": { "ActivityMode": [2294590554], "ItemCategory": [13] }, "3539440759": { "ActivityMode": [2294590554], "ItemCategory": [10] }, "3539440766": { "ActivityMode": [2294590554] }, "3540140708": { "Destination": [677774031] }, "3540140709": { "Destination": [677774031] }, "3540140710": { "Destination": [677774031] }, "3540140711": { "Destination": [677774031] }, "3560528864": { "DamageType": [151347233] }, "3564089590": { "Destination": [1416096592] }, "3566017743": { "ActivityMode": [1164760504], "ItemCategory": [5] }, "3568578179": { "Destination": [677774031] }, "3577538269": { "Destination": [3990611421] }, "3586902510": { "Destination": [3607432451] }, "3593342633": { "Destination": [1729879943] }, "3597986944": { "Destination": [677774031] }, "3597986945": { "Destination": [677774031], "KillType": [3] }, "3597986946": { "Destination": [677774031] }, "3597986947": { "Destination": [677774031] }, "3597986949": { "Destination": [677774031], "KillType": [3] }, "3597986950": { "Destination": [677774031], "KillType": [3] }, "3597986951": { "Destination": [677774031] }, "3601169172": { "Destination": [1729879943] }, "3601169173": { "Destination": [1729879943], "ItemCategory": [13] }, "3604605346": { "ActivityMode": [1164760504, 157639802] }, "3610649340": { "Destination": [2481646875] }, "3612061731": { "Destination": [1729879943] }, "3629873748": { "Destination": [1729879943] }, "3629873749": { "Destination": [1729879943] }, "3629873750": { "Destination": [1729879943] }, "3629873751": { "Destination": [1729879943], "KillType": [4] }, "3634238441": { "Destination": [2244580325] }, "3636568846": { "ActivityMode": [1673724806], "ItemCategory": [7] }, "3636655077": { "ActivityMode": [1673724806] }, "3638686275": { "Destination": [2481646875], "DamageType": [3949783978] }, "3644638448": { "ActivityMode": [1164760504] }, "3647168216": { "ActivityMode": [1673724806], "KillType": [2] }, "3651031892": { "Destination": [1416096592] }, "3656769954": { "ActivityMode": [1848252830] }, "3670562313": { "DamageType": [3949783978] }, "3676690659": { "DamageType": [3454344768], "KillType": [1] }, "3679234405": { "ActivityMode": [1164760504], "DamageType": [151347233], "KillType": [0, 4] }, "3691663932": { "Destination": [1729879943], "DamageType": [3373582085], "KillType": [4] }, "3691663933": { "Destination": [1729879943] }, "3702334623": { "KillType": [1] }, "3735363404": { "Destination": [677774031] }, "3742423637": { "DamageType": [3949783978] }, "3758175010": { "ActivityMode": [2394616003], "Destination": [3990611421] }, "3765938216": { "ActivityMode": [2394616003], "DamageType": [151347233], "KillType": [1, 0] }, "3791769270": { "Destination": [1416096592] }, "3805576189": { "ActivityMode": [1164760504] }, "3817061966": { "Destination": [1729879943] }, "3817061967": { "Destination": [1729879943], "DamageType": [1847026933, 2303181850, 3454344768] }, "3821311001": { "ActivityMode": [608898761] }, "3824718318": { "DamageType": [151347233] }, "3831027416": { "Destination": [1729879943], "ItemCategory": [54] }, "3831027417": { "Destination": [1729879943], "ItemCategory": [9] }, "3831027418": { "Destination": [1729879943], "ItemCategory": [13] }, "3831027419": { "Destination": [1729879943], "ItemCategory": [153950757, -153950757] }, "3831027422": { "Destination": [1729879943], "ItemCategory": [1504945536], "KillType": [4] }, "3831027423": { "Destination": [1729879943], "ItemCategory": [2489664120] }, "3846956448": { "Destination": [2481646875] }, "3846956449": { "Destination": [2481646875], "ItemCategory": [6, 11, 54, 2489664120] }, "3846956450": { "Destination": [2481646875], "DamageType": [3373582085] }, "3846956451": { "Destination": [2481646875], "DamageType": [2303181850, 1847026933, 3454344768] }, "3846956452": { "Destination": [2481646875], "ItemCategory": [3317538576, 153950757, -153950757, 7, 2489664120] }, "3846956453": { "Destination": [2481646875], "ItemCategory": [1504945536, 13, 8, 10] }, "3846956454": { "Destination": [2481646875], "ItemCategory": [3954685534, 11, 14, 54] }, "3846956455": { "Destination": [2481646875], "ItemCategory": [5, 9, 6, 12] }, "3846956462": { "Destination": [2481646875], "ItemCategory": [5, 12, 7, 3954685534] }, "3846956463": { "Destination": [2481646875], "ItemCategory": [3317538576, 9, 153950757, -153950757, 13] }, "3848329302": { "DamageType": [151347233] }, "3896716771": { "Destination": [1729879943], "ItemCategory": [5] }, "3897611837": { "Destination": [1729879943], "KillType": [4] }, "3897611838": { "Destination": [1729879943] }, "3897611839": { "Destination": [1729879943] }, "3904432054": { "Destination": [1729879943], "ItemCategory": [9] }, "3907528162": { "Destination": [2481646875] }, "3956285600": { "Destination": [2244580325] }, "3963636042": { "ActivityMode": [2394616003], "DamageType": [151347233] }, "3966078208": { "Destination": [697502628] }, "3984170438": { "Destination": [1729879943] }, "3985923224": { "Destination": [677774031] }, "3985923225": { "ItemCategory": [54] }, "3985923226": { "Destination": [677774031] }, "3993430445": { "Destination": [3607432451] }, "3993430447": { "Destination": [697502628] }, "3999201698": { "Destination": [677774031], "ItemCategory": [10] }, "4003211415": { "Destination": [677774031] }, "4010589224": { "Destination": [677774031] }, "4016941122": { "Destination": [677774031] }, "4019596374": { "ActivityMode": [3789021730] }, "4023822046": { "ActivityMode": [1164760504] }, "4036455306": { "ActivityMode": [608898761] }, "4044330008": { "Destination": [1729879943] }, "4044330009": { "ActivityMode": [332181804] }, "4044330010": { "Destination": [677774031] }, "4044330015": { "Destination": [1416096592] }, "4050047257": { "Destination": [2244580325] }, "4050047258": { "Destination": [2244580325], "DamageType": [151347233] }, "4050047259": { "Destination": [2244580325], "DamageType": [3454344768] }, "4050047260": { "Destination": [2244580325], "DamageType": [2303181850] }, "4050047261": { "Destination": [2244580325], "DamageType": [1847026933] }, "4050047262": { "Destination": [2244580325], "ItemCategory": [1504945536, 8, 14, 10], "KillType": [4] }, "4050047263": { "Destination": [2244580325], "ItemCategory": [3317538576, 9, 153950757, -153950757, 13] }, "4059065847": { "ActivityMode": [1673724806], "KillType": [1] }, "4066891017": { "Destination": [697502628] }, "4084168684": { "Destination": [1729879943], "ItemCategory": [7] }, "4084168685": { "Destination": [1729879943] }, "4095766153": { "Destination": [1416096592] }, "4107530250": { "Destination": [1729879943], "ItemCategory": [54] }, "4120120192": { "Destination": [1729879943], "ItemCategory": [11] }, "4120120193": { "Destination": [1729879943] }, "4126111451": { "ActivityMode": [1673724806], "ItemCategory": [153950757, -153950757] }, "4136665139": { "ActivityMode": [1164760504] }, "4141787158": { "Destination": [677774031] }, "4145233940": { "Destination": [1729879943], "ItemCategory": [153950757, -153950757] }, "4150657477": { "Destination": [677774031] }, "4156787317": { "Destination": [677774031] }, "4165581488": { "Destination": [1729879943] }, "4188409722": { "DamageType": [2303181850] }, "4195357129": { "Destination": [2244580325] }, "4204937008": { "Destination": [1729879943] }, "4204937009": { "Destination": [1729879943] }, "4204937010": { "Destination": [1729879943] }, "4204937011": { "Destination": [1729879943] }, "4205508063": { "Destination": [3990611421] }, "4208016531": { "Destination": [677774031], "KillType": [1, 2, 0] }, "4213377134": { "Destination": [1416096592] }, "4216908924": { "Destination": [3607432451] }, "4224533134": { "Destination": [1729879943] }, "4224767086": { "Destination": [1729879943] }, "4224767087": { "Destination": [1729879943], "ItemCategory": [14] }, "4233883935": { "Destination": [677774031] }, "4235693605": { "ActivityMode": [1673724806], "DamageType": [2303181850] }, "4252762628": { "Destination": [2481646875] }, "4265122765": { "Destination": [677774031] }, "4265906105": { "Destination": [2481646875], "ItemCategory": [153950757, -153950757] }, "4269063328": { "Destination": [2481646875], "KillType": [3] }, "4269063329": { "Destination": [2481646875], "KillType": [1] }, "4269063330": { "Destination": [2481646875], "DamageType": [3454344768] }, "4269063331": { "Destination": [2481646875], "DamageType": [151347233] }, "4276814926": { "ActivityMode": [1848252830] } } ================================================ FILE: src/data/d2/raid-mod-plug-category-hashes.json ================================================ [ 13646368, 125494331, 1486918022, 1703496685, 2106680364, 2140235634, 2173937871, 2207493141, 2274750776 ] ================================================ FILE: src/data/d2/reduced-cost-mod-mappings.ts ================================================ export const normalToReducedMod: { [normalModHash: number]: number } = { '14520248': 2318667184, '40751621': 4149682173, '48578555': 3047946307, '84503918': 1208761894, '193878019': 2267311547, '319908131': 792400107, '335129856': 2801811288, '350061697': 2519597513, '377010989': 1153260021, '450381139': 4283953067, '467550918': 1017385934, '531057500': 830369300, '534479613': 2436471653, '554409585': 644105, '633101315': 2771425787, '686455429': 3075302157, '688956976': 56663992, '707237917': 657773637, '721001747': 1801153435, '802695661': 2815817957, '837201397': 3188328909, '856936828': 2136310244, '953234331': 3539253011, '967052942': 96682422, '1024379611': 422994787, '1039115606': 3791691774, '1079896271': 634608391, '1097608874': 579997810, '1125523126': 3046678542, '1130820873': 3461249873, '1180408010': 2568808786, '1193713026': 1672155562, '1237786518': 1124184622, '1262438062': 2325151798, '1301391064': 877723168, '1305536863': 1891463783, '1388734897': 897335593, '1435557120': 3896141096, '1553790504': 1019574576, '1589556860': 2888195476, '1669792723': 2413278875, '1677180919': 2479297167, '1755737153': 1783952505, '1763668984': 2562645296, '1763780622': 1139671158, '1781551382': 1255614814, '1834163303': 2246316031, '1971149752': 1103878128, '2059068466': 2794359402, '2076329105': 1561736585, '2257238439': 1305848463, '2319885414': 2283894334, '2407398462': 2305736470, '2414626352': 1604394872, '2447449706': 1709236482, '2452545487': 2634786903, '2467203039': 2214424583, '2485657760': 965934024, '2526773280': 411014648, '2532323436': 2113881316, '2577472338': 3181984586, '2586562813': 2237975061, '2595839237': 3775800797, '2657604783': 1702273159, '2719698929': 331268185, '2724068510': 3847471926, '2724608735': 1866564759, '2734674728': 3174771856, '2773358872': 1210012576, '2793473444': 3184690956, '2793548555': 703902595, '2921714558': 2526922422, '2959504464': 1118428792, '3000428062': 3013778406, '3067648983': 531665167, '3094620656': 293178904, '3112965625': 1501094193, '3149307605': 2982306509, '3160387771': 1044888195, '3194530172': 3846931924, '3323910164': 3979300428, '3410844187': 2788997987, '3437323171': 3887037435, '3462414552': 3294892432, '3467460423': 3914973263, '3518670115': 2899505723, '3573031954': 3276278122, '3581696649': 2805854721, '3599522901': 2031584061, '3685945823': 3657186535, '3712696020': 2996369932, '3719981603': 930759851, '3726719281': 3245543337, '3775916472': 3675553168, '3926119246': 3279257734, '3938489430': 1036557198, '3979621113': 3598972737, '3980769162': 3224649746, '3994043492': 1627901452, '4039026690': 4267244538, '4046357305': 1901221009, '4081595582': 2245839670, '4182064480': 1924584408, '4183296050': 3808902618, '4204488676': 2493161484, '4244246940': 95934356, '4255093903': 1086997255, '4287799666': 1763607626, '4287822553': 2303417969, '4294909663': 3798468567, }; export const reducedToNormalMod: { [reducedModHash: number]: number } = Object.fromEntries( Object.entries(normalToReducedMod).map(([normal, reduced]) => [reduced, parseInt(normal, 10)]), ); ================================================ FILE: src/data/d2/season-tags.json ================================================ { "redwar": 1, "osiris": 2, "warmind": 3, "outlaw": 4, "forge": 5, "drifter": 6, "opulence": 7, "undying": 8, "dawn": 9, "worthy": 10, "arrivals": 11, "hunt": 12, "chosen": 13, "splicer": 14, "lost": 15, "risen": 16, "haunted": 17, "plunder": 18, "seraph": 19, "defiance": 20, "deep": 21, "witch": 22, "wish": 23, "echoes": 24, "revenant": 25, "heresy": 26, "reclamation": 27, "lawless": 28 } ================================================ FILE: src/data/d2/season-to-source.json ================================================ { "sources": { "11666839": 2, "13912404": 1, "21494224": 26, "32323943": 11, "43842395": 22, "92433064": 18, "100617404": 2, "110159004": 4, "139160732": 15, "139599745": 1, "148542898": 2, "160129377": 18, "164083100": 15, "178383754": 28, "210885364": 13, "266896577": 5, "286427063": 12, "287889699": 13, "351235593": 4, "354493557": 2, "406406003": 14, "409652252": 16, "443340273": 15, "443793689": 26, "462484651": 14, "464727567": 15, "508245276": 13, "539840256": 10, "547767158": 2, "550270332": 8, "557146120": 1, "561126969": 23, "569214265": 1, "594760007": 12, "596084342": 27, "613435025": 23, "613791463": 22, "629617846": 12, "633667627": 27, "641018908": 3, "654652973": 4, "675740011": 15, "677167936": 1, "707740602": 4, "709680645": 20, "712662541": 27, "736336644": 4, "745186842": 9, "745481267": 26, "772619302": 1, "794422188": 22, "817015032": 2, "877404349": 26, "887452441": 13, "901482731": 20, "925197669": 5, "929025440": 7, "958460845": 24, "1007078046": 16, "1054169368": 15, "1076222895": 1, "1102533392": 15, "1103518848": 3, "1126234343": 10, "1127923611": 4, "1148859274": 12, "1175566043": 2, "1216155659": 8, "1217831333": 10, "1225476079": 19, "1253026984": 8, "1266018974": 25, "1281387702": 1, "1282207663": 17, "1331532890": 27, "1360005982": 1, "1397119901": 1, "1400219831": 2, "1405897559": 12, "1411886787": 2, "1412777465": 4, "1433518193": 13, "1457456824": 5, "1459595344": 8, "1462687159": 1, "1465057711": 15, "1465990789": 5, "1483048674": 5, "1497107113": 10, "1505938361": 27, "1564061133": 9, "1568732528": 23, "1581680964": 2, "1581731027": 24, "1596507419": 5, "1597738585": 19, "1605890568": 25, "1618754228": 9, "1654120320": 2, "1666677522": 17, "1677921161": 4, "1723452413": 12, "1745960977": 8, "1751739544": 20, "1763998430": 15, "1792957897": 26, "1897187034": 22, "1919933822": 11, "1923289424": 10, "1924238751": 3, "1926923633": 22, "1943976384": 26, "1992319882": 20, "1995616326": 14, "2011810450": 13, "2039343154": 11, "2045032171": 27, "2050870152": 24, "2085016678": 5, "2124937714": 22, "2127551856": 27, "2187511136": 6, "2206233229": 1, "2223404774": 1, "2230358252": 11, "2242939082": 2, "2278847330": 20, "2292685703": 11, "2296534980": 24, "2308290458": 2, "2335095658": 11, "2347293565": 2, "2353223954": 11, "2363489105": 16, "2364515524": 19, "2364933290": 14, "2379344669": 11, "2384327872": 5, "2463956052": 25, "2487203690": 2, "2514060836": 24, "2541753910": 5, "2585665369": 23, "2601524261": 9, "2607739079": 10, "2607970476": 26, "2622122683": 22, "2648408612": 7, "2658055900": 8, "2669524419": 11, "2675385179": 4, "2694738712": 14, "2700267533": 24, "2717017239": 2, "2723305286": 11, "2744321951": 1, "2755511565": 21, "2765304727": 1, "2778435282": 8, "2797674516": 15, "2805208672": 4, "2851783112": 4, "2856954949": 12, "2926805810": 3, "2927095256": 28, "2929562373": 1, "2929839827": 27, "2937902448": 2, "2952071500": 23, "2966694626": 5, "2967385539": 14, "2986594962": 23, "2988465950": 1, "3022766747": 4, "3041847664": 20, "3047033583": 5, "3067146211": 2, "3079246067": 2, "3094114967": 15, "3095773956": 26, "3098906085": 4, "3099553329": 1, "3100467592": 1, "3112857249": 1, "3126774631": 2, "3174947771": 20, "3190710249": 20, "3190938946": 8, "3226099405": 14, "3237053501": 27, "3247513834": 28, "3257722699": 5, "3277652589": 15, "3288974535": 21, "3390269646": 3, "3404977524": 9, "3422985544": 10, "3431853656": 1, "3466789677": 28, "3494247523": 8, "3522070610": 11, "3563833902": 16, "3564069447": 21, "3567813252": 19, "3656787928": 13, "3693722471": 11, "3704442923": 2, "3724111213": 7, "3736521079": 1, "3740731576": 18, "3747711246": 22, "3764925750": 5, "3807243511": 12, "3829951162": 27, "3874934421": 4, "3936473457": 2, "3942778906": 22, "3954922099": 16, "4006434081": 20, "4008954452": 25, "4009509410": 1, "4066007318": 2, "4079816474": 13, "4122810030": 8, "4173145322": 16, "4208190159": 1, "4247521481": 5, "4263201695": 2, "4267157320": 9, "4278841194": 23, "4284811963": 27, "4288102251": 2, "4290227252": 5, "4290499613": 1 } } ================================================ FILE: src/data/d2/seasonal-armor-mods.json ================================================ [ 110793779, 2077016094, 2812559045, 2831374162, 3458160468 ] ================================================ FILE: src/data/d2/seasonal-challenges.json ================================================ {} ================================================ FILE: src/data/d2/seasons.json ================================================ { "1053696": 28, "4772275": 12, "7665310": 10, "9762701": 9, "10656656": 10, "14279832": 28, "15628614": 27, "16277432": 1, "16277433": 1, "16638392": 24, "16638393": 24, "18179099": 19, "18743640": 25, "29194593": 3, "31953744": 1, "31953746": 1, "31953747": 1, "32452990": 28, "33795475": 1, "38912240": 4, "40494562": 25, "40512774": 1, "40549752": 5, "40549754": 5, "40549755": 5, "40549756": 5, "40549757": 5, "40549758": 5, "40549759": 5, "41589556": 20, "46532100": 27, "48790291": 1, "51925409": 16, "51945187": 16, "52039562": 14, "54004490": 22, "54004491": 22, "54004492": 22, "54004495": 22, "55826751": 9, "56435118": 24, "60802325": 7, "60936592": 2, "61740554": 22, "61740555": 22, "63024229": 2, "63348567": 24, "72775246": 27, "74038334": 12, "76739872": 28, "76764720": 9, "76764721": 9, "76764722": 9, "79833168": 24, "79833169": 24, "79833170": 24, "79833172": 24, "79833173": 24, "79833174": 24, "79833175": 24, "80303715": 2, "80338527": 27, "82149497": 28, "83207822": 27, "84509991": 27, "85572943": 27, "87994267": 3, "89300158": 19, "91501643": 12, "93029342": 1, "93029343": 1, "95537029": 1, "101761654": 19, "105300091": 2, "105911828": 17, "107607117": 24, "116784191": 7, "116918307": 12, "125438212": 3, "126615444": 28, "127603177": 22, "128069444": 25, "130076865": 23, "130305507": 14, "131450128": 27, "131450129": 27, "131450130": 27, "131450131": 27, "131450132": 27, "131450133": 27, "131450134": 27, "131450135": 27, "131450136": 27, "131450137": 27, "133558437": 20, "134085740": 16, "134085741": 16, "134085743": 16, "136291991": 12, "139044974": 28, "139044987": 28, "139281386": 14, "139281387": 14, "140842223": 7, "143299650": 7, "145178484": 19, "146081606": 24, "146910655": 23, "147902881": 26, "147998763": 11, "148339472": 28, "153144587": 7, "156845152": 13, "156845153": 13, "158426788": 27, "159093681": 23, "165005424": 28, "165005425": 28, "165005426": 28, "165005427": 28, "165005428": 28, "165005429": 28, "165005430": 28, "165005431": 28, "165005438": 28, "165005439": 28, "165824714": 28, "166080281": 24, "166080282": 24, "166080283": 24, "167651268": 2, "168042471": 8, "168438525": 14, "170952905": 8, "171635528": 21, "171635529": 21, "171635530": 21, "171748061": 4, "177150401": 25, "177463495": 22, "177568179": 22, "178451227": 3, "178757083": 26, "179202986": 3, "179654396": 24, "180108390": 4, "180108391": 4, "181754010": 3, "181783008": 28, "181783009": 28, "181783016": 27, "181783017": 27, "181783018": 27, "181783019": 27, "181783020": 27, "181783021": 27, "181783022": 28, "181783023": 27, "182537383": 25, "183980811": 1, "185321778": 4, "185321779": 4, "187034864": 28, "193705506": 28, "195422190": 10, "197822824": 8, "199733460": 15, "204059183": 11, "204918711": 1, "213377779": 16, "213458862": 10, "213708705": 27, "214374661": 16, "215338185": 28, "215338186": 28, "215338187": 28, "215338188": 28, "215338189": 28, "215338190": 28, "215338191": 28, "215596672": 2, "215618528": 21, "217626588": 11, "217626590": 11, "217626591": 11, "221164696": 2, "221164697": 2, "223308216": 1, "223597399": 16, "223776600": 3, "223776601": 3, "225456309": 27, "226436555": 7, "228647643": 28, "228855596": 28, "231432261": 3, "232908218": 28, "233125175": 16, "233896077": 21, "234526563": 27, "234970842": 3, "238335348": 24, "239189018": 8, "242419885": 20, "242730894": 7, "242828821": 11, "244316917": 28, "249784434": 12, "250513201": 3, "253815640": 14, "253922071": 20, "254514436": 8, "258025496": 22, "259267040": 10, "259522459": 22, "260425472": 21, "260521462": 1, "261886690": 11, "261886691": 11, "263371512": 2, "263371513": 2, "263371514": 2, "263371515": 2, "263371516": 2, "263371517": 2, "263371518": 2, "263371519": 2, "265428940": 1, "267290351": 1, "267916777": 6, "268035671": 20, "269052681": 11, "269339124": 2, "270610849": 28, "270671209": 2, "279289257": 2, "280187206": 1, "280533858": 8, "280903308": 27, "281400500": 27, "281400501": 27, "281400504": 27, "281400505": 27, "281400506": 27, "281400507": 27, "281400509": 27, "281400510": 27, "281400511": 27, "281718534": 6, "281718535": 6, "286098604": 6, "286098606": 6, "286098607": 6, "286365244": 28, "287923131": 11, "289811733": 24, "292778444": 1, "293622383": 12, "294129361": 16, "296300392": 28, "296300393": 28, "296300394": 28, "298089926": 3, "298089927": 3, "298334058": 8, "298334059": 8, "298334063": 8, "301177215": 15, "302085772": 27, "307805822": 24, "309000506": 27, "309633584": 22, "309731943": 28, "311128543": 28, "311164277": 27, "311219520": 27, "311219521": 27, "311219522": 27, "311219523": 27, "311219525": 27, "311219526": 27, "311219527": 27, "311219532": 27, "311219533": 27, "313685908": 2, "313685909": 2, "313685910": 2, "313685911": 2, "314979874": 26, "316332414": 27, "316740353": 2, "317350029": 24, "317465074": 2, "317465075": 2, "320750826": 11, "320750827": 11, "322499005": 27, "323635379": 27, "327680457": 24, "333041308": 1, "335763433": 3, "335988632": 25, "338612265": 3, "340680574": 27, "343171404": 27, "343482208": 14, "343482209": 14, "343789488": 24, "343789489": 24, "346065606": 3, "346066663": 22, "346473619": 21, "346878928": 1, "346878930": 1, "350054538": 24, "350414343": 27, "351707684": 19, "353932628": 9, "354404332": 24, "354404333": 24, "354404335": 24, "362494424": 28, "364132014": 27, "365524697": 9, "366019830": 20, "366418892": 1, "366474809": 16, "367778392": 2, "371705660": 24, "374780030": 3, "375770440": 4, "377540667": 5, "379479305": 1, "380589816": 28, "380637670": 28, "383734233": 17, "383734236": 17, "383734237": 17, "383734238": 17, "385402729": 17, "386610725": 16, "386969562": 15, "388458229": 3, "388618952": 27, "389459993": 28, "391889347": 7, "392504592": 23, "393225356": 11, "396134792": 3, "400177089": 3, "400623726": 26, "403465735": 7, "405115754": 1, "407316269": 26, "410537200": 25, "411709610": 13, "411993769": 28, "412299646": 1, "412523236": 27, "412523237": 27, "412523240": 27, "412523241": 27, "412523242": 27, "412523243": 27, "412523244": 27, "412523245": 27, "412523246": 27, "412523247": 27, "412682987": 27, "413901114": 22, "414672658": 3, "418753772": 15, "418753773": 15, "419435523": 6, "419571946": 2, "425681240": 25, "426147374": 2, "426283548": 4, "426368686": 11, "428862428": 12, "430448851": 14, "431150731": 27, "433582103": 11, "436665408": 6, "437190449": 28, "437421244": 5, "444202817": 14, "444302065": 27, "444302066": 27, "444302067": 27, "446078514": 27, "446299266": 8, "448177396": 27, "448286934": 27, "448286935": 27, "448286936": 27, "448286937": 27, "448286938": 27, "448286939": 27, "448286940": 27, "448286941": 27, "448286942": 27, "448286943": 27, "450844637": 7, "452400791": 3, "454222017": 27, "454583975": 1, "455024236": 27, "455112322": 26, "456874738": 15, "460400687": 4, "461171930": 1, "461727014": 24, "462395043": 20, "462856096": 27, "462856097": 27, "462856098": 27, "462856099": 27, "462856100": 27, "462856101": 27, "462856102": 27, "462856103": 27, "462856108": 27, "462856109": 27, "463166592": 3, "464814870": 24, "465064624": 27, "465064625": 27, "465064626": 27, "465064627": 27, "465064628": 27, "465064629": 27, "465064630": 27, "465064631": 27, "465064634": 27, "465064635": 27, "465351846": 16, "466236950": 9, "466236951": 9, "466796172": 4, "466796173": 4, "466796175": 4, "468456840": 14, "468456841": 14, "469511105": 22, "471426285": 21, "471764029": 5, "476513004": 21, "478751073": 1, "481345527": 1, "481675395": 1, "481842213": 27, "484260847": 16, "485239296": 14, "486920324": 11, "492834021": 6, "494187468": 18, "494187469": 18, "494493680": 1, "495940989": 19, "498109198": 9, "503145555": 24, "506974826": 28, "510898033": 21, "510898034": 21, "510898035": 21, "511487664": 17, "514561133": 11, "514572085": 15, "514698970": 25, "518566750": 2, "518930465": 3, "519046634": 24, "527001801": 25, "531005896": 3, "532530776": 13, "534630542": 27, "537036424": 7, "537036425": 7, "537036427": 7, "537041732": 20, "538867171": 22, "540603119": 4, "540653483": 3, "540791504": 26, "540791505": 26, "542198399": 18, "544720811": 10, "544864743": 18, "547061385": 28, "547387122": 27, "547804116": 10, "548288405": 10, "549246985": 1, "549468645": 27, "552146359": 28, "557947534": 23, "558870048": 7, "566102034": 1, "566818961": 25, "568239104": 20, "568769546": 17, "569039965": 19, "570143750": 4, "574167778": 7, "574406295": 15, "574433875": 17, "574694085": 7, "574790717": 7, "574898816": 26, "576069028": 24, "577331743": 27, "577345565": 7, "581237363": 4, "581782322": 28, "583402086": 27, "583548063": 3, "586497084": 21, "586671776": 24, "588138826": 23, "589151513": 2, "589719184": 28, "589978193": 14, "591672323": 27, "592081314": 27, "592081315": 27, "592288414": 3, "592288415": 3, "594509158": 11, "594640136": 2, "594640137": 2, "594640138": 2, "594640139": 2, "595406727": 9, "596633475": 14, "596833222": 2, "597612829": 20, "597733441": 28, "598178607": 1, "599687980": 2, "599777086": 28, "600923142": 27, "601948197": 25, "604124708": 14, "604581312": 16, "605209364": 21, "605812812": 27, "610201346": 14, "610443345": 1, "611182000": 27, "611182001": 27, "611182002": 27, "611182003": 27, "611182004": 27, "611182005": 27, "611182006": 27, "611182007": 27, "611182014": 27, "611182015": 27, "613647804": 12, "613984400": 22, "616392721": 15, "617617371": 16, "619391783": 25, "619729470": 27, "621701973": 3, "624775194": 6, "626649506": 2, "627596132": 7, "627959586": 27, "627959587": 27, "627959592": 27, "627959593": 27, "627959594": 27, "627959595": 27, "627959596": 27, "627959597": 27, "627959598": 27, "627959599": 27, "628413376": 24, "629159779": 11, "637391773": 20, "640136282": 11, "640411453": 3, "644737181": 27, "646486611": 1, "650418244": 20, "655712834": 23, "659359923": 16, "659709387": 24, "664042851": 1, "666522389": 27, "669267835": 2, "671247011": 5, "673231129": 27, "676453416": 28, "676554393": 28, "677939288": 3, "679887739": 5, "681808640": 21, "682455836": 20, "682455838": 20, "682455839": 20, "682783191": 3, "683947069": 1, "685004314": 10, "685157381": 1, "685157383": 1, "685299502": 4, "689294985": 25, "690228054": 5, "691392383": 27, "691819782": 24, "691914261": 2, "693901806": 26, "695719646": 28, "697983358": 28, "700322568": 1, "700322570": 1, "702981643": 2, "707118595": 15, "707997638": 26, "709082555": 28, "710694221": 23, "712123915": 11, "713403449": 3, "716099381": 26, "717767855": 21, "718136887": 1, "721111598": 27, "721111611": 27, "721146704": 3, "721208609": 1, "722310193": 27, "722346046": 25, "722967004": 28, "723006641": 26, "731529630": 28, "736513444": 27, "738873646": 3, "745759692": 2, "745759694": 2, "745759695": 2, "747033132": 3, "747033133": 3, "748214628": 1, "748692001": 28, "748692002": 28, "749168710": 23, "749248212": 28, "749248213": 28, "749248215": 28, "749733608": 11, "754050190": 14, "754513817": 26, "754884040": 20, "758932444": 27, "759016287": 28, "760272956": 24, "761663477": 28, "764833385": 28, "765122182": 19, "765946822": 22, "766122634": 28, "771273473": 4, "772163452": 28, "773659236": 15, "773659238": 15, "773659239": 15, "775189549": 23, "775684249": 28, "776529032": 1, "776529033": 1, "776529034": 1, "776878384": 2, "776878385": 2, "776878386": 2, "776878387": 2, "779082129": 28, "779962716": 2, "781214070": 27, "784499738": 7, "784603963": 18, "785442930": 4, "787024992": 16, "788073489": 25, "788073490": 25, "788073491": 25, "788073493": 25, "793351985": 28, "795389673": 1, "796298276": 15, "796633253": 4, "798307794": 23, "798985558": 25, "801509177": 11, "802191381": 1, "802191382": 1, "802191383": 1, "803297678": 28, "804309451": 15, "805381928": 24, "805756410": 28, "807693916": 24, "809643949": 27, "810321696": 11, "810623803": 4, "811724212": 3, "814448788": 1, "814520772": 23, "814933120": 1, "817283478": 23, "818240646": 15, "819191764": 4, "819191765": 4, "819191767": 4, "819232495": 16, "821031610": 27, "826128643": 19, "830497630": 3, "830789013": 26, "833418240": 28, "833755609": 27, "834178986": 7, "837509459": 3, "838156448": 13, "839740147": 7, "840447754": 27, "841151780": 28, "842157716": 3, "842157717": 3, "842157718": 3, "842157719": 3, "847859071": 15, "855333069": 1, "855333070": 1, "855333071": 1, "855351524": 5, "855351525": 5, "855968858": 27, "856000672": 8, "856633363": 11, "861860247": 27, "863007481": 1, "864505614": 1, "864505615": 1, "866034298": 18, "866034302": 18, "866128808": 28, "866128809": 28, "866128810": 28, "866128811": 28, "866128815": 28, "871210651": 2, "871819007": 28, "873100338": 13, "873720784": 12, "873770815": 3, "873999827": 28, "874877120": 1, "876119751": 11, "877063737": 28, "880745877": 3, "881651163": 15, "885593286": 1, "889173106": 25, "889413643": 2, "889443955": 9, "890476372": 27, "890476373": 27, "890476375": 27, "891771298": 27, "892683994": 27, "892683995": 27, "896032513": 27, "897074661": 15, "901903811": 28, "902219633": 28, "905869860": 4, "906505391": 22, "908153539": 20, "908153543": 20, "908153550": 20, "909175624": 27, "909175625": 27, "909175626": 27, "909175627": 27, "909175628": 27, "909175629": 27, "909175630": 27, "911356576": 10, "912222548": 5, "912276057": 12, "912515804": 5, "914653197": 4, "917896686": 14, "921357268": 2, "924779391": 1, "928070392": 16, "929148730": 7, "930064526": 1, "932194044": 24, "937162783": 4, "938804347": 1, "939209126": 3, "941476219": 27, "942888402": 3, "942888403": 3, "946867072": 20, "950899352": 1, "951413738": 12, "955394464": 1, "957763733": 27, "957848724": 12, "959098413": 3, "959508480": 27, "959508481": 27, "959508482": 27, "959508483": 27, "959508484": 27, "959508485": 27, "959508486": 27, "959508487": 27, "959508488": 27, "959508489": 27, "961496619": 3, "967650555": 3, "967781090": 10, "968360381": 6, "968669760": 25, "968669761": 25, "968669762": 25, "969863968": 2, "970956845": 27, "971728596": 6, "971728597": 6, "973639516": 28, "974161790": 28, "980228755": 28, "980228756": 28, "980228757": 28, "980228758": 28, "980228759": 28, "980898608": 3, "980898609": 3, "980898610": 3, "980898611": 3, "980898614": 3, "980898615": 3, "981450701": 28, "983967254": 20, "984002469": 4, "985750811": 22, "989310394": 27, "990304814": 1, "993540920": 27, "995900888": 23, "1002354686": 24, "1002555530": 22, "1005594230": 8, "1005594231": 8, "1007199041": 16, "1011670904": 3, "1011670905": 3, "1012254326": 1, "1012508294": 28, "1012508307": 28, "1013401891": 20, "1015069912": 25, "1015730268": 24, "1016114126": 8, "1018190408": 9, "1020589069": 14, "1021165796": 6, "1021443313": 26, "1021939294": 4, "1021939295": 4, "1024894484": 5, "1025347717": 27, "1026128010": 17, "1027203766": 6, "1027203767": 6, "1027749710": 8, "1028757552": 5, "1028757553": 5, "1028757554": 5, "1028757555": 5, "1028757556": 5, "1028757557": 5, "1028757558": 5, "1028757559": 5, "1028757567": 5, "1032136201": 27, "1037916674": 6, "1039280681": 22, "1039460381": 18, "1040380121": 28, "1042964491": 28, "1043883715": 13, "1044615214": 14, "1044615215": 14, "1045106944": 27, "1045633725": 1, "1045633727": 1, "1045929536": 1, "1045929537": 1, "1046955906": 1, "1047542010": 28, "1047542011": 28, "1049963080": 28, "1050368698": 11, "1051903593": 2, "1051938194": 1, "1051938195": 1, "1051938196": 1, "1051938197": 1, "1051938198": 1, "1051938199": 1, "1052553862": 2, "1052553863": 2, "1054640441": 25, "1056992393": 6, "1057119308": 3, "1059304051": 4, "1059396448": 26, "1059396449": 26, "1059396450": 26, "1059396451": 26, "1059396454": 26, "1061186327": 1, "1061507881": 28, "1065911138": 10, "1067975722": 25, "1067975723": 25, "1069117426": 27, "1069214754": 3, "1069380472": 25, "1072603541": 27, "1074758434": 16, "1075647353": 4, "1077836170": 20, "1078097668": 11, "1082381334": 22, "1082407266": 5, "1083938223": 22, "1084954603": 21, "1085403133": 26, "1087344267": 27, "1089851988": 2, "1089851989": 2, "1091411796": 15, "1091411797": 15, "1092719892": 5, "1095370002": 21, "1098249309": 28, "1098547840": 28, "1099926164": 23, "1099926165": 23, "1103965354": 22, "1105341592": 24, "1105928645": 2, "1114225063": 28, "1120048000": 5, "1120064792": 1, "1120186562": 15, "1122245142": 21, "1123433952": 25, "1124054883": 23, "1125064385": 14, "1126354272": 17, "1126889172": 11, "1127128759": 10, "1127558480": 27, "1127992351": 27, "1131177620": 27, "1131244817": 4, "1133567616": 27, "1134447515": 16, "1135392623": 26, "1135933726": 1, "1138508272": 11, "1138508276": 11, "1138508286": 11, "1141639721": 3, "1141716966": 15, "1143897057": 27, "1144054109": 27, "1146380447": 25, "1146620476": 28, "1147673598": 20, "1147673599": 20, "1150304053": 12, "1152762705": 8, "1153991042": 24, "1153991043": 24, "1154659864": 2, "1155452660": 23, "1160766519": 14, "1162929425": 5, "1163935877": 6, "1164089027": 28, "1165452712": 22, "1166152736": 5, "1166603202": 11, "1172941888": 27, "1175295126": 27, "1176948896": 28, "1176948897": 28, "1176948898": 28, "1176948899": 28, "1176948900": 28, "1176948901": 28, "1176948902": 28, "1176948903": 28, "1177179936": 1, "1178333691": 28, "1179310688": 27, "1179310689": 27, "1179310690": 27, "1179310691": 27, "1179310692": 27, "1179310693": 27, "1179310694": 27, "1179310695": 27, "1179310696": 27, "1179310697": 27, "1179571663": 26, "1181381245": 10, "1181969391": 9, "1182552175": 22, "1183116657": 25, "1185244198": 11, "1186946848": 27, "1186946854": 27, "1186946855": 27, "1187045864": 19, "1194085652": 28, "1199546220": 23, "1201773703": 28, "1201782502": 8, "1201782503": 8, "1202133695": 27, "1205148160": 10, "1205148161": 10, "1206696357": 28, "1206746476": 1, "1206746478": 1, "1208043873": 28, "1208826751": 23, "1209319450": 27, "1210362881": 1, "1210937132": 3, "1213492937": 24, "1214401677": 18, "1218004886": 27, "1219897208": 16, "1220635053": 27, "1220635064": 27, "1220787353": 27, "1221462631": 18, "1222467473": 11, "1223564136": 18, "1226858006": 28, "1226963430": 27, "1229033176": 21, "1229961870": 7, "1230647257": 11, "1230660647": 12, "1230660648": 12, "1232004606": 4, "1232390730": 16, "1234914148": 25, "1235471577": 28, "1239024834": 14, "1240184693": 21, "1242865739": 24, "1249334869": 12, "1250188421": 27, "1250597597": 24, "1251226123": 27, "1254077559": 28, "1256660988": 8, "1257250409": 26, "1257952519": 24, "1259278657": 3, "1260041928": 21, "1260362486": 18, "1260473085": 28, "1261107326": 22, "1266122672": 9, "1266122673": 9, "1273131832": 3, "1273131835": 3, "1273131836": 3, "1273354936": 13, "1273354937": 13, "1273510836": 6, "1274190840": 28, "1277051957": 27, "1277486053": 3, "1277677855": 1, "1280248869": 1, "1280755883": 5, "1280894514": 18, "1281013968": 1, "1281013969": 1, "1281013970": 1, "1281013971": 1, "1281013972": 1, "1281013973": 1, "1281013974": 1, "1281013975": 1, "1281013978": 1, "1281013979": 1, "1282122328": 14, "1283615405": 3, "1283654212": 8, "1284330836": 27, "1284563760": 1, "1284563761": 1, "1284563762": 1, "1284563763": 1, "1284563764": 1, "1284563765": 1, "1284563766": 1, "1284563767": 1, "1284563774": 1, "1284563775": 1, "1287851098": 5, "1288683596": 7, "1289590866": 13, "1289622079": 20, "1289637143": 26, "1290784012": 2, "1291068165": 3, "1291068170": 3, "1291068171": 3, "1291068172": 3, "1291931904": 28, "1292425061": 14, "1293002379": 1, "1294717622": 1, "1298644738": 22, "1298682514": 1, "1298682515": 1, "1301763821": 25, "1301766302": 22, "1302985294": 9, "1303706313": 13, "1305141224": 25, "1305141225": 25, "1305141227": 25, "1305274547": 1, "1312472209": 2, "1312626341": 3, "1314682851": 28, "1315728068": 28, "1317763552": 18, "1317763554": 18, "1317763555": 18, "1319502155": 14, "1319520530": 12, "1319537766": 14, "1319537767": 14, "1319899129": 24, "1320113564": 4, "1321546045": 2, "1323035623": 21, "1324151602": 11, "1326541974": 28, "1327083312": 28, "1327236527": 28, "1327630195": 10, "1328786973": 26, "1330563002": 4, "1333488564": 27, "1334842411": 2, "1334959255": 1, "1336181349": 28, "1337933721": 25, "1339405989": 3, "1339662042": 27, "1341471164": 3, "1344619700": 3, "1344619701": 3, "1345789366": 9, "1346652857": 28, "1350878542": 1, "1351738498": 25, "1352815976": 21, "1353919026": 27, "1353919027": 27, "1353919032": 27, "1353919033": 27, "1353919034": 27, "1353919035": 27, "1353919036": 27, "1353919037": 27, "1353919038": 27, "1353919039": 27, "1359039627": 27, "1361620030": 3, "1362709750": 14, "1362709751": 14, "1362725637": 25, "1363029408": 7, "1363029409": 7, "1363360060": 23, "1364413233": 5, "1365413546": 2, "1365491398": 7, "1367348576": 26, "1367348577": 26, "1370302135": 8, "1370696614": 27, "1370696615": 27, "1370696616": 27, "1370696617": 27, "1370696618": 27, "1370696619": 27, "1370696620": 27, "1370696621": 27, "1370696622": 27, "1370696623": 27, "1370974707": 2, "1372428179": 22, "1374246204": 17, "1374517485": 16, "1376512508": 27, "1376763596": 6, "1378496336": 2, "1378496337": 2, "1378639429": 28, "1379889389": 25, "1381973235": 23, "1384371471": 19, "1387474177": 27, "1387688628": 8, "1389298745": 14, "1389546626": 20, "1390587439": 1, "1390684733": 22, "1391923617": 14, "1396048722": 24, "1396114223": 1, "1397284432": 2, "1402876744": 25, "1402876745": 25, "1404854454": 27, "1405321765": 21, "1410465636": 23, "1410496312": 28, "1412999460": 18, "1414309946": 27, "1422712818": 1, "1423305584": 2, "1423305585": 2, "1423305586": 2, "1423305587": 2, "1423305588": 2, "1423305589": 2, "1423305590": 2, "1423305591": 2, "1423305598": 2, "1423305599": 2, "1425794805": 16, "1429874803": 19, "1430140002": 9, "1430140003": 9, "1430606515": 24, "1431132354": 28, "1431165322": 2, "1437388682": 28, "1440616900": 11, "1444655707": 1, "1448808637": 7, "1449701838": 1, "1450633717": 3, "1451091657": 4, "1452074541": 27, "1453761528": 26, "1454610995": 7, "1455043575": 28, "1456070807": 26, "1458722318": 23, "1459837414": 25, "1460042662": 28, "1460790368": 25, "1460790369": 25, "1460790370": 25, "1460790371": 25, "1460790372": 25, "1460790373": 25, "1465090512": 23, "1465090513": 23, "1465090515": 23, "1465283453": 27, "1467209292": 14, "1470356892": 27, "1471300080": 4, "1471723144": 1, "1473368760": 4, "1473368761": 4, "1473368764": 4, "1473368765": 4, "1473368766": 4, "1473368767": 4, "1473511322": 28, "1477100072": 24, "1479330385": 27, "1479532637": 1, "1482931023": 7, "1483496162": 25, "1483633423": 17, "1485222020": 8, "1489178152": 1, "1489178153": 1, "1489178154": 1, "1489178155": 1, "1489178156": 1, "1489178157": 1, "1495259740": 27, "1495259741": 27, "1495259742": 27, "1496718000": 20, "1496718001": 20, "1498917124": 6, "1499029664": 27, "1499029665": 27, "1499029673": 27, "1499029674": 27, "1499029675": 27, "1499029676": 27, "1499029677": 27, "1499029678": 27, "1499029679": 27, "1502135233": 5, "1502135240": 5, "1502135241": 5, "1502135242": 5, "1502135243": 5, "1502135244": 5, "1502135246": 5, "1502135247": 5, "1502692899": 7, "1502960190": 4, "1503713660": 1, "1504542608": 2, "1505278293": 1, "1506317964": 27, "1510405477": 7, "1510949672": 27, "1511214612": 4, "1511214613": 4, "1511744861": 23, "1511744862": 23, "1511744863": 23, "1512491423": 22, "1512570524": 1, "1514358116": 16, "1515861434": 6, "1517831918": 10, "1518774294": 3, "1518774295": 3, "1519758964": 28, "1519942860": 3, "1519942861": 3, "1520144521": 24, "1524010549": 27, "1526969819": 27, "1527687869": 16, "1529650477": 11, "1530138662": 27, "1530302985": 28, "1531478589": 4, "1532971903": 27, "1533012008": 1, "1533101114": 3, "1533101115": 3, "1533935492": 28, "1537581030": 6, "1537854340": 28, "1537854341": 28, "1539578239": 1, "1540031264": 3, "1540650548": 21, "1540875016": 14, "1545181237": 22, "1548056407": 7, "1551213059": 28, "1552044323": 3, "1553202889": 14, "1554459526": 26, "1554459527": 27, "1554631969": 16, "1554631970": 16, "1554631971": 16, "1555316216": 7, "1556831535": 6, "1559478790": 22, "1560678953": 3, "1560953183": 27, "1561249470": 6, "1561544959": 28, "1565013321": 27, "1567215868": 4, "1568774875": 27, "1569145193": 24, "1570246134": 28, "1576992137": 4, "1577190509": 20, "1579706082": 3, "1579706083": 3, "1581478491": 15, "1584837156": 2, "1584837157": 2, "1588254578": 3, "1589318419": 3, "1590407914": 24, "1590498252": 4, "1590498254": 4, "1590498255": 4, "1591188458": 15, "1592780622": 27, "1593707036": 22, "1595521942": 4, "1596190497": 28, "1596190498": 28, "1596190499": 28, "1597797637": 14, "1599443272": 16, "1599898966": 2, "1599949358": 3, "1600065451": 10, "1602334068": 2, "1605596084": 2, "1605599021": 28, "1608003536": 28, "1608119540": 2, "1609141880": 24, "1611948521": 26, "1611948522": 26, "1614326414": 10, "1614326415": 10, "1615259008": 28, "1616346845": 24, "1622764089": 27, "1623332175": 16, "1623653768": 2, "1623653769": 2, "1623653770": 2, "1623653771": 2, "1623693459": 11, "1625014740": 16, "1627792659": 11, "1630553967": 27, "1631206822": 11, "1631448645": 19, "1632954375": 3, "1633261020": 11, "1633854071": 4, "1640597844": 21, "1641172978": 27, "1645521766": 12, "1649663920": 16, "1649929380": 3, "1651275175": 7, "1652641560": 26, "1652641561": 26, "1652641563": 26, "1654077219": 20, "1656278203": 8, "1656333252": 27, "1658124146": 24, "1660923784": 28, "1660923785": 28, "1660923786": 28, "1660923787": 28, "1660923788": 28, "1660923789": 28, "1660923790": 28, "1660923791": 28, "1661191186": 7, "1661191187": 7, "1661191192": 7, "1661191193": 7, "1661191194": 7, "1661191195": 7, "1661191196": 7, "1661191197": 7, "1661191198": 7, "1661191199": 7, "1662439581": 1, "1665551138": 9, "1666450988": 24, "1667376042": 2, "1671745951": 28, "1671775166": 4, "1671775167": 4, "1672416975": 27, "1673857967": 23, "1674525506": 9, "1676252281": 23, "1676763078": 18, "1680130304": 28, "1680130305": 28, "1680130306": 28, "1680130307": 28, "1680130308": 28, "1680130309": 28, "1680130310": 28, "1680130311": 28, "1683482799": 7, "1684784588": 14, "1684784589": 14, "1685769250": 6, "1685769251": 6, "1686483719": 27, "1690059054": 22, "1690229508": 3, "1691540653": 14, "1692473496": 20, "1695429263": 5, "1699568676": 17, "1700810598": 1, "1702504372": 3, "1706764072": 2, "1706764073": 2, "1706874193": 6, "1708415886": 5, "1710813116": 24, "1711459689": 1, "1711735725": 27, "1714370698": 8, "1715842350": 8, "1715842351": 13, "1718586432": 27, "1719391718": 26, "1719602308": 28, "1720894545": 3, "1721185806": 4, "1721185807": 4, "1723894000": 1, "1723894001": 1, "1723894003": 1, "1724152704": 1, "1724152705": 1, "1724152706": 1, "1729424352": 18, "1731825007": 26, "1735710919": 26, "1736897074": 9, "1736897077": 9, "1736897080": 9, "1739076284": 19, "1739076285": 19, "1742690744": 28, "1743191871": 22, "1745945065": 24, "1748608556": 19, "1749693791": 28, "1750365155": 3, "1750913471": 25, "1751782730": 1, "1752648948": 24, "1756958880": 27, "1766769942": 27, "1767312242": 5, "1767664464": 20, "1767664465": 20, "1767664466": 20, "1767664467": 20, "1767664468": 20, "1767664469": 20, "1767664470": 20, "1767664471": 20, "1767664478": 20, "1767664479": 20, "1770862260": 14, "1770862261": 14, "1773000505": 18, "1773456751": 27, "1775707016": 7, "1776069409": 27, "1776174698": 18, "1776174699": 18, "1779961758": 16, "1781082581": 18, "1781585292": 25, "1781585293": 25, "1782320603": 2, "1782768110": 27, "1783144332": 28, "1784442048": 20, "1784442049": 20, "1784442056": 19, "1784442057": 19, "1784442058": 19, "1784442059": 19, "1784442060": 19, "1784442061": 19, "1784442062": 19, "1784442063": 19, "1785547370": 13, "1788892384": 4, "1788892385": 4, "1791076969": 28, "1792195381": 5, "1792801201": 27, "1792846777": 28, "1797416766": 27, "1799214498": 20, "1806024532": 27, "1806258196": 15, "1807652646": 27, "1808095282": 22, "1808095283": 22, "1808095284": 22, "1808095285": 22, "1808095286": 22, "1808095287": 22, "1809917548": 27, "1812385586": 2, "1812385587": 2, "1813474267": 27, "1816441351": 27, "1816495538": 2, "1818656374": 25, "1819413126": 3, "1820842676": 3, "1820842677": 3, "1822914432": 5, "1822927441": 20, "1824935794": 15, "1825213269": 12, "1831485463": 28, "1832133195": 27, "1833569876": 4, "1833569877": 4, "1833569879": 4, "1835787579": 3, "1836235748": 26, "1836636207": 4, "1837000826": 9, "1837000827": 9, "1840919274": 12, "1841270916": 25, "1841451177": 1, "1841451179": 1, "1841728090": 28, "1843912327": 22, "1844125034": 2, "1844904392": 18, "1844904393": 18, "1844904396": 18, "1844904397": 18, "1844904398": 18, "1844904399": 18, "1845372864": 25, "1845978721": 25, "1846358622": 27, "1847708611": 5, "1849669673": 27, "1849697741": 7, "1851649370": 15, "1855675471": 6, "1856262127": 17, "1858216083": 7, "1858618510": 18, "1860355337": 3, "1861087035": 24, "1862324869": 7, "1863732222": 25, "1865638436": 12, "1868330208": 17, "1868330209": 17, "1868330216": 16, "1868330217": 17, "1868330218": 17, "1868330219": 17, "1868330220": 17, "1868330221": 17, "1868330222": 17, "1868330223": 17, "1870273657": 10, "1871823355": 19, "1873273984": 2, "1873857625": 1, "1876000888": 25, "1876000889": 25, "1879022254": 27, "1884588413": 21, "1885107794": 16, "1885107795": 16, "1885107800": 16, "1885107801": 16, "1885107802": 16, "1885107803": 16, "1885107804": 16, "1885107805": 16, "1885107806": 16, "1885107807": 16, "1887823827": 8, "1890040155": 27, "1890164722": 1, "1891321753": 23, "1891916078": 28, "1892982956": 27, "1896146275": 24, "1897528210": 3, "1897536429": 3, "1900594973": 19, "1901885382": 16, "1901885383": 16, "1901885384": 16, "1901885385": 16, "1901885386": 16, "1901885387": 16, "1901885388": 16, "1901885389": 16, "1901885390": 16, "1901885391": 16, "1902452687": 3, "1903259976": 11, "1905328541": 21, "1907110519": 26, "1907211032": 28, "1907211033": 28, "1907211034": 28, "1907211035": 28, "1907211036": 28, "1907211037": 28, "1907211038": 28, "1907211039": 28, "1908170679": 26, "1909657913": 5, "1911078836": 23, "1913006743": 19, "1913500528": 9, "1914374939": 28, "1914761507": 12, "1914989540": 6, "1914989541": 6, "1916502201": 1, "1916876143": 9, "1917595114": 28, "1918663072": 15, "1918663073": 15, "1918663074": 16, "1918663075": 16, "1918663076": 16, "1918663077": 16, "1918663078": 16, "1918663079": 16, "1918663080": 16, "1918663081": 16, "1918710127": 27, "1922571986": 27, "1922808508": 16, "1923236933": 5, "1923817547": 8, "1926033630": 15, "1929891160": 23, "1930161380": 21, "1930161381": 21, "1931297242": 28, "1933944659": 1, "1935440656": 19, "1935440657": 19, "1935440658": 19, "1935440659": 19, "1935440660": 19, "1935440661": 19, "1935440662": 19, "1935440663": 19, "1935440668": 19, "1935440669": 19, "1936281116": 3, "1936516278": 2, "1938477372": 15, "1940590816": 1, "1940590817": 1, "1940590818": 1, "1940590819": 1, "1940590820": 1, "1940590821": 1, "1940590822": 1, "1940590823": 1, "1940590824": 1, "1940590825": 1, "1941493632": 28, "1945196704": 11, "1947358241": 22, "1950047920": 25, "1950431413": 9, "1950912972": 1, "1950948038": 25, "1951638138": 14, "1952218240": 19, "1952218241": 19, "1952218242": 18, "1952218243": 18, "1952218244": 18, "1952218245": 18, "1952218246": 18, "1952218247": 18, "1952218254": 19, "1952218255": 19, "1952523260": 27, "1952643886": 28, "1954221115": 1, "1956273477": 2, "1958368075": 25, "1958555234": 28, "1959648454": 1, "1961918267": 16, "1963513388": 23, "1963513389": 23, "1963513391": 23, "1968995954": 18, "1968995955": 18, "1968995960": 18, "1968995961": 18, "1968995962": 17, "1968995963": 16, "1968995964": 18, "1968995965": 18, "1968995966": 18, "1968995967": 18, "1971240964": 22, "1971240965": 22, "1971412320": 6, "1971919313": 22, "1972548224": 11, "1977129966": 27, "1981116089": 24, "1981661519": 12, "1982350172": 28, "1983275378": 15, "1983519831": 10, "1983519832": 10, "1983519834": 10, "1983519837": 10, "1983598535": 28, "1984190529": 2, "1985174646": 28, "1985773540": 17, "1985773541": 17, "1985773544": 17, "1985773545": 17, "1985773546": 17, "1985773547": 17, "1985773548": 17, "1985773549": 17, "1985773550": 17, "1985773551": 17, "1987263977": 5, "1987790789": 4, "1990971000": 27, "1991778561": 15, "1991946241": 13, "1997025201": 1, "1997760146": 19, "1997760147": 19, "1998779811": 27, "2000448433": 19, "2001563200": 10, "2005133865": 14, "2009106091": 5, "2010554576": 1, "2010554578": 1, "2010554579": 1, "2012077288": 23, "2012491044": 16, "2013981053": 21, "2014411539": 1, "2015081171": 22, "2016937064": 3, "2021434466": 14, "2021594588": 15, "2021594589": 15, "2021594590": 15, "2024085840": 11, "2024188850": 4, "2025044902": 27, "2025044903": 27, "2025044904": 27, "2025044905": 27, "2025044906": 27, "2025044907": 27, "2025044908": 27, "2025044909": 27, "2025044910": 27, "2025044911": 27, "2026109715": 19, "2026109719": 19, "2028247893": 27, "2029506313": 1, "2032131120": 3, "2035035510": 14, "2035035511": 14, "2036454102": 28, "2038017661": 1, "2039333456": 7, "2040680321": 14, "2041822464": 27, "2041822465": 27, "2041822466": 27, "2041822467": 27, "2041822468": 27, "2041822469": 27, "2041822470": 27, "2041822471": 27, "2041822474": 27, "2041822475": 27, "2046918315": 27, "2047844848": 3, "2047844849": 3, "2052186462": 27, "2053705146": 27, "2056606165": 22, "2058600181": 27, "2059442237": 12, "2062419607": 7, "2065742740": 19, "2069797992": 21, "2069797993": 21, "2069797995": 21, "2070759328": 28, "2071635914": 10, "2071635915": 10, "2073101876": 14, "2073576287": 5, "2074467606": 28, "2077249154": 14, "2077249155": 14, "2077748640": 27, "2077748641": 27, "2077748643": 27, "2077748644": 27, "2077748645": 27, "2077748646": 27, "2077748647": 27, "2077748650": 27, "2077748651": 27, "2078240126": 18, "2079349511": 24, "2079505484": 3, "2079505485": 3, "2081109262": 28, "2085006950": 27, "2085536058": 24, "2086902184": 28, "2086902185": 28, "2086902186": 28, "2086902187": 28, "2086902190": 28, "2087899461": 3, "2093762600": 1, "2094466368": 23, "2094466369": 23, "2094466371": 23, "2095897862": 22, "2097092608": 7, "2097732955": 28, "2098788836": 27, "2099710385": 17, "2100117443": 22, "2100631068": 28, "2105109359": 28, "2105396169": 25, "2105409832": 7, "2106367500": 23, "2106726848": 20, "2109156727": 27, "2109561326": 4, "2111039903": 25, "2111111693": 3, "2112508290": 24, "2112889975": 2, "2113350276": 23, "2113350277": 23, "2114877381": 3, "2116083674": 28, "2117628185": 23, "2118730408": 9, "2118730409": 9, "2120905920": 6, "2121121504": 27, "2121445668": 15, "2125798995": 27, "2127474099": 7, "2128916453": 15, "2132252935": 20, "2133500854": 4, "2133500855": 4, "2133500856": 4, "2133500857": 4, "2133500858": 4, "2133500859": 4, "2133500860": 4, "2133500861": 4, "2133500862": 4, "2133500863": 4, "2134318165": 2, "2135043770": 27, "2136073194": 27, "2138467220": 24, "2140049750": 16, "2145013895": 27, "2146494981": 1, "2149531002": 27, "2149696880": 22, "2149696881": 22, "2149696883": 22, "2150778206": 3, "2155928170": 3, "2156531500": 22, "2156817213": 7, "2158080418": 23, "2158247744": 26, "2159113266": 26, "2160851680": 28, "2160858284": 22, "2161024815": 2, "2161618499": 28, "2162261876": 3, "2162502468": 1, "2162656341": 27, "2162656342": 27, "2162656343": 27, "2163483610": 27, "2163922573": 3, "2168550540": 5, "2170265806": 27, "2171727442": 10, "2171727443": 10, "2172129765": 27, "2173237160": 25, "2174713383": 27, "2177429667": 14, "2177680704": 17, "2179603792": 6, "2179603793": 6, "2179603794": 6, "2179603795": 6, "2179603798": 6, "2179603799": 6, "2179902689": 15, "2181638943": 27, "2182316188": 11, "2182330181": 3, "2182765913": 26, "2184423372": 24, "2185878931": 24, "2188135103": 25, "2189358721": 22, "2189749430": 27, "2190633952": 18, "2191200811": 23, "2194193990": 17, "2195752360": 15, "2197832433": 16, "2200172911": 10, "2200729532": 16, "2200729533": 16, "2200729535": 16, "2201628119": 10, "2203814271": 14, "2213504923": 8, "2213649830": 10, "2213649831": 10, "2214129016": 14, "2214469173": 17, "2215619028": 20, "2217042834": 13, "2217640604": 27, "2217640605": 27, "2217640606": 27, "2217640607": 27, "2217751623": 24, "2219602427": 23, "2222286218": 13, "2223901117": 23, "2225838050": 1, "2230428468": 27, "2230659790": 27, "2231509992": 28, "2231509993": 28, "2231509994": 28, "2231509995": 28, "2231509996": 28, "2231509997": 28, "2231509998": 28, "2231509999": 28, "2234157635": 6, "2240097604": 4, "2240888816": 17, "2243475693": 27, "2243497631": 28, "2244422610": 27, "2245996336": 14, "2245996337": 14, "2251060291": 7, "2252742528": 1, "2255318456": 14, "2260280759": 10, "2260326783": 17, "2261046232": 15, "2262043425": 26, "2263885190": 5, "2265583879": 9, "2266083651": 24, "2266517764": 3, "2268509068": 26, "2268540161": 27, "2269749616": 28, "2272646880": 10, "2272646881": 10, "2272646882": 10, "2273494378": 22, "2274738792": 27, "2275638293": 28, "2276139500": 2, "2276139501": 2, "2276139502": 2, "2276139503": 2, "2277536120": 4, "2277536122": 4, "2277536123": 4, "2281501155": 26, "2285636663": 4, "2286474983": 22, "2286806200": 27, "2287277682": 3, "2287556848": 1, "2287797791": 28, "2291082292": 7, "2293905440": 17, "2294119165": 1, "2297249368": 15, "2297479091": 26, "2298896088": 3, "2298896090": 3, "2298896091": 3, "2298896092": 3, "2298896093": 3, "2298896094": 3, "2298896095": 3, "2299884162": 8, "2300270673": 10, "2302094943": 4, "2303499975": 2, "2307306630": 9, "2307426896": 1, "2307426898": 1, "2311027942": 1, "2311506225": 5, "2316331767": 28, "2319580354": 28, "2322759918": 27, "2323170449": 4, "2323339168": 24, "2324903362": 18, "2325217837": 7, "2325321839": 8, "2326578623": 24, "2328211300": 18, "2328435454": 6, "2328443760": 27, "2328497849": 1, "2330017983": 5, "2330483323": 26, "2331063860": 3, "2331063861": 3, "2335022422": 23, "2337196620": 18, "2337290000": 7, "2339497078": 22, "2341446709": 27, "2342711685": 9, "2346279889": 14, "2346836664": 25, "2350566579": 24, "2352138838": 8, "2352869761": 9, "2353359390": 16, "2355278664": 22, "2355278665": 22, "2359582409": 12, "2360840510": 14, "2367025562": 10, "2367456861": 27, "2376081322": 2, "2376234856": 10, "2381907801": 3, "2383793186": 16, "2385083333": 27, "2386208942": 28, "2390807586": 27, "2393591593": 23, "2394866220": 5, "2396888157": 3, "2397436643": 3, "2398232028": 28, "2398680997": 27, "2400712188": 16, "2402780870": 12, "2407661191": 14, "2408788216": 21, "2409711982": 8, "2410392568": 28, "2410392569": 28, "2410392570": 28, "2410392571": 28, "2410392572": 28, "2410392573": 28, "2410392574": 28, "2410392575": 28, "2410421545": 16, "2410637937": 27, "2412345582": 28, "2414564781": 5, "2417955889": 27, "2418053814": 1, "2418147569": 18, "2418399143": 28, "2419100474": 7, "2419113768": 4, "2419113769": 4, "2419910641": 7, "2420153990": 4, "2420153991": 4, "2420432880": 23, "2422643144": 6, "2423798800": 15, "2424824187": 18, "2426387438": 4, "2427328578": 3, "2428876598": 28, "2430770464": 25, "2433436300": 15, "2433900295": 2, "2437611296": 28, "2437968453": 1, "2446304953": 23, "2449203932": 7, "2449623004": 10, "2450544934": 16, "2451830981": 11, "2453351420": 16, "2453921511": 26, "2455898712": 27, "2459768632": 6, "2461358088": 1, "2462335932": 27, "2464438595": 25, "2470583197": 7, "2473252800": 4, "2473909427": 27, "2474741543": 5, "2474886440": 12, "2477028154": 6, "2477980485": 25, "2479769639": 11, "2483701864": 26, "2484637936": 1, "2484637938": 1, "2484637939": 1, "2484811305": 26, "2488007584": 27, "2489453969": 24, "2492590623": 14, "2492769187": 3, "2498433744": 27, "2498433745": 27, "2498433746": 27, "2498433747": 27, "2498433748": 27, "2498433749": 27, "2498433750": 27, "2498433751": 27, "2498433752": 27, "2498433753": 27, "2498589276": 25, "2501396275": 16, "2503665585": 16, "2505678740": 25, "2505678742": 25, "2505678743": 25, "2510169794": 14, "2510169795": 14, "2510169803": 14, "2510169805": 14, "2515211331": 27, "2517318050": 3, "2519247511": 23, "2523388612": 7, "2523856971": 27, "2525626415": 3, "2525874061": 28, "2526538979": 1, "2526736320": 8, "2526736321": 8, "2526736322": 8, "2526736323": 8, "2526736324": 8, "2526736325": 8, "2526736326": 8, "2526736327": 8, "2526736328": 8, "2526736329": 8, "2531130672": 23, "2531130673": 23, "2532993847": 13, "2533771627": 3, "2535002841": 22, "2537840254": 1, "2538439951": 2, "2538781119": 20, "2541494210": 1, "2542725780": 3, "2543806755": 1, "2543971899": 24, "2544513644": 3, "2545426109": 15, "2546370410": 7, "2548766624": 27, "2548766625": 27, "2548766632": 27, "2548766633": 27, "2548766634": 27, "2548766635": 27, "2548766636": 27, "2548766637": 27, "2548766638": 27, "2548766639": 27, "2549404869": 2, "2549951962": 18, "2550323932": 17, "2551510151": 9, "2552460292": 19, "2552460293": 19, "2552577806": 27, "2552954151": 7, "2554933025": 28, "2556098840": 5, "2557881346": 24, "2565108496": 27, "2565108499": 27, "2565108508": 27, "2565108509": 27, "2567383401": 22, "2569113415": 1, "2574262860": 4, "2574262861": 4, "2578820926": 7, "2579162237": 9, "2580357958": 11, "2580432862": 19, "2580533670": 16, "2581389132": 24, "2586806208": 22, "2586851812": 14, "2587085584": 28, "2588647360": 12, "2588647362": 12, "2589289009": 27, "2591111628": 14, "2593080269": 2, "2596801138": 9, "2601719270": 28, "2603319494": 22, "2603335652": 18, "2603400972": 16, "2607012497": 18, "2607476204": 2, "2607476205": 2, "2607476206": 2, "2607476207": 2, "2610515000": 27, "2611348487": 23, "2616697701": 24, "2617715132": 25, "2617715133": 25, "2618313500": 7, "2621732134": 28, "2623660327": 2, "2624974641": 13, "2629508583": 28, "2630114060": 28, "2631256485": 13, "2631466936": 27, "2632846356": 6, "2633916995": 12, "2635366068": 13, "2638689062": 10, "2640973641": 1, "2644193799": 27, "2645567209": 28, "2646096805": 27, "2649911453": 1, "2650448800": 28, "2650448801": 28, "2650481177": 11, "2651704748": 28, "2653114997": 8, "2655812783": 12, "2657527105": 27, "2657932152": 1, "2657932153": 1, "2657932157": 1, "2657932158": 1, "2660300386": 25, "2661272172": 2, "2661272173": 2, "2661999724": 28, "2662868359": 27, "2663827842": 8, "2664485240": 2, "2664485241": 2, "2664485242": 2, "2664485243": 2, "2665176422": 6, "2665176423": 6, "2667112711": 27, "2669452287": 22, "2671066062": 27, "2671075344": 13, "2671982863": 3, "2675394045": 22, "2676329044": 23, "2679694814": 17, "2680217524": 28, "2680217525": 28, "2680217526": 28, "2680217527": 28, "2680217529": 28, "2680976411": 23, "2683352144": 17, "2683352145": 17, "2685001662": 3, "2689748168": 28, "2689748169": 28, "2689748170": 28, "2689748171": 28, "2689748174": 28, "2692796380": 8, "2693103582": 21, "2694357688": 2, "2694905144": 27, "2696245301": 3, "2696974648": 12, "2696974649": 12, "2696974651": 12, "2699000684": 3, "2702372534": 5, "2703464692": 14, "2704911691": 27, "2709194827": 10, "2715580076": 28, "2715650945": 5, "2716265198": 28, "2716279146": 11, "2716279147": 11, "2716406907": 9, "2717158440": 2, "2717540192": 6, "2717540193": 6, "2717540196": 6, "2717540197": 6, "2717540198": 6, "2717540199": 6, "2720534902": 7, "2721907666": 19, "2721907667": 19, "2723301959": 11, "2728416796": 20, "2728416797": 20, "2728416798": 20, "2728851518": 23, "2730176990": 12, "2734291260": 28, "2737886288": 1, "2737886290": 1, "2739996165": 27, "2741695224": 1, "2743185250": 12, "2745609979": 27, "2749020594": 22, "2749628923": 24, "2749904087": 8, "2750411529": 21, "2750854395": 28, "2752157162": 17, "2752635126": 25, "2752635127": 25, "2753228730": 24, "2753784961": 27, "2756505818": 14, "2756505819": 14, "2757833801": 7, "2758992410": 26, "2760398988": 2, "2760449282": 16, "2760841051": 15, "2763422586": 23, "2763470379": 15, "2763985022": 13, "2764545753": 11, "2764644855": 2, "2764769717": 6, "2767412312": 19, "2768024953": 25, "2769834047": 2, "2770157746": 3, "2770607176": 23, "2770607177": 23, "2770607178": 23, "2770607179": 23, "2770988335": 28, "2771440667": 27, "2773494644": 28, "2774782768": 5, "2774782769": 5, "2777868042": 27, "2777913564": 2, "2777913565": 2, "2778157412": 27, "2779499874": 20, "2779841490": 9, "2779841491": 10, "2779841496": 1, "2779841497": 1, "2779841499": 1, "2779841500": 7, "2779841501": 8, "2779841502": 1, "2779841503": 6, "2783367502": 27, "2785855278": 21, "2788609407": 10, "2790542790": 23, "2790542791": 23, "2790542792": 23, "2790542793": 23, "2790542794": 23, "2790542795": 23, "2790542796": 23, "2790542797": 23, "2790542798": 23, "2790542799": 23, "2794014115": 6, "2795689398": 14, "2795689399": 14, "2795914918": 14, "2797445293": 10, "2798209984": 27, "2798209985": 27, "2799886702": 5, "2803131573": 28, "2803587647": 27, "2805101184": 7, "2805962096": 14, "2806946666": 28, "2808451697": 27, "2808791241": 27, "2809286531": 27, "2809967986": 14, "2809967987": 14, "2810182789": 8, "2811138603": 15, "2811521172": 18, "2811521173": 18, "2812100428": 19, "2814093983": 19, "2814383126": 3, "2816306775": 21, "2816485244": 3, "2818777309": 15, "2818971729": 24, "2822855549": 24, "2822855550": 24, "2822855551": 24, "2824302184": 3, "2824493179": 27, "2828252061": 2, "2829865752": 13, "2830258806": 24, "2833210534": 27, "2834933816": 3, "2835570795": 27, "2835717213": 14, "2836298415": 16, "2836936354": 28, "2837295684": 6, "2838582256": 18, "2838582257": 18, "2840606178": 22, "2840773569": 11, "2841844225": 27, "2842471112": 16, "2847079604": 12, "2847579028": 24, "2847579029": 24, "2847579035": 24, "2849050827": 16, "2850235156": 27, "2851897225": 21, "2852373306": 24, "2853747581": 19, "2854370169": 8, "2858342979": 5, "2859470126": 25, "2868525732": 6, "2868525736": 6, "2868525737": 6, "2868525739": 6, "2868525740": 6, "2868525741": 6, "2868525742": 6, "2868525743": 6, "2869466318": 18, "2870168892": 1, "2871095181": 1, "2871264750": 18, "2872740129": 28, "2873488357": 16, "2873900232": 22, "2873996295": 6, "2877046370": 7, "2879309661": 10, "2879738380": 28, "2881439013": 20, "2881559732": 27, "2883258880": 27, "2883258881": 27, "2883258882": 27, "2883258883": 27, "2883258885": 27, "2883258894": 27, "2884778147": 28, "2890977363": 3, "2891490654": 5, "2891979647": 4, "2892092373": 15, "2893288899": 28, "2893288900": 28, "2893288901": 28, "2893288902": 28, "2893288903": 28, "2894981916": 28, "2896897122": 26, "2897206615": 22, "2899925137": 8, "2901472731": 27, "2905834150": 28, "2908653246": 28, "2909593268": 15, "2909846572": 16, "2910312059": 22, "2912265353": 3, "2913001516": 28, "2915137853": 23, "2916149327": 22, "2916186848": 27, "2916219759": 1, "2916406440": 2, "2917303019": 28, "2919429251": 2, "2919599145": 27, "2919938481": 3, "2922275939": 10, "2922902263": 3, "2922964484": 28, "2923764557": 26, "2924095235": 3, "2924794318": 11, "2925817709": 3, "2926662833": 1, "2926662834": 1, "2926662836": 1, "2926662837": 1, "2926662839": 1, "2926662840": 1, "2928555612": 23, "2928555613": 23, "2928555615": 23, "2931483505": 1, "2932390016": 18, "2933249366": 24, "2933904296": 11, "2934509775": 14, "2936183184": 24, "2936930036": 27, "2937665788": 27, "2938523486": 1, "2938627541": 27, "2940416351": 7, "2944782064": 1, "2946312594": 20, "2949279114": 28, "2949414982": 1, "2949664689": 3, "2951975870": 23, "2952826467": 15, "2953575380": 27, "2954040640": 27, "2954040646": 27, "2954040647": 27, "2955427634": 27, "2957044930": 7, "2957044931": 7, "2957208150": 25, "2958378809": 1, "2961559651": 1, "2962058736": 12, "2962058737": 12, "2962058744": 12, "2962058745": 12, "2962058746": 12, "2962058747": 12, "2962058748": 12, "2962058749": 12, "2962058750": 12, "2962058751": 12, "2962546544": 25, "2962546545": 25, "2962546546": 25, "2962546547": 25, "2962546548": 25, "2962546549": 25, "2962546550": 25, "2962546551": 25, "2962546552": 25, "2962546553": 25, "2962622602": 25, "2963292596": 1, "2963870192": 12, "2963870193": 11, "2963870194": 14, "2963870195": 13, "2963870196": 16, "2963870197": 15, "2963870198": 18, "2963870199": 17, "2963870200": 20, "2963870201": 19, "2964031580": 17, "2964031581": 17, "2965080304": 27, "2965439266": 3, "2967332123": 11, "2971194471": 14, "2971341205": 28, "2974160011": 26, "2975041411": 12, "2976357066": 21, "2978337238": 1, "2978747767": 3, "2979324128": 26, "2979324129": 26, "2979324130": 26, "2979324131": 25, "2979324132": 26, "2979324133": 26, "2979324134": 26, "2979324135": 26, "2979324138": 26, "2979324139": 26, "2983096953": 28, "2984888631": 15, "2985926430": 25, "2985926431": 25, "2988472426": 14, "2988472427": 14, "2989132488": 3, "2990668488": 19, "2990668490": 19, "2990668491": 19, "2991116200": 13, "2991116201": 13, "2991116203": 13, "2991353901": 11, "2994721336": 3, "2998296658": 2, "3000974824": 14, "3002603177": 27, "3004379087": 14, "3005747976": 9, "3007479950": 23, "3010244697": 3, "3012249670": 5, "3012249671": 5, "3012879296": 25, "3012879297": 25, "3012879304": 24, "3012879305": 24, "3012879306": 24, "3012879307": 24, "3012879308": 24, "3012879309": 24, "3012879310": 25, "3012879311": 24, "3013363601": 4, "3015197581": 3, "3015877110": 27, "3017226694": 28, "3017453901": 27, "3023230941": 2, "3027193329": 5, "3029657016": 26, "3029657017": 26, "3029657020": 26, "3029657021": 26, "3029657022": 26, "3029657023": 26, "3031612900": 7, "3035129091": 6, "3037589442": 22, "3039687635": 7, "3040820634": 28, "3041136324": 1, "3044160466": 13, "3044482290": 25, "3052096176": 1, "3053891303": 22, "3054638345": 3, "3055452222": 8, "3057124503": 20, "3058007494": 19, "3060237106": 16, "3060668363": 28, "3063212160": 26, "3063212161": 26, "3063212162": 26, "3063212163": 26, "3063212164": 26, "3063212165": 26, "3063212166": 26, "3063212167": 26, "3063212170": 26, "3063212171": 25, "3065077658": 27, "3065077659": 27, "3065077661": 27, "3065473122": 27, "3065473123": 27, "3065473124": 27, "3065473125": 27, "3065586312": 23, "3070016616": 28, "3070632546": 28, "3070846922": 27, "3070846923": 27, "3074755706": 16, "3077367255": 3, "3078369200": 27, "3078369201": 27, "3078369202": 27, "3078369203": 27, "3078369205": 27, "3078567075": 27, "3079989872": 26, "3079989873": 26, "3079989874": 26, "3079989875": 26, "3079989876": 26, "3079989877": 26, "3079989878": 26, "3079989879": 26, "3079989884": 26, "3079989885": 26, "3081047495": 8, "3082233610": 15, "3083188238": 28, "3085024168": 26, "3085024169": 26, "3085024171": 26, "3085191056": 28, "3085191057": 28, "3085191058": 28, "3087166452": 18, "3087717202": 27, "3088135833": 16, "3090048282": 27, "3090681042": 25, "3098900136": 27, "3100848450": 24, "3101709156": 24, "3101709157": 24, "3101709158": 24, "3101968640": 28, "3101968641": 28, "3101968642": 28, "3101968643": 28, "3101968644": 28, "3101968645": 28, "3101968646": 28, "3101968647": 28, "3101968650": 28, "3101968651": 28, "3102003364": 23, "3103255595": 22, "3103842271": 27, "3104384024": 3, "3104486761": 16, "3104651352": 12, "3105935002": 1, "3105953706": 2, "3105953707": 2, "3107915043": 27, "3110827848": 8, "3110986887": 21, "3111568261": 28, "3113416514": 23, "3113954915": 27, "3115221584": 3, "3115221585": 3, "3116258568": 14, "3116258570": 14, "3116258571": 14, "3117902614": 27, "3118323620": 4, "3118323621": 4, "3118746352": 28, "3118746353": 28, "3118746354": 28, "3118746355": 28, "3118746356": 28, "3118746357": 28, "3118746358": 28, "3118746359": 28, "3118746366": 28, "3118746367": 28, "3120368538": 10, "3121760799": 27, "3122197216": 27, "3123639228": 6, "3128149872": 27, "3129418034": 1, "3130152719": 27, "3130164381": 23, "3134905452": 1, "3136100690": 7, "3136552544": 5, "3136552545": 5, "3140833552": 2, "3140833553": 2, "3140833554": 2, "3140833555": 2, "3140833556": 2, "3140833557": 2, "3140833558": 2, "3140833559": 2, "3143728275": 24, "3149352181": 27, "3152414792": 1, "3153681208": 18, "3154193408": 22, "3154194046": 23, "3154516913": 2, "3157963939": 14, "3159052337": 7, "3159160837": 24, "3159710857": 20, "3159969305": 28, "3160643158": 25, "3161524490": 2, "3163467796": 1, "3166802179": 5, "3168164098": 2, "3168693152": 28, "3168693153": 28, "3168693154": 28, "3168693155": 28, "3168693156": 28, "3168693157": 28, "3168693158": 28, "3168693159": 28, "3168934826": 24, "3168997075": 18, "3171649853": 2, "3175295830": 25, "3175859009": 26, "3177119978": 2, "3179652816": 26, "3179985807": 22, "3183180185": 3, "3184938442": 3, "3186457725": 15, "3187955025": 10, "3188413900": 9, "3192336962": 3, "3193721068": 16, "3196288028": 2, "3197331902": 15, "3199368173": 1, "3201503919": 27, "3201839676": 1, "3205869472": 1, "3205869473": 1, "3205869474": 1, "3205869475": 1, "3205869476": 1, "3205869477": 1, "3205869478": 1, "3205869479": 1, "3205869484": 1, "3205869485": 1, "3206190066": 23, "3206549834": 27, "3207224909": 27, "3214073080": 23, "3214073081": 23, "3215252549": 6, "3216694244": 27, "3216891360": 28, "3216891361": 28, "3216891362": 28, "3216891363": 28, "3216891364": 28, "3216891365": 28, "3216891366": 28, "3216891367": 28, "3216891370": 28, "3216891371": 28, "3217260771": 28, "3220992341": 28, "3224066584": 15, "3224071925": 28, "3225959819": 1, "3226562059": 28, "3226685059": 27, "3228576704": 24, "3228576705": 24, "3228576706": 24, "3228576707": 24, "3228576708": 24, "3228576709": 24, "3228576710": 24, "3228576711": 24, "3228576714": 24, "3228576715": 24, "3230467553": 14, "3230867320": 4, "3230867321": 4, "3230867322": 4, "3232831379": 24, "3233781546": 17, "3234030313": 19, "3234265291": 13, "3234459356": 13, "3234744410": 14, "3234744411": 14, "3235287656": 11, "3236510875": 3, "3240434620": 21, "3242972517": 14, "3244015567": 4, "3244569127": 3, "3249133238": 16, "3252358296": 6, "3252358297": 6, "3252358300": 6, "3252358301": 6, "3252358302": 6, "3252358303": 6, "3254175265": 16, "3254463492": 15, "3256326692": 25, "3256453690": 21, "3256698432": 14, "3257147585": 1, "3257562381": 3, "3257710283": 9, "3258345482": 1, "3260232501": 28, "3260482534": 2, "3260881938": 28, "3261189180": 27, "3264152240": 15, "3264951903": 28, "3267552999": 24, "3268422857": 27, "3268768924": 28, "3268768926": 28, "3268768927": 28, "3269282583": 12, "3272276569": 28, "3273280393": 21, "3278002119": 15, "3280087347": 14, "3283170684": 14, "3284443097": 27, "3287805174": 2, "3287805175": 2, "3287805176": 2, "3287805177": 2, "3287805178": 2, "3287805179": 2, "3287805180": 2, "3287805181": 2, "3287805183": 2, "3289085830": 14, "3291545503": 12, "3292091251": 23, "3293207827": 27, "3293251509": 15, "3295115440": 28, "3297537117": 24, "3297559996": 28, "3299468272": 22, "3299468273": 22, "3299468275": 22, "3299562545": 28, "3301342408": 12, "3301342410": 12, "3301342411": 12, "3301429924": 5, "3305748852": 28, "3306267716": 2, "3306754793": 24, "3309556272": 2, "3310526732": 27, "3314030149": 28, "3314737869": 11, "3315596891": 28, "3315867430": 28, "3317961966": 27, "3320612381": 28, "3320668100": 28, "3321444752": 12, "3321444753": 12, "3321444754": 12, "3324845151": 22, "3325463374": 4, "3326173065": 14, "3326837142": 11, "3326837143": 11, "3328019216": 27, "3328839466": 5, "3328839467": 5, "3329514528": 24, "3332509033": 19, "3333986015": 6, "3334815691": 2, "3338329088": 28, "3338329089": 28, "3338329096": 28, "3338329097": 28, "3338329098": 28, "3338329099": 28, "3338329100": 28, "3338329101": 28, "3338329102": 28, "3338329103": 28, "3340962430": 28, "3342375124": 1, "3344147835": 27, "3344147836": 27, "3344147837": 27, "3344147838": 27, "3344147839": 27, "3344467069": 17, "3344732822": 3, "3344861342": 9, "3345201682": 25, "3345579132": 27, "3345862920": 3, "3345862921": 3, "3346875528": 23, "3347303957": 27, "3349393475": 27, "3349439959": 1, "3349747415": 26, "3351378128": 1, "3351378129": 1, "3351378130": 1, "3351378131": 1, "3351378132": 1, "3351378133": 1, "3351378134": 1, "3351378135": 1, "3351378142": 1, "3351378143": 1, "3352019292": 4, "3352566658": 2, "3355614447": 24, "3356720704": 27, "3357295428": 4, "3358687360": 16, "3360014173": 9, "3361651341": 28, "3362582864": 19, "3365248655": 10, "3367423699": 27, "3367964921": 3, "3368317463": 5, "3369249323": 20, "3373303016": 1, "3373303017": 1, "3373357626": 2, "3374656737": 28, "3377448506": 27, "3380377210": 14, "3382391785": 1, "3383426858": 27, "3386596901": 21, "3386658625": 19, "3386680334": 3, "3387050814": 23, "3387401614": 18, "3388529809": 27, "3388661953": 28, "3388913371": 27, "3388974843": 17, "3389066870": 3, "3389066871": 3, "3389746221": 3, "3393212812": 10, "3393212813": 10, "3394362619": 12, "3395525649": 23, "3396133977": 28, "3397132454": 15, "3397132455": 15, "3397709326": 21, "3399976694": 12, "3400256755": 15, "3400316022": 1, "3401335479": 12, "3401752901": 28, "3403116792": 1, "3403116793": 1, "3403116794": 1, "3403116795": 1, "3403116796": 1, "3403116797": 1, "3403330594": 27, "3403347110": 9, "3403347111": 9, "3403933998": 28, "3405767136": 28, "3408716127": 16, "3420271354": 27, "3422298367": 28, "3424624961": 28, "3426701406": 28, "3431536253": 27, "3431752854": 25, "3432146796": 23, "3433719984": 28, "3434490958": 17, "3438695065": 28, "3438799702": 11, "3441379841": 28, "3446591075": 27, "3447207572": 28, "3447522745": 28, "3450065536": 1, "3450065540": 1, "3450065541": 1, "3450065543": 1, "3450902429": 26, "3451625162": 2, "3452825266": 9, "3455566107": 2, "3457574564": 28, "3457574565": 28, "3457574566": 28, "3457574567": 28, "3459379565": 13, "3462542128": 27, "3462542129": 27, "3462542130": 27, "3462542131": 27, "3462542132": 27, "3462542134": 27, "3462542135": 27, "3462542140": 27, "3462542141": 27, "3462749963": 28, "3469344438": 21, "3469344439": 21, "3470237739": 23, "3470431947": 20, "3471922734": 16, "3472194867": 28, "3474363664": 14, "3474583133": 25, "3475074928": 3, "3475278204": 28, "3478033487": 27, "3481131423": 24, "3481749002": 28, "3481861797": 1, "3483485727": 25, "3487471557": 19, "3487922223": 1, "3490417196": 27, "3491209107": 17, "3493596115": 21, "3493861052": 20, "3493861054": 20, "3493861055": 20, "3494199208": 2, "3498489831": 27, "3499472512": 15, "3504150651": 28, "3505282728": 28, "3507133960": 15, "3507818312": 3, "3508236467": 8, "3508476915": 25, "3508476920": 25, "3508476926": 25, "3509010033": 14, "3510138048": 5, "3510413604": 23, "3510701861": 27, "3511698030": 5, "3514694513": 28, "3515978520": 23, "3524020798": 15, "3526028978": 1, "3526595120": 22, "3527716502": 23, "3531985793": 8, "3534987060": 28, "3536420626": 2, "3537584122": 28, "3537745894": 8, "3538363663": 14, "3539322898": 5, "3541326820": 1, "3541326821": 1, "3541326823": 1, "3544910195": 6, "3546241902": 1, "3546470356": 3, "3547113102": 15, "3547113103": 15, "3547298846": 3, "3547298847": 3, "3550772897": 28, "3551103093": 28, "3553394349": 27, "3554342685": 28, "3554800389": 27, "3557577228": 27, "3558681245": 25, "3559361670": 20, "3560184043": 8, "3561149313": 27, "3562697442": 7, "3563098445": 27, "3564705134": 21, "3564890687": 27, "3568351785": 12, "3569791559": 2, "3570466759": 28, "3573256294": 27, "3573256307": 27, "3573686365": 19, "3573734088": 21, "3574066955": 11, "3574120120": 12, "3574168117": 27, "3579036040": 28, "3579036044": 28, "3579036045": 28, "3579036046": 28, "3579036047": 28, "3580570631": 1, "3581751827": 16, "3583146813": 27, "3584938332": 16, "3585698380": 27, "3586026952": 22, "3586096795": 28, "3587167050": 2, "3587167051": 2, "3587987404": 15, "3589089942": 14, "3589159886": 28, "3591083053": 25, "3591179300": 27, "3592198503": 14, "3595878960": 23, "3596008431": 21, "3598944128": 26, "3598944129": 26, "3598944130": 26, "3598944131": 26, "3598944132": 26, "3599111900": 14, "3599131206": 26, "3599356244": 22, "3603558141": 19, "3603807747": 27, "3605230072": 1, "3605230073": 1, "3605230074": 2, "3605230075": 1, "3605490912": 8, "3605490913": 8, "3605490914": 8, "3605490915": 8, "3605490916": 8, "3605490917": 8, "3605490918": 8, "3605490919": 8, "3605490922": 8, "3605490923": 8, "3606698736": 27, "3606698737": 27, "3606698739": 27, "3607796223": 27, "3607928024": 13, "3611487543": 3, "3612254098": 14, "3613286014": 12, "3613955012": 23, "3614593590": 5, "3615976865": 9, "3617209212": 19, "3620108268": 28, "3620946736": 17, "3622268496": 8, "3622268497": 8, "3622268498": 8, "3622268499": 8, "3622268500": 8, "3622268501": 8, "3622268502": 8, "3622268503": 8, "3622268510": 8, "3622268511": 8, "3622281554": 27, "3624435060": 23, "3626242164": 17, "3626242165": 17, "3626242166": 17, "3626658637": 15, "3631942882": 19, "3632593563": 16, "3632912674": 20, "3633178755": 17, "3633213242": 21, "3635991036": 1, "3636181723": 1, "3636889164": 14, "3639046080": 8, "3639046081": 8, "3639046088": 8, "3639046089": 8, "3639046090": 8, "3639046091": 8, "3639046092": 8, "3639046093": 8, "3639046094": 8, "3639046095": 8, "3640619409": 28, "3640658628": 3, "3643047597": 27, "3644991365": 20, "3645474285": 25, "3645474286": 25, "3645474287": 25, "3648282895": 28, "3649985571": 25, "3651793541": 13, "3654020043": 27, "3655823796": 8, "3655823797": 8, "3655823800": 8, "3655823801": 8, "3655823802": 8, "3655823803": 8, "3655823804": 8, "3655823805": 8, "3655823806": 8, "3655823807": 8, "3656394176": 11, "3656766039": 12, "3664536990": 14, "3665398231": 20, "3665594271": 4, "3672748946": 3, "3674581496": 22, "3674851283": 15, "3675783772": 28, "3677746972": 8, "3677746973": 8, "3677746975": 8, "3681082702": 27, "3684684730": 19, "3685829362": 28, "3685996623": 3, "3686736741": 22, "3687819002": 28, "3689280567": 28, "3690574071": 13, "3692806198": 7, "3696163951": 22, "3703045304": 27, "3708661256": 26, "3717471208": 3, "3717471209": 3, "3719164773": 2, "3720500948": 28, "3721195043": 11, "3725942064": 24, "3727205096": 8, "3727346032": 15, "3727346033": 15, "3727631160": 28, "3729709035": 2, "3730248014": 8, "3733212952": 28, "3734501342": 2, "3734826478": 27, "3735037521": 7, "3735294176": 15, "3735294177": 15, "3735294178": 15, "3735294179": 15, "3735294180": 15, "3735294181": 15, "3735294182": 15, "3735294183": 15, "3735294184": 15, "3735294185": 15, "3736041099": 3, "3739279256": 28, "3739279257": 28, "3739279258": 28, "3739279259": 28, "3739279260": 28, "3739279261": 28, "3739279262": 28, "3739279263": 28, "3744631007": 22, "3746956458": 19, "3748622249": 3, "3750865260": 28, "3750893516": 19, "3752071760": 15, "3752071761": 15, "3752071762": 15, "3752071763": 15, "3752071764": 15, "3752071765": 15, "3752071766": 15, "3752071767": 15, "3752071770": 15, "3752517619": 19, "3752630563": 26, "3754910498": 2, "3755201983": 8, "3755408274": 27, "3756389242": 1, "3756805800": 6, "3758415809": 18, "3760835322": 28, "3760835323": 28, "3760835324": 28, "3760835325": 28, "3763392098": 1, "3766145631": 27, "3767115045": 28, "3769043228": 22, "3770475778": 3, "3770475779": 3, "3771412616": 28, "3772918371": 7, "3772973256": 27, "3777269964": 27, "3777269965": 27, "3778032326": 28, "3782433407": 27, "3782991997": 25, "3782991998": 25, "3782991999": 25, "3785035696": 1, "3785035697": 1, "3785035698": 1, "3785437156": 14, "3785442599": 20, "3786062476": 27, "3790423802": 14, "3792590697": 5, "3793036417": 25, "3793612644": 27, "3794918801": 28, "3799750641": 15, "3800267412": 2, "3800267413": 2, "3800267414": 2, "3800267415": 2, "3802876271": 2, "3803329707": 16, "3804466364": 28, "3807544519": 7, "3808901541": 6, "3809278479": 23, "3809722908": 2, "3809902215": 24, "3811760832": 1, "3813974329": 28, "3814697630": 24, "3814697631": 24, "3817338783": 27, "3820128352": 23, "3820835868": 3, "3823317559": 27, "3825769808": 1, "3829285960": 4, "3830061117": 21, "3830186047": 1, "3831031876": 16, "3834187337": 27, "3835954362": 3, "3840083172": 3, "3844528610": 22, "3844703409": 7, "3845833465": 23, "3845888162": 27, "3845973845": 10, "3846727856": 11, "3846969568": 28, "3846969569": 28, "3846969570": 28, "3848868583": 4, "3850287637": 27, "3850655136": 2, "3854296178": 27, "3857100891": 9, "3859483818": 3, "3859483819": 3, "3859783010": 8, "3860970608": 28, "3860970609": 28, "3860970610": 28, "3860970611": 28, "3860970612": 28, "3860970613": 28, "3860970614": 28, "3860970615": 28, "3863829688": 27, "3864896927": 17, "3866715933": 2, "3867293152": 28, "3867293153": 28, "3867293160": 28, "3867293161": 28, "3867293162": 28, "3867293163": 28, "3867293164": 28, "3867293165": 28, "3867293166": 28, "3867293167": 28, "3871226707": 22, "3871252980": 4, "3872116425": 1, "3872337571": 18, "3874217662": 2, "3874217663": 2, "3874641219": 28, "3875413893": 1, "3876796314": 5, "3884067141": 11, "3886292674": 12, "3886292675": 12, "3886292680": 12, "3886292681": 12, "3886292682": 12, "3886292683": 12, "3886292684": 12, "3886292685": 12, "3886292686": 12, "3886292687": 12, "3887892656": 1, "3890581141": 28, "3892841518": 7, "3893112950": 24, "3896528758": 27, "3898143407": 3, "3899548068": 1, "3899944893": 4, "3900456246": 25, "3903070390": 12, "3903070391": 12, "3903070392": 12, "3903070393": 12, "3903070394": 12, "3903070395": 12, "3903070396": 12, "3903070397": 12, "3903070398": 12, "3903070399": 12, "3903548288": 1, "3905724875": 28, "3908760309": 3, "3909352274": 2, "3910216754": 23, "3911008533": 14, "3911560897": 23, "3915764593": 16, "3915764594": 16, "3915764595": 16, "3916608434": 3, "3917101356": 27, "3919847952": 13, "3919847953": 13, "3919847954": 13, "3919847955": 13, "3919847956": 13, "3919847957": 13, "3919847958": 13, "3919847959": 13, "3919847960": 13, "3919847961": 13, "3921006352": 1, "3921006353": 1, "3921006354": 1, "3921006355": 1, "3921006356": 1, "3921485172": 23, "3921554260": 28, "3922069396": 1, "3924400899": 18, "3925270067": 4, "3927413694": 28, "3929403535": 3, "3932439362": 5, "3932814032": 7, "3932962120": 26, "3936625536": 13, "3936625537": 13, "3936625538": 13, "3936625539": 13, "3936625540": 13, "3936625541": 13, "3936625542": 13, "3936625543": 13, "3936625548": 14, "3936625549": 14, "3938069942": 17, "3941026874": 20, "3941205951": 17, "3941269100": 25, "3941766715": 9, "3943394479": 3, "3946295144": 1, "3946321634": 23, "3946669007": 27, "3946927545": 6, "3947596543": 2, "3953403248": 14, "3953403249": 14, "3953403250": 14, "3953403251": 14, "3953403252": 14, "3953403253": 14, "3953403254": 14, "3953403255": 14, "3953403262": 14, "3953403263": 14, "3953986336": 3, "3955736323": 23, "3956230125": 2, "3958460049": 1, "3960522253": 4, "3961503936": 4, "3961503937": 4, "3961503938": 4, "3961503939": 4, "3961503940": 4, "3961503941": 4, "3961503942": 4, "3961503943": 4, "3961503948": 4, "3961503949": 4, "3965417933": 6, "3968560442": 7, "3969043896": 3, "3969654637": 27, "3970040886": 24, "3970180834": 14, "3970180835": 14, "3970180840": 14, "3970180841": 14, "3970180842": 14, "3970180843": 14, "3970180844": 14, "3970180845": 14, "3970180846": 14, "3970180847": 14, "3970398144": 3, "3970398145": 3, "3974075603": 26, "3977496457": 25, "3977654524": 27, "3978281650": 4, "3978281651": 4, "3978281652": 4, "3978281653": 4, "3978281654": 4, "3978281655": 4, "3980154334": 28, "3980259370": 8, "3980259371": 8, "3980449645": 28, "3981634627": 9, "3983534173": 21, "3983534174": 21, "3983534175": 21, "3986958420": 15, "3986958421": 15, "3986958424": 15, "3986958425": 15, "3986958426": 15, "3986958427": 15, "3986958428": 15, "3986958429": 15, "3986958430": 15, "3986958431": 15, "3987309016": 22, "3987442049": 7, "3988299563": 25, "3990400405": 5, "3992231371": 26, "3992231372": 26, "3992231375": 26, "4006192308": 1, "4006192310": 1, "4006192311": 1, "4011270348": 27, "4011836820": 4, "4011836821": 4, "4011836824": 4, "4011836825": 4, "4011836826": 4, "4011836827": 4, "4011836828": 4, "4011836829": 4, "4011836830": 4, "4011836831": 4, "4012203003": 8, "4012977684": 14, "4012977685": 14, "4018523064": 23, "4019651319": 27, "4020349587": 27, "4022107312": 27, "4022107313": 27, "4022107314": 27, "4022107315": 27, "4022107317": 27, "4022107318": 27, "4022107319": 27, "4022107324": 27, "4022107325": 27, "4024179293": 28, "4025934129": 20, "4026309323": 28, "4026414261": 27, "4026515849": 5, "4029022121": 16, "4029346515": 16, "4029814949": 11, "4030660414": 27, "4031340274": 5, "4031340275": 5, "4031412222": 28, "4032296272": 24, "4034060602": 15, "4035690282": 5, "4036178150": 2, "4036779270": 16, "4039169178": 20, "4040221326": 24, "4041218086": 27, "4043176609": 28, "4043342755": 20, "4045313562": 24, "4045752130": 25, "4048186065": 3, "4048242887": 20, "4048947954": 26, "4049365947": 1, "4049753919": 23, "4050450481": 7, "4052354655": 28, "4052831236": 3, "4054029966": 23, "4054596410": 21, "4059030097": 2, "4059318875": 2, "4063792252": 28, "4068780317": 28, "4072587059": 1, "4074920327": 28, "4074998859": 27, "4075522049": 6, "4076827687": 26, "4078226397": 8, "4082425930": 24, "4085986809": 4, "4086164721": 19, "4087530286": 8, "4087530287": 8, "4088731040": 27, "4088823605": 27, "4088827200": 12, "4088827201": 12, "4088827202": 12, "4089147221": 8, "4089840981": 24, "4089840982": 24, "4089840983": 24, "4089988225": 3, "4090391256": 12, "4090775747": 11, "4090950690": 28, "4092937102": 14, "4093148081": 18, "4095784502": 23, "4095816113": 27, "4096637925": 7, "4097227155": 2, "4097505799": 20, "4099900662": 22, "4099900663": 22, "4099900664": 22, "4099900665": 22, "4099900666": 22, "4099900667": 22, "4099900668": 22, "4099900669": 22, "4099900670": 22, "4099900671": 22, "4100029812": 7, "4100957909": 28, "4101386442": 1, "4105858344": 8, "4106757302": 24, "4107663664": 13, "4110556120": 2, "4113704648": 27, "4114677048": 7, "4114681644": 28, "4114707355": 2, "4116389173": 27, "4116537170": 11, "4116678224": 22, "4116678225": 22, "4116837065": 24, "4119095630": 3, "4119622018": 21, "4119627352": 27, "4123860739": 25, "4128095381": 6, "4128297107": 7, "4129164627": 14, "4132147344": 1, "4132147345": 1, "4132147346": 1, "4132147347": 1, "4132147348": 1, "4132147349": 1, "4132147350": 1, "4132147351": 1, "4132147352": 1, "4132147353": 1, "4133455808": 22, "4133455809": 22, "4133455810": 22, "4133455811": 22, "4133455812": 22, "4133455813": 22, "4133455814": 22, "4133455815": 22, "4133455820": 22, "4133455821": 22, "4136254009": 23, "4138697980": 21, "4139659913": 27, "4140529506": 21, "4140869021": 16, "4142792564": 3, "4143534670": 3, "4144402969": 28, "4145541360": 25, "4150228564": 28, "4150233520": 22, "4150233521": 22, "4150233522": 22, "4150233523": 22, "4150233524": 22, "4150233525": 22, "4150233526": 22, "4150233527": 22, "4150233534": 22, "4150233535": 22, "4151287333": 24, "4153787689": 11, "4154821248": 3, "4159550315": 7, "4159830752": 23, "4159935832": 4, "4160988416": 24, "4163975820": 24, "4164299630": 27, "4164883102": 27, "4167011106": 20, "4167011107": 20, "4167011112": 20, "4167011113": 20, "4167011114": 20, "4167011115": 20, "4167011116": 20, "4167011117": 20, "4167011118": 20, "4167011119": 20, "4169225313": 24, "4173467416": 7, "4173467417": 7, "4174632306": 27, "4178714181": 27, "4178714184": 27, "4178714185": 27, "4178714186": 27, "4178714187": 27, "4179794597": 17, "4180444323": 27, "4182403848": 26, "4182480224": 1, "4182480225": 1, "4182480232": 1, "4182480233": 1, "4182480234": 1, "4182480235": 1, "4182480236": 1, "4182480237": 1, "4182480239": 1, "4183635599": 20, "4183788692": 22, "4183788693": 20, "4183788696": 20, "4183788697": 20, "4183788698": 20, "4183788699": 20, "4183788700": 20, "4183788701": 20, "4183788702": 20, "4183788703": 20, "4187524534": 1, "4192551114": 28, "4196967425": 27, "4199529752": 27, "4201060647": 25, "4201697932": 3, "4201697933": 3, "4202615266": 19, "4204413574": 20, "4204482447": 23, "4210246646": 27, "4210715468": 27, "4211475741": 2, "4212410112": 1, "4213271810": 3, "4213271811": 3, "4213290893": 28, "4216207299": 27, "4220529694": 23, "4221096044": 12, "4221563250": 22, "4222561358": 19, "4223882778": 1, "4224283322": 27, "4224972854": 4, "4224972855": 4, "4227065942": 27, "4229237079": 8, "4231293639": 11, "4231469209": 28, "4236468733": 3, "4240221838": 16, "4242592192": 1, "4242592193": 1, "4242592195": 1, "4245469491": 3, "4248210736": 1, "4252684909": 11, "4253857367": 21, "4256068881": 22, "4256854144": 20, "4257676106": 11, "4258704672": 1, "4258704673": 1, "4258704674": 1, "4258704675": 1, "4258704676": 1, "4260972669": 27, "4260972670": 27, "4260972671": 27, "4261480748": 2, "4261480749": 2, "4261618303": 27, "4264364347": 15, "4264494593": 11, "4266295281": 23, "4266438808": 26, "4268478333": 27, "4269600130": 21, "4271401910": 28, "4272367383": 3, "4274829953": 28, "4274829954": 28, "4275472608": 20, "4275472612": 20, "4275472613": 20, "4275472614": 20, "4275472615": 20, "4276035208": 5, "4282413899": 27, "4282591831": 24, "4283023978": 4, "4283023979": 4, "4283023980": 4, "4283023981": 4, "4283023982": 4, "4283023983": 4, "4285212385": 17, "4287182959": 25, "4291079811": 27 } ================================================ FILE: src/data/d2/seasons_backup.json ================================================ { "25524335": 23, "37174328": 28, "40494562": 25, "49468316": 27, "52039562": 14, "55393445": 26, "56435118": 24, "56790432": 27, "58945355": 17, "61740554": 22, "61740555": 22, "62194641": 5, "63024229": 2, "72775246": 27, "76739872": 28, "76764720": 9, "76764721": 9, "76764722": 9, "79931464": 28, "105911828": 17, "109666087": 3, "116784191": 7, "116838027": 27, "117772190": 26, "123124985": 4, "126615444": 28, "128330886": 4, "130305507": 14, "132648577": 7, "134085740": 16, "134085741": 16, "134085743": 16, "139281386": 14, "139281387": 14, "140297752": 19, "140297755": 19, "140842223": 7, "141232678": 27, "143229982": 28, "143299650": 7, "147902881": 26, "153144587": 7, "156518114": 6, "156845152": 13, "156845153": 13, "158296659": 27, "158426788": 27, "159093681": 23, "166080281": 24, "166080282": 24, "166080283": 24, "167651268": 2, "171635528": 21, "171635529": 21, "171635530": 21, "176118904": 4, "176118905": 4, "177463495": 22, "177568179": 22, "179654396": 24, "187034864": 28, "195422190": 10, "199733460": 15, "204749352": 23, "211863483": 23, "213708705": 27, "215596672": 2, "217626588": 11, "217626590": 11, "217626591": 11, "221164696": 2, "221164697": 2, "222565136": 3, "223308216": 1, "223597399": 16, "226436555": 7, "231432261": 3, "233896077": 21, "234970842": 3, "239189018": 8, "242828821": 11, "244316917": 28, "245110123": 3, "246568432": 3, "249784434": 12, "250513201": 3, "257592544": 28, "257592545": 28, "257592552": 28, "257592553": 28, "257592554": 28, "257592555": 28, "257592556": 28, "257592557": 28, "257592558": 28, "257592559": 28, "259522459": 22, "260425472": 21, "261886690": 11, "261886691": 11, "263371512": 2, "263371513": 2, "263371514": 2, "263371515": 2, "263371516": 2, "263371517": 2, "263371518": 2, "263371519": 2, "268592485": 7, "269339124": 2, "270610849": 28, "270671209": 2, "275458597": 19, "280116880": 7, "281718534": 6, "281718535": 6, "285537093": 3, "286098604": 6, "286098606": 6, "286098607": 6, "287923131": 11, "293615869": 6, "294129361": 16, "309633584": 22, "309731943": 28, "316370328": 5, "316370329": 5, "316370330": 5, "316370331": 5, "316370334": 5, "316740353": 2, "317465074": 2, "317465075": 2, "320750826": 11, "320750827": 11, "327680457": 24, "334393553": 27, "335763433": 3, "340680574": 27, "343482208": 14, "343482209": 14, "343789488": 24, "343789489": 24, "346065606": 3, "346066663": 22, "346878928": 1, "346878930": 1, "350054538": 24, "362494424": 28, "366019830": 20, "373307731": 27, "373634602": 11, "381563628": 7, "386610725": 16, "389583837": 28, "391889347": 7, "394562660": 22, "396910433": 19, "401345492": 3, "407316269": 26, "407703832": 27, "412048979": 23, "412682987": 27, "413901114": 22, "418753772": 15, "418753773": 15, "419435523": 6, "425681240": 25, "426368686": 11, "428845810": 7, "437190449": 28, "437421244": 5, "438524081": 6, "450844637": 7, "464814870": 24, "466236950": 9, "466236951": 9, "467667358": 7, "468456840": 14, "468456841": 14, "472776702": 12, "476513004": 21, "477417894": 7, "478696310": 7, "492834021": 6, "495940989": 19, "503145555": 24, "506042498": 28, "510027376": 7, "510898033": 21, "510898034": 21, "510898035": 21, "511117849": 15, "512463852": 3, "516735972": 17, "518566750": 2, "518930465": 3, "528834068": 8, "530399817": 27, "531005896": 3, "533446628": 22, "536106547": 7, "537036424": 7, "537036425": 7, "537036427": 7, "537041732": 20, "539189884": 7, "540653483": 3, "540791504": 26, "540791505": 26, "545021994": 3, "550583155": 3, "558697641": 11, "564525440": 27, "566732136": 4, "568956803": 27, "574167778": 7, "574790717": 7, "581782322": 28, "586671776": 24, "591672323": 27, "599777086": 28, "601948197": 25, "613984400": 22, "617617371": 16, "622966241": 3, "627596132": 7, "628413376": 24, "637391773": 20, "638752234": 7, "640136282": 11, "647507422": 3, "650857753": 28, "655712834": 23, "657927352": 23, "671247011": 5, "677939288": 3, "678051457": 6, "679887739": 5, "682455836": 20, "682455838": 20, "682455839": 20, "682682138": 3, "689294985": 25, "692234472": 7, "693901806": 26, "695547243": 28, "700322568": 1, "700322570": 1, "703202729": 27, "704309987": 19, "707997638": 26, "709082555": 28, "721146704": 3, "722967004": 28, "724152384": 7, "728878482": 7, "749733608": 11, "751418951": 28, "754050190": 14, "754513817": 26, "757630934": 3, "758932444": 27, "761663477": 28, "764833385": 28, "765122182": 19, "766122634": 28, "773659236": 15, "773659238": 15, "773659239": 15, "778247590": 7, "779082129": 28, "779962716": 2, "780038942": 3, "784499738": 7, "785657282": 7, "796635575": 3, "798985558": 25, "803297678": 28, "807693916": 24, "808331801": 7, "814520772": 23, "817808889": 7, "821031610": 27, "826128643": 19, "829700536": 7, "830497630": 3, "830789013": 26, "855351524": 5, "855351525": 5, "855792702": 7, "856633363": 11, "859683485": 7, "861860247": 27, "862152988": 6, "871819007": 28, "873850027": 3, "876119751": 11, "889413643": 2, "891621634": 7, "896032513": 27, "901903811": 28, "902219633": 28, "906505391": 22, "912276057": 12, "921357268": 2, "929148730": 7, "935879349": 7, "937343085": 28, "948304981": 27, "967650555": 3, "967781090": 10, "968669760": 25, "968669761": 25, "968669762": 25, "970568088": 7, "980898608": 3, "980898609": 3, "980898610": 3, "980898611": 3, "980898614": 3, "980898615": 3, "981450701": 28, "985750811": 22, "1002555530": 22, "1005594230": 8, "1005594231": 8, "1013401891": 20, "1021217306": 3, "1025347717": 27, "1028757552": 5, "1028757553": 5, "1028757554": 5, "1028757555": 5, "1028757556": 5, "1028757557": 5, "1028757558": 5, "1028757559": 5, "1028757567": 5, "1032136201": 27, "1035129135": 27, "1042420727": 27, "1043609040": 17, "1044615214": 14, "1044615215": 14, "1045633725": 1, "1045633727": 1, "1047542010": 28, "1047542011": 28, "1049963080": 28, "1051903593": 2, "1052553862": 2, "1052553863": 2, "1056992393": 6, "1059396448": 26, "1059396449": 26, "1059396450": 26, "1059396451": 26, "1059396454": 26, "1061507881": 28, "1063676090": 27, "1067535032": 28, "1068450901": 17, "1072603541": 27, "1078536537": 27, "1084954603": 21, "1085298906": 7, "1091411796": 15, "1091411797": 15, "1095307966": 28, "1098547840": 28, "1099926164": 23, "1099926165": 23, "1103965354": 22, "1107624473": 13, "1118642141": 27, "1122245142": 21, "1123433952": 25, "1125064385": 14, "1126194683": 7, "1126354272": 17, "1126354273": 17, "1131177620": 27, "1133567616": 27, "1141639721": 3, "1146620476": 28, "1147673598": 20, "1147673599": 20, "1150304053": 12, "1153991042": 24, "1153991043": 24, "1154659864": 2, "1163431805": 27, "1165452712": 22, "1166603202": 11, "1167848164": 7, "1174021263": 3, "1175295126": 27, "1178333691": 28, "1183116657": 25, "1194085652": 28, "1201773703": 28, "1205148160": 10, "1205148161": 10, "1208043873": 28, "1209319450": 27, "1213512144": 28, "1214401677": 18, "1219866639": 7, "1224603527": 3, "1225396570": 3, "1229961870": 7, "1232802389": 6, "1239024834": 14, "1240184693": 21, "1241704260": 7, "1250597597": 24, "1253607903": 7, "1257250409": 26, "1260041928": 21, "1266122672": 9, "1266122673": 9, "1270288709": 17, "1273354936": 13, "1273354937": 13, "1273510836": 6, "1276995479": 7, "1277281611": 7, "1280755883": 5, "1280894514": 18, "1286837122": 19, "1287851098": 5, "1288683596": 7, "1289637143": 26, "1290784012": 2, "1291931904": 28, "1292425061": 14, "1301731333": 3, "1301766302": 22, "1305141224": 25, "1305141225": 25, "1305141227": 25, "1313059774": 7, "1314609174": 7, "1317763552": 18, "1317763554": 18, "1317763555": 18, "1319537766": 14, "1319537767": 14, "1326541974": 28, "1327236527": 28, "1328786973": 26, "1334842411": 2, "1336181349": 28, "1341471164": 3, "1346652857": 28, "1359567721": 7, "1361620030": 3, "1362709750": 14, "1362709751": 14, "1363029408": 7, "1363029409": 7, "1365491398": 7, "1372428179": 22, "1372475916": 28, "1376512508": 27, "1376763596": 6, "1378639429": 28, "1387769408": 28, "1389298745": 14, "1389520972": 28, "1389546626": 20, "1397284432": 2, "1403799587": 27, "1405130395": 7, "1409670916": 19, "1412999460": 18, "1423305584": 2, "1423305585": 2, "1423305586": 2, "1423305587": 2, "1423305588": 2, "1423305589": 2, "1423305590": 2, "1423305591": 2, "1423305598": 2, "1423305599": 2, "1426177239": 27, "1430140002": 9, "1430140003": 9, "1437388682": 28, "1437735332": 7, "1450633717": 3, "1454610995": 7, "1460042662": 28, "1460885752": 3, "1470356892": 27, "1473368760": 4, "1473368761": 4, "1473368764": 4, "1473368765": 4, "1473368766": 4, "1473368767": 4, "1473923371": 3, "1474803667": 7, "1477100072": 24, "1498848678": 7, "1502135233": 5, "1502135240": 5, "1502135241": 5, "1502135242": 5, "1502135243": 5, "1502135244": 5, "1502135246": 5, "1502135247": 5, "1502692899": 7, "1503505449": 7, "1504815965": 28, "1506728251": 3, "1510405477": 7, "1511744861": 23, "1511744862": 23, "1511744863": 23, "1520144521": 24, "1520696335": 7, "1521934049": 19, "1529773610": 17, "1530302985": 28, "1535334771": 17, "1536294536": 23, "1537854340": 28, "1537854341": 28, "1540031264": 3, "1540650548": 21, "1545181237": 22, "1548056407": 7, "1549308050": 7, "1551213059": 28, "1555733772": 26, "1556831535": 6, "1559478790": 22, "1561249470": 6, "1568774875": 27, "1584837156": 2, "1584837157": 2, "1588392610": 27, "1589318419": 3, "1590938305": 7, "1591188458": 15, "1597235361": 7, "1599443272": 16, "1602334068": 2, "1605596084": 2, "1605599021": 28, "1608003536": 28, "1608003561": 27, "1608034363": 27, "1609141880": 24, "1614326414": 10, "1614326415": 10, "1623653768": 2, "1623653769": 2, "1623653770": 2, "1623653771": 2, "1636916040": 27, "1641172978": 27, "1644189372": 3, "1649929380": 3, "1651275175": 7, "1652641560": 26, "1652641561": 26, "1652641563": 26, "1658124146": 24, "1659137741": 27, "1660362255": 7, "1683482799": 7, "1684784588": 14, "1684784589": 14, "1685642729": 7, "1695429263": 5, "1706764072": 2, "1706764073": 2, "1706874193": 6, "1719391718": 26, "1721185806": 4, "1721185807": 4, "1724587366": 7, "1731825007": 26, "1735710919": 26, "1739076284": 19, "1739076285": 19, "1743191871": 22, "1752648948": 24, "1756958880": 27, "1767312242": 5, "1770862260": 14, "1770862261": 14, "1775707016": 7, "1776174698": 18, "1776174699": 18, "1776851209": 27, "1785547370": 13, "1792195381": 5, "1792846777": 28, "1797416766": 27, "1806024532": 27, "1808697851": 28, "1809917548": 27, "1812385586": 2, "1812385587": 2, "1813474267": 27, "1814904342": 22, "1816495538": 2, "1831485463": 28, "1833569876": 4, "1833569877": 4, "1833569879": 4, "1834731376": 7, "1836235748": 26, "1836636207": 4, "1837000826": 9, "1837000827": 9, "1839291598": 6, "1840892693": 17, "1841030642": 3, "1841451177": 1, "1841451179": 1, "1844125034": 2, "1845372864": 25, "1845978721": 25, "1847708611": 5, "1856262127": 17, "1862324869": 7, "1873273984": 2, "1876000888": 25, "1876000889": 25, "1889037296": 7, "1891321753": 23, "1891373428": 3, "1896146275": 24, "1897528210": 3, "1905328541": 21, "1911078836": 23, "1913500528": 9, "1914989540": 6, "1914989541": 6, "1916785746": 27, "1917595114": 28, "1918109014": 23, "1926033630": 15, "1926693852": 6, "1930161380": 21, "1930161381": 21, "1936516278": 2, "1937181057": 28, "1945196704": 11, "1946527033": 27, "1947358241": 22, "1952523260": 27, "1952643886": 28, "1956273477": 2, "1963513388": 23, "1963513389": 23, "1963513391": 23, "1969910192": 3, "1977129966": 27, "1980042536": 28, "1982350172": 28, "1984190529": 2, "1987263977": 5, "1997760146": 19, "1997760147": 19, "1998779811": 27, "2005189570": 7, "2007623609": 28, "2009106091": 5, "2013981053": 21, "2020751009": 27, "2021594588": 15, "2021594589": 15, "2021594590": 15, "2022821262": 17, "2029899814": 26, "2030596705": 3, "2035035510": 14, "2035035511": 14, "2035692489": 7, "2040496532": 27, "2040680321": 14, "2046798468": 3, "2046938350": 28, "2046938351": 28, "2052186462": 27, "2056606165": 22, "2057068417": 27, "2058055880": 7, "2063744629": 27, "2065460339": 3, "2073101876": 14, "2073576287": 5, "2074467606": 28, "2077249154": 14, "2077249155": 14, "2079349511": 24, "2086902184": 28, "2086902185": 28, "2086902186": 28, "2086902187": 28, "2086902190": 28, "2094466368": 23, "2094466369": 23, "2094466371": 23, "2097732955": 28, "2098788836": 27, "2105409832": 7, "2109156727": 27, "2109271512": 28, "2111111693": 3, "2112508290": 24, "2112889975": 2, "2115919719": 22, "2117628185": 23, "2118730408": 9, "2118730409": 9, "2120905920": 6, "2127474099": 7, "2132252935": 20, "2138467220": 24, "2147433548": 3, "2149696880": 22, "2149696881": 22, "2149696883": 22, "2150778206": 3, "2155928170": 3, "2156817213": 7, "2158678429": 3, "2159113266": 26, "2160858284": 22, "2161618499": 28, "2162879194": 28, "2163610255": 7, "2168550540": 5, "2170265806": 27, "2171727442": 10, "2171727443": 10, "2179603792": 6, "2179603793": 6, "2179603794": 6, "2179603795": 6, "2179603798": 6, "2179603799": 6, "2182425197": 27, "2184423372": 24, "2185878931": 24, "2188135103": 25, "2188783622": 27, "2189358721": 22, "2191200811": 23, "2200729532": 16, "2200729533": 16, "2200729535": 16, "2203814271": 14, "2213504923": 8, "2213649830": 10, "2213649831": 10, "2214585028": 15, "2214585029": 15, "2217042834": 13, "2217751623": 24, "2218362034": 28, "2223901117": 23, "2233576420": 8, "2245996336": 14, "2245996337": 14, "2261046232": 15, "2262043425": 26, "2266083651": 24, "2268540161": 27, "2272646880": 10, "2272646881": 10, "2272646882": 10, "2274492816": 3, "2275638293": 28, "2277536120": 4, "2277536122": 4, "2277536123": 4, "2281501155": 26, "2282978207": 28, "2283141005": 23, "2286806200": 27, "2287277682": 3, "2288100780": 27, "2291082292": 7, "2298896088": 3, "2298896090": 3, "2298896091": 3, "2298896092": 3, "2298896093": 3, "2298896094": 3, "2298896095": 3, "2302680981": 7, "2303499975": 2, "2307238551": 27, "2307306630": 9, "2307426896": 1, "2307426898": 1, "2311506225": 5, "2316331767": 28, "2319743206": 3, "2323339168": 24, "2326578623": 24, "2328435454": 6, "2337290000": 7, "2337719389": 27, "2339497078": 22, "2352138838": 8, "2355278664": 22, "2355278665": 22, "2358907143": 7, "2360840510": 14, "2365263969": 28, "2376234856": 10, "2386208942": 28, "2390807586": 27, "2392813623": 28, "2393578168": 7, "2417955889": 27, "2419100474": 7, "2420567618": 4, "2430095495": 7, "2436402701": 7, "2437611296": 28, "2450544934": 16, "2451830981": 11, "2459768632": 6, "2462335932": 27, "2470583197": 7, "2477028154": 6, "2477980485": 25, "2479769639": 11, "2484811305": 26, "2488007584": 27, "2489453969": 24, "2492769187": 3, "2505000979": 11, "2505678740": 25, "2505678742": 25, "2505678743": 25, "2523388612": 7, "2524308947": 28, "2524996169": 28, "2531130672": 23, "2531130673": 23, "2535002841": 22, "2535628810": 28, "2538439951": 2, "2543971899": 24, "2545426109": 15, "2546370410": 7, "2549404869": 2, "2549951962": 18, "2552460292": 19, "2552460293": 19, "2566083804": 17, "2567978449": 27, "2572547217": 6, "2578820926": 7, "2580533670": 16, "2581389132": 24, "2585373098": 7, "2589289009": 27, "2590810641": 23, "2591111628": 14, "2593080269": 2, "2595813005": 26, "2603335652": 18, "2616697701": 24, "2618313500": 7, "2620089345": 28, "2623660327": 2, "2630114060": 28, "2631466936": 27, "2635366068": 13, "2637837647": 3, "2642798136": 22, "2644193799": 27, "2644573111": 17, "2645567209": 28, "2650448800": 28, "2650448801": 28, "2657823754": 27, "2661272172": 2, "2661272173": 2, "2661326387": 7, "2661999724": 28, "2662868359": 27, "2666273249": 26, "2672844280": 27, "2680809683": 28, "2680976411": 23, "2683352144": 17, "2683352145": 17, "2685001662": 3, "2689748168": 28, "2689748169": 28, "2689748170": 28, "2689748171": 28, "2689748174": 28, "2696245301": 3, "2696974648": 12, "2696974649": 12, "2696974651": 12, "2702372534": 5, "2703464692": 14, "2710427191": 6, "2710628962": 17, "2715580076": 28, "2716279146": 11, "2716279147": 11, "2716406907": 9, "2717158440": 2, "2717540192": 6, "2717540193": 6, "2717540196": 6, "2717540197": 6, "2717540198": 6, "2717540199": 6, "2720534902": 7, "2720753042": 23, "2721907666": 19, "2721907667": 19, "2728851518": 23, "2730237354": 28, "2737282443": 27, "2737886288": 1, "2737886290": 1, "2739996165": 27, "2749628923": 24, "2750411529": 21, "2752635126": 25, "2752635127": 25, "2753887215": 27, "2756505818": 14, "2756505819": 14, "2760398988": 2, "2760449282": 16, "2764545753": 11, "2764769717": 6, "2770157746": 3, "2770988335": 28, "2771192417": 28, "2777868042": 27, "2777913564": 2, "2777913565": 2, "2778157412": 27, "2779499874": 20, "2779687217": 7, "2779700938": 27, "2790011330": 7, "2795689398": 14, "2795689399": 14, "2795914918": 14, "2798209984": 27, "2798209985": 27, "2803131573": 28, "2805101184": 7, "2806946666": 28, "2808451697": 27, "2809967986": 14, "2809967987": 14, "2811138603": 15, "2811521172": 18, "2811521173": 18, "2811539092": 28, "2812100428": 19, "2814093983": 19, "2816306775": 21, "2818971729": 24, "2822855549": 24, "2822855550": 24, "2822855551": 24, "2824302184": 3, "2826719795": 7, "2828252061": 2, "2829803132": 7, "2830258806": 24, "2831606566": 27, "2833210534": 27, "2835570795": 27, "2837295684": 6, "2838582256": 18, "2838582257": 18, "2840606178": 22, "2851897225": 21, "2855956290": 27, "2862929472": 27, "2863148329": 7, "2863148331": 7, "2865556497": 28, "2869466318": 18, "2873488357": 16, "2873900232": 22, "2877046370": 7, "2891490654": 5, "2892092373": 15, "2894282039": 23, "2896897122": 26, "2897466191": 3, "2900154861": 7, "2901472731": 27, "2908653246": 28, "2913001516": 28, "2916149327": 22, "2917303019": 28, "2922964484": 28, "2924095235": 3, "2924794318": 11, "2924954886": 3, "2928555612": 23, "2928555613": 23, "2928555615": 23, "2933249366": 24, "2933904296": 11, "2940416351": 7, "2948276931": 27, "2957208150": 25, "2964031580": 17, "2964031581": 17, "2965080304": 27, "2972379642": 23, "2977588624": 28, "2978747767": 3, "2985926430": 25, "2985926431": 25, "2988472426": 14, "2988472427": 14, "2990668488": 19, "2990668490": 19, "2990668491": 19, "2991116200": 13, "2991116201": 13, "2991116203": 13, "2991353901": 11, "2994721336": 3, "2999808676": 7, "3004379087": 14, "3007479950": 23, "3007586538": 3, "3013191049": 26, "3015197581": 3, "3015877110": 27, "3039687635": 7, "3040820634": 28, "3053891303": 22, "3070846922": 27, "3070846923": 27, "3077367255": 3, "3078369200": 27, "3078369201": 27, "3078369202": 27, "3078369203": 27, "3078369205": 27, "3078552186": 15, "3082058813": 19, "3084686800": 7, "3085024168": 26, "3085024169": 26, "3085024171": 26, "3085191056": 28, "3085191057": 28, "3085191058": 28, "3087039277": 4, "3101330395": 3, "3101968640": 28, "3101968641": 28, "3101968642": 28, "3101968643": 28, "3101968644": 28, "3101968645": 28, "3101968646": 28, "3101968647": 28, "3101968650": 28, "3101968651": 28, "3103255595": 22, "3104384024": 3, "3105953706": 2, "3105953707": 2, "3110986887": 21, "3111568261": 28, "3116258568": 14, "3116258570": 14, "3116258571": 14, "3117902614": 27, "3118746352": 28, "3118746353": 28, "3118746354": 28, "3118746355": 28, "3118746356": 28, "3118746357": 28, "3118746358": 28, "3118746359": 28, "3118746366": 28, "3118746367": 28, "3130152719": 27, "3131849739": 26, "3140833552": 2, "3140833553": 2, "3140833554": 2, "3140833555": 2, "3140833556": 2, "3140833557": 2, "3140833558": 2, "3140833559": 2, "3148074173": 6, "3153330859": 28, "3154193408": 22, "3154194046": 23, "3154516913": 2, "3159052337": 7, "3159160837": 24, "3160643158": 25, "3161483583": 26, "3161524490": 2, "3168934826": 24, "3175859009": 26, "3177119978": 2, "3179985807": 22, "3186325460": 5, "3186325461": 5, "3186325462": 5, "3186325463": 5, "3192336962": 3, "3192591867": 7, "3206190066": 23, "3206549834": 27, "3209634107": 7, "3211222305": 3, "3214073080": 23, "3214073081": 23, "3216891360": 28, "3216891361": 28, "3216891362": 28, "3216891363": 28, "3216891364": 28, "3216891365": 28, "3216891366": 28, "3216891367": 28, "3216891370": 28, "3216891371": 28, "3220992341": 28, "3224066584": 15, "3226562059": 28, "3232214604": 27, "3232831379": 24, "3234744410": 14, "3234744411": 14, "3235287656": 11, "3236510875": 3, "3240434620": 21, "3252358296": 6, "3252358297": 6, "3252358300": 6, "3252358301": 6, "3252358302": 6, "3252358303": 6, "3256453690": 21, "3260983964": 7, "3264951903": 28, "3266866780": 28, "3268422857": 27, "3272276569": 28, "3275543811": 23, "3275722635": 28, "3280087347": 14, "3283170684": 14, "3287208327": 7, "3287805174": 2, "3287805175": 2, "3287805176": 2, "3287805177": 2, "3287805178": 2, "3287805179": 2, "3287805180": 2, "3287805181": 2, "3287805183": 2, "3291371395": 27, "3293207827": 27, "3297537117": 24, "3299468272": 22, "3299468273": 22, "3299468275": 22, "3299562545": 28, "3301342408": 12, "3301342410": 12, "3301342411": 12, "3301429924": 5, "3305748852": 28, "3306267716": 2, "3306754793": 24, "3309556272": 2, "3312324260": 27, "3314030149": 28, "3317961966": 27, "3325463374": 4, "3328019216": 27, "3328129868": 7, "3328839466": 5, "3328839467": 5, "3329153302": 3, "3329514528": 24, "3340962430": 28, "3344147835": 27, "3344147836": 27, "3344147837": 27, "3344147838": 27, "3344147839": 27, "3350146056": 6, "3352566658": 2, "3359671646": 7, "3365248655": 10, "3368317463": 5, "3377110417": 27, "3386596901": 21, "3393712456": 22, "3396133977": 28, "3397132454": 15, "3397132455": 15, "3400256755": 15, "3401752901": 28, "3403933998": 28, "3405314918": 7, "3421411300": 7, "3431536253": 27, "3431752854": 25, "3432146796": 23, "3433719984": 28, "3433854194": 7, "3444687693": 3, "3446591075": 27, "3446862222": 13, "3446862223": 13, "3450902429": 26, "3455566107": 2, "3459537037": 28, "3469344438": 21, "3469344439": 21, "3475074928": 3, "3478783829": 11, "3483485727": 25, "3493861052": 20, "3493861054": 20, "3493861055": 20, "3499472512": 15, "3505282728": 28, "3510413604": 23, "3510701861": 27, "3514681868": 7, "3515978520": 23, "3520162133": 7, "3525545299": 3, "3526595120": 22, "3537584122": 28, "3538363663": 14, "3542826854": 15, "3547113102": 15, "3547113103": 15, "3557577228": 27, "3557583914": 28, "3558681245": 25, "3559361670": 20, "3569791559": 2, "3570942492": 11, "3573686365": 19, "3574168117": 27, "3579036040": 28, "3579036044": 28, "3579036045": 28, "3579036046": 28, "3579036047": 28, "3585698380": 27, "3586026952": 22, "3587167050": 2, "3587167051": 2, "3588864529": 4, "3589159886": 28, "3596008431": 21, "3599356244": 22, "3607796223": 27, "3611487543": 3, "3613939175": 7, "3620666320": 3, "3622281554": 27, "3626242164": 17, "3626242165": 17, "3626242166": 17, "3633213242": 21, "3634214125": 7, "3636889164": 14, "3643047597": 27, "3645474285": 25, "3645474286": 25, "3645474287": 25, "3649985571": 25, "3650837301": 27, "3665405251": 27, "3677746972": 8, "3677746973": 8, "3677746975": 8, "3684684730": 19, "3685829362": 28, "3685996623": 3, "3689280567": 28, "3696163951": 22, "3713006278": 25, "3713006279": 25, "3717471208": 3, "3717471209": 3, "3717583858": 27, "3720087872": 23, "3721195043": 11, "3725260498": 7, "3729709035": 2, "3733212952": 28, "3734501342": 2, "3738044006": 6, "3744631007": 22, "3748622249": 3, "3755201983": 8, "3755408274": 27, "3766145631": 27, "3767115045": 28, "3769043228": 22, "3782991997": 25, "3782991998": 25, "3782991999": 25, "3785574525": 28, "3790856148": 27, "3791120690": 3, "3792590697": 5, "3796358144": 6, "3800267412": 2, "3800267413": 2, "3800267414": 2, "3800267415": 2, "3802876271": 2, "3809902215": 24, "3811494885": 28, "3823337168": 7, "3829285960": 4, "3830828709": 7, "3845731526": 4, "3845888162": 27, "3846727856": 11, "3850655136": 2, "3851917393": 17, "3853434114": 3, "3856725767": 28, "3859483818": 3, "3859483819": 3, "3863829688": 27, "3866715933": 2, "3867275449": 3, "3871226707": 22, "3875413893": 1, "3876588445": 17, "3892841518": 7, "3896528758": 27, "3898373019": 28, "3900456246": 25, "3903976633": 7, "3909352274": 2, "3915635973": 3, "3919847952": 13, "3926237005": 7, "3929403535": 3, "3932439362": 5, "3932814032": 7, "3933714914": 22, "3943394479": 3, "3948527015": 11, "3956230125": 2, "3965417933": 6, "3967067356": 17, "3968560442": 7, "3970040886": 24, "3971839985": 7, "3977654524": 27, "3980154334": 28, "3980931274": 28, "3981634627": 9, "3983534173": 21, "3983534174": 21, "3983534175": 21, "3987309016": 22, "3987442049": 7, "4008136730": 28, "4012977684": 14, "4012977685": 14, "4018727386": 4, "4019596374": 5, "4019596375": 9, "4019651319": 27, "4024179293": 28, "4027251027": 7, "4031340274": 5, "4031340275": 5, "4036178150": 2, "4040885654": 27, "4048242887": 20, "4051296206": 27, "4059030097": 2, "4068780317": 28, "4075522049": 6, "4076827687": 26, "4082180156": 23, "4082425930": 24, "4087530286": 8, "4087530287": 8, "4088827200": 12, "4088827201": 12, "4088827202": 12, "4090775747": 11, "4094504409": 6, "4095784502": 23, "4095816113": 27, "4100029812": 7, "4106757302": 24, "4108914784": 7, "4116537170": 11, "4119622018": 21, "4123860739": 25, "4125594864": 3, "4128297107": 7, "4138697980": 21, "4142792564": 3, "4144402969": 28, "4153787689": 11, "4155785311": 27, "4163975820": 24, "4169225313": 24, "4173467416": 7, "4173467417": 7, "4178158375": 7, "4180444323": 27, "4182403848": 26, "4201060647": 25, "4203612964": 6, "4205213276": 27, "4213271810": 3, "4213271811": 3, "4216207299": 27, "4219537344": 3, "4220529694": 23, "4221563250": 22, "4222561358": 19, "4224972854": 4, "4224972855": 4, "4242735326": 7, "4243004391": 3, "4245469491": 3, "4250956545": 27, "4251951858": 5, "4251951859": 5, "4251951860": 5, "4251951862": 5, "4251951863": 5, "4253857367": 21, "4261618303": 27, "4266438808": 26, "4272367383": 3, "4280200672": 11, "4282413899": 27, "4282498224": 7, "4283023978": 4, "4283023979": 4, "4283023980": 4, "4283023981": 4, "4283023982": 4, "4283023983": 4 } ================================================ FILE: src/data/d2/source-info-v2.ts ================================================ const D2Sources: { [key: string]: { itemHashes?: number[]; sourceHashes?: number[]; aliases?: string[]; enteredDCV?: number; }; } = { '30th': { sourceHashes: [ 443340273, // Source: Xûr's Treasure Hoard in Eternity 675740011, // Source: "Grasp of Avarice" Dungeon 1102533392, // Source: Xûr (Eternity) 1394793197, // Source: "Magnum Opus" Quest 2763252588, // Source: "And Out Fly the Wolves" Quest ], }, adventure: { sourceHashes: [ 194661944, // Source: Adventure "Siren Song" on Saturn's Moon, Titan 482012099, // Source: Adventure "Thief of Thieves" on Saturn's Moon, Titan 636474187, // Source: Adventure "Deathless" on Saturn's Moon, Titan 783399508, // Source: Adventure "Supply and Demand" in the European Dead Zone 790433146, // Source: Adventure "Dark Alliance" in the European Dead Zone 1067250718, // Source: Adventure "Arecibo" on Io 1186140085, // Source: Adventure "Unbreakable" on Nessus 1289998337, // Source: Adventure "Hack the Planet" on Nessus 1527887247, // Source: Adventure "Red Legion, Black Oil" in the European Dead Zone 1736997121, // Source: Adventure "Stop and Go" in the European Dead Zone 1861838843, // Source: Adventure "A Frame Job" in the European Dead Zone 2040548068, // Source: Adventure "Release" on Nessus 2096915131, // Source: Adventure "Poor Reception" in the European Dead Zone 2345202459, // Source: Adventure "Invitation from the Emperor" on Nessus 2392127416, // Source: Adventure "Cliffhanger" on Io 2553369674, // Source: Adventure "Exodus Siege" on Nessus 3427537854, // Source: Adventure "Road Rage" on Io 3754173885, // Source: Adventure "Getting Your Hands Dirty" in the European Dead Zone 4214471686, // Source: Adventure "Unsafe at Any Speed" in the European Dead Zone ], enteredDCV: 20, }, avalon: { sourceHashes: [ 709680645, // Source: "Truly Satisfactory" Triumph 1476475066, // Source: "Firmware Update" Triumph 1730197643, // Source: //node.ovrd.AVALON// Exotic Quest ], enteredDCV: 24, }, battlegrounds: { itemHashes: [ 2121785039, // Brass Attacks 3075224551, // Threaded Needle ], sourceHashes: [ 3391325445, // Source: Battlegrounds ], enteredDCV: 24, }, blackarmory: { itemHashes: [ 417164956, // Jötunn 3211806999, // Izanagi's Burden 3588934839, // Le Monarque 3650581584, // New Age Black Armory 3650581585, // Refurbished Black Armory 3650581586, // Rasmussen Clan 3650581587, // House of Meyrin 3650581588, // Satou Tribe 3650581589, // Bergusian Night ], sourceHashes: [ 266896577, // Source: Solve the Norse glyph puzzle. 439994003, // Source: Complete the "Master Smith" Triumph. 925197669, // Source: Complete a Bergusia Forge ignition. 948753311, // Source: Found by completing Volundr Forge ignitions. 1286332045, // Source: Found by completing Izanami Forge ignitions. 1457456824, // Source: Complete the "Reunited Siblings" Triumph. 1465990789, // Source: Solve the Japanese glyph puzzle. 1596507419, // Source: Complete a Gofannon Forge ignition. 2062058385, // Source: Crafted in a Black Armory forge. 2384327872, // Source: Solve the French glyph puzzle. 2541753910, // Source: Complete the "Master Blaster" Triumph. 2966694626, // Source: Found by solving the mysteries behind the Black Armory's founding families. 3047033583, // Source: Returned the Obsidian Accelerator. 3257722699, // Source: Complete the "Clean Up on Aisle Five" Triumph. 3390164851, // Source: Found by turning in Black Armory bounties. 3764925750, // Source: Complete an Izanami Forge ignition. 4101102010, // Source: Found by completing Bergusia Forge ignitions. 4247521481, // Source: Complete the "Beautiful but Deadly" Triumph. 4290227252, // Source: Complete a Volundr Forge ignition. ], aliases: ['ada'], enteredDCV: 20, }, brave: { itemHashes: [ 205225492, // Hung Jury SR4 211732170, // Hammerhead 243425374, // Falling Guillotine 570866107, // Succession 2228325504, // Edge Transit 2480074702, // Forbearance 2499720827, // Midnight Coup 2533990645, // Blast Furnace 3098328572, // The Recluse 3757612024, // Luna's Howl 3851176026, // Elsie's Rifle 4043921923, // The Mountaintop ], sourceHashes: [ 2952071500, // Source: Into the Light ], }, calus: { itemHashes: [ 947448544, // Shadow of Earth Shell 1661191192, // The Tribute Hall 1661191193, // Crown of Sorrow 1661191194, // A Hall of Delights 1661191195, // The Imperial Menagerie 2027598066, // Imperial Opulence 2027598067, // Imperial Dress 2816212794, // Bad Juju 3176509806, // Árma Mákhēs 3580904580, // Legend of Acrius 3841416152, // Golden Empire 3841416153, // Goldleaf 3841416154, // Shadow Gilt 3841416155, // Cinderchar 3875444086, // The Emperor's Chosen ], enteredDCV: 20, sourceHashes: [ 1675483099, // Source: Leviathan, Spire of Stars raid lair. 2399751101, // Acquired from the raid "Crown of Sorrow." 2511152325, // Acquired from the Menagerie aboard the Leviathan. 2653618435, // Source: Leviathan raid. 2765304727, // Source: Leviathan raid on Prestige difficulty. 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. 2937902448, // Source: Leviathan, Eater of Worlds raid lair. 3147603678, // Acquired from the raid "Crown of Sorrow." 4009509410, // Source: Complete challenges in the Leviathan raid. 4066007318, // Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty. ], }, campaign: { sourceHashes: [ 13912404, // Source: Unlock Your Arc Subclass 100617404, // Requires Titan Class 286427063, // Source: Fallen Empire Campaign 409652252, // Source: The Witch Queen Campaign 431243768, // Source: The Edge of Fate Campaign 460742691, // Requires Guardian Rank 6: Masterwork Weapons 569214265, // Source: Red War Campaign 633667627, // Requires Tier 4 or 5 Weapon 677167936, // Source: Complete the campaign as a Warlock. 712662541, // Requires Season 27 Tier 5 Weapon 736336644, // Source: "A Spark of Hope" Quest 901482731, // Source: Lightfall Campaign 918840100, // Source: Shadowkeep Campaign 923708784, // Requires Guardian Rank 7: Threats and Surges 958460845, // Source: The Final Shape Campaign 1076222895, // Source: Defeat bosses in Flashpoints. 1103518848, // Source: Earned over the course of the Warmind campaign. 1118966764, // Source: Dismantle an item with this shader applied to it. 1281387702, // Source: Unlock Your Void Subclass 1701477406, // Source: Flashpoint milestones; Legendary engrams. 2242939082, // Requires Hunter Class 2278847330, // Requires Guardian Rank 3 2308290458, // Requires 1,000 Warlock Kills 2552784968, // Requires Guardian Rank 2 2744321951, // Source: Complete a heroic Public Event. 2892963218, // Source: Earned while leveling. 2895784523, // Source: Pledge to all factions on a single character. 2929562373, // Source: Unlock Your Solar Subclass 2988465950, // Source: Planetary faction chests. 3099553329, // Source: Complete the campaign as a Titan. 3126774631, // Requires 1,000 Hunter Kills 3174947771, // Requires Guardian Rank 6: Vendor Challenges 3431853656, // Achieved a Grimoire score of over 5000 in Destiny. 3532642391, // Source: Forsaken Campaign 3704442923, // Source: Curse of Osiris Campaign 3936473457, // Requires Warlock Class 4008954452, // Requires Shaped or Enhanced Weapon 4288102251, // Requires 1,000 Titan Kills 4290499613, // Source: Complete the campaign as a Hunter. ], }, cayde6: { sourceHashes: [ 2206233229, // Source: Follow treasure maps. ], enteredDCV: 20, }, compass: { sourceHashes: [ 164083100, // Source: Display of Supremacy, Weekly Challenge 3100439379, // Source: Mission "Exorcism" ], enteredDCV: 20, }, conquest: { sourceHashes: [ 1331532890, // Source: Seasonal Conquest Triumph "Ultimate Victory" ], }, contact: { sourceHashes: [ 2039343154, // Source: Contact Public Event ], enteredDCV: 20, }, crotasend: { sourceHashes: [ 1897187034, // Source: "Crota's End" Raid ], aliases: ['crota'], }, crownofsorrow: { itemHashes: [ 947448544, // Shadow of Earth Shell 1661191193, // Crown of Sorrow 2027598066, // Imperial Opulence 2027598067, // Imperial Dress ], sourceHashes: [ 2399751101, // Acquired from the raid "Crown of Sorrow." 3147603678, // Acquired from the raid "Crown of Sorrow." ], aliases: ['cos'], enteredDCV: 20, }, crucible: { itemHashes: [ 2307365, // The Inquisitor (Adept) 51129316, // The Inquisitor 161675590, // Whistler's Whim (Adept) 303107619, // Tomorrow's Answer (Adept) 501345268, // Shayura's Wrath (Adept) 548809020, // Exalted Truth 627188188, // Eye of Sol 711889599, // Whistler's Whim (Adept) 769099721, // Devil in the Details 825554997, // The Inquisitor (Adept) 854379020, // Astral Horizon (Adept) 874623537, // Cataphract GL3 (Adept) 906840740, // Unwavering Duty 1141586039, // Unexpected Resurgence (Adept) 1201528146, // Exalted Truth (Adept) 1230660649, // Victory's Wreath 1292594730, // The Summoner (Adept) 1321626661, // Eye of Sol (Adept) 1401300690, // Eye of Sol 1574601402, // Whistler's Whim 1661191197, // Disdain for Glitter 1705843397, // Exalted Truth (Adept) 1711056134, // Incisor 1820994983, // The Summoner 1893967086, // Keen Thistle 1968410628, // The Prophet 1973107014, // Igneous Hammer 2022294213, // Shayura's Wrath 2059255495, // Eye of Sol (Adept) 2185327324, // The Inquisitor 2300143112, // Yesterday's Question 2314610827, // Igneous Hammer (Adept) 2330860573, // The Inquisitor (Adept) 2378785953, // Yesterday's Question (Adept) 2414564781, // Punctuation Marks 2420153991, // Made Shaxx Proud 2421180981, // Incisor (Adept) 2588739576, // Crucible Solemnity 2588739578, // Crucible Legacy 2588739579, // Crucible Metallic 2632846356, // Rain of Ashes 2653171212, // The Inquisitor 2653171213, // Astral Horizon 2738601016, // Cataphract GL3 2759251821, // Unwavering Duty (Adept) 2839600459, // Incisor (Adept) 3001205424, // Ecliptic Distaff 3009199534, // Tomorrow's Answer 3019024381, // The Prophet (Adept) 3102421004, // Exalted Truth 3165143747, // Whistler's Whim 3193598749, // The Immortal (Adept) 3332125295, // Aisha's Care (Adept) 3436626079, // Exalted Truth 3444632029, // Unwavering Duty (Adept) 3503560035, // Keen Thistle (Adept) 3624844116, // Unwavering Duty 3920882229, // Exalted Truth (Adept) 3928440584, // Crucible Carmine 3928440585, // Crucible Redjack 3969379530, // Aisha's Care 4005780578, // Unexpected Resurgence 4039572196, // The Immortal 4060882456, // Rubicund Wrap (Ornament) 4248997900, // Incisor ], sourceHashes: [ 164083100, // Source: Display of Supremacy, Weekly Challenge 454115234, // Source: Associated Crucible Quest 598662729, // Source: Reach Glory Rank "Legend" in the Crucible. 705363737, // Source: Heavy Metal: Supremacy 745186842, // Source: Associated Crucible Quest 897576623, // Source: Complete Crucible matches and earn rank-up packages from Lord Shaxx. 929025440, // Acquired by competing in the Crucible during the Prismatic Inferno. 1217831333, // Source: Associated Crucible Quest 1223492644, // Source: Complete the "Reconnaissance by Fire" quest. 1465057711, // Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit) 1494513645, // Source: Glory Matches in Crucible 2055470113, // Source: Chance to acquire when completing Crucible Survival matches after reaching Glory Rank "Mythic." 2537301256, // Source: Glory Rank of "Fabled" in Crucible 2558941813, // Source: Place Silver III Division or Higher in Ranked Crucible Playlists 2622122683, // Source: Lord Shaxx Rank Up Reputation 2641169841, // Source: Purchase from Lord Shaxx 2658055900, // Source: Complete the "Season 8: Battle Drills" quest. 2669524419, // Source: Crucible 2821852478, // Source: Complete this weapon's associated Crucible quest. 2915991372, // Source: Crucible 3020288414, // Source: Crucible 3226099405, // Source: Crucible Seasonal Ritual Rank Reward 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit 3466789677, // Source: Place Ascendant III Division or Higher in Ranked Crucible Playlists 3656787928, // Source: Crucible Salvager's Salvo Armament ], aliases: ['shaxx'], }, deepstonecrypt: { sourceHashes: [ 866530798, // Source: "Not a Scratch" Triumph 1405897559, // Source: "Deep Stone Crypt" Raid 1692165595, // Source: "Rock Bottom" Triumph ], aliases: ['dsc'], }, deluxe: { sourceHashes: [ 639650067, // Source: Limited Edition of Destiny 2. 1358645302, // Source: Unlocked by a special offer. 1412777465, // Source: Forsaken Refer-a-Friend 1743434737, // Source: Destiny 2 "Forsaken" preorder bonus gift. 1866448829, // Source: Deluxe Edition Bonus 2968206374, // Source: Earned as a Deluxe Edition bonus. 2985242208, // Source: Earned from a charity promotion. 3173463761, // Source: Pre-order Bonus 3212282221, // Source: Forsaken Annual Pass 3672287903, // Source: The Witch Queen Digital Deluxe Edition 4069355515, // Source: Handed out at US events in 2019. 4166998204, // Source: Earned as a pre-order bonus. ], aliases: ['limited'], }, desertperpetual: { sourceHashes: [ 596084342, // Source: "The Desert Perpetual" Raid 2127551856, // Source: "The Desert Perpetual" Epic Raid ], }, do: { sourceHashes: [ 146504277, // Source: Earn rank-up packages from Arach Jalaal. ], enteredDCV: 20, }, dreaming: { itemHashes: [ 185321779, // Ennead 3352019292, // Secret Victories ], sourceHashes: [ 2559145507, // Source: Complete activities in the Dreaming City. 3874934421, // Source: Complete Nightfall strike "The Corrupted." ], }, duality: { sourceHashes: [ 1282207663, // Source: Dungeon "Duality" ], }, dungeon: { sourceHashes: [ 506073192, // Source: "Prophecy" Dungeon 613435025, // Source: "Warlord's Ruin" Dungeon 675740011, // Source: "Grasp of Avarice" Dungeon 877404349, // Source: Rite of the Nine 1282207663, // Source: Dungeon "Duality" 1597738585, // Source: "Spire of the Watcher" Dungeon 1745960977, // Source: "Pit of Heresy" Dungeon 2463956052, // Source: Vesper's Host 2607970476, // Source: Sundered Doctrine 3247513834, // Source: Equilibrium 3288974535, // Source: "Ghosts of the Deep" Dungeon ], itemHashes: [ 14929251, // Long Arm 185321778, // The Eternal Return 189194532, // No Survivors (Adept) 233402416, // New Pacific Epitaph (Adept) 291447487, // Cold Comfort 492673102, // New Pacific Epitaph 749483159, // Prosecutor (Adept) 814876684, // Wish-Ender 1050582210, // Greasy Luck (Adept) 1066598837, // Relentless (Adept) 1157220231, // No Survivors (Adept) 1303313141, // Unsworn 1460079227, // Liminal Vigil 1685406703, // Greasy Luck 1773934241, // Judgment 1817605554, // Cold Comfort (Adept) 1904170910, // A Sudden Death 1987644603, // Judgment (Adept) 2059741649, // New Pacific Epitaph 2126543269, // Cold Comfort (Adept) 2129814338, // Prosecutor 2477408004, // Wilderflight (Adept) 2730671571, // Terminus Horizon 2760833884, // Cold Comfort 2764074355, // A Sudden Death (Adept) 2844014413, // Pallas Galliot 2934305134, // Greasy Luck 2982006965, // Wilderflight 3185151619, // New Pacific Epitaph (Adept) 3210739171, // Greasy Luck (Adept) 3329218848, // Judgment (Adept) 3421639790, // Liminal Vigil (Adept) 3681280908, // Relentless 3692140710, // Long Arm (Adept) 4193602194, // No Survivors 4228149269, // No Survivors 4267192886, // Terminus Horizon (Adept) ], }, echoes: { sourceHashes: [ 536806855, // Source: Episode: Echoes 2306801178, // Source: Episode: Echoes Activities 2514060836, // Source: Episode: Echoes Enigma Protocol Activity 2631398023, // Source: Radiolite Bay Deposits ], }, edgeoffate: { sourceHashes: [ 431243768, // Source: The Edge of Fate Campaign 4034415948, // Source: The Edge of Fate Activities ], }, edz: { sourceHashes: [ 783399508, // Source: Adventure "Supply and Demand" in the European Dead Zone 790433146, // Source: Adventure "Dark Alliance" in the European Dead Zone 1373723300, // Source: Complete activities and earn rank-up packages in the EDZ. 1527887247, // Source: Adventure "Red Legion, Black Oil" in the European Dead Zone 1736997121, // Source: Adventure "Stop and Go" in the European Dead Zone 1861838843, // Source: Adventure "A Frame Job" in the European Dead Zone 2096915131, // Source: Adventure "Poor Reception" in the European Dead Zone 3754173885, // Source: Adventure "Getting Your Hands Dirty" in the European Dead Zone 4214471686, // Source: Adventure "Unsafe at Any Speed" in the European Dead Zone 4292996207, // Source: World Quest "Enhance!" in the European Dead Zone. ], }, eow: { sourceHashes: [ 2937902448, // Source: Leviathan, Eater of Worlds raid lair. 4066007318, // Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty. ], enteredDCV: 20, }, ep: { sourceHashes: [ 4137108180, // Source: Escalation Protocol on Mars. ], enteredDCV: 20, }, equilibrium: { sourceHashes: [ 3247513834, // Source: Equilibrium ], }, europa: { sourceHashes: [ 286427063, // Source: Fallen Empire Campaign 1148859274, // Source: Exploring Europa 1492981395, // Source: "The Stasis Prototype" Quest 2171520631, // Source: "Lost Lament" Exotic Quest 3125456997, // Source: Europan Tour 3965815470, // Source: Higher Difficulty Empire Hunts ], }, events: { itemHashes: [ 425681240, // Acosmic 601948197, // Zephyr 689294985, // Jurassic Green 1280894514, // Mechabre 2477980485, // Mechabre 2603335652, // Jurassic Green 2869466318, // BrayTech Werewolf 3400256755, // Zephyr 3558681245, // BrayTech Werewolf 3559361670, // The Title ], sourceHashes: [ 32323943, // Source: Moments of Triumph 151416041, // Source: Solstice 464727567, // Source: Dawning 2021 547767158, // Source: Dawning 2018 611838069, // Source: Guardian Games 629617846, // Source: Dawning 2020 641018908, // Source: Solstice 2018 772619302, // Completed all 8 Moments of Triumph in Destiny's second year. 894030814, // Source: Heavy Metal Event 923678151, // Source: Upgraded Event Card Reward 1054169368, // Source: Festival of the Lost 2021 1225476079, // Source: Moments of Triumph 2022 1232863328, // Source: Moments of Triumph 2024 1360005982, // Completed a Moment of Triumph in Destiny's second year. 1397119901, // Completed a Moment of Triumph in Destiny's first year. 1416471099, // Source: Moments of Triumph 2023 1462687159, // Reached level 5 in the Ages of Triumph record book. 1505938361, // Source: Call to Arms Event 1568732528, // Source: Guardian Games 2024 1666677522, // Source: Solstice 1677921161, // Source: Festival of the Lost 2018. 1919933822, // Source: Festival of the Lost 2020 1953779156, // Source: Events 2006303146, // Source: Guardian Games 2022 2011810450, // Source: Season 13 Guardian Games 2045032171, // Source: Arms Week Event 2050870152, // Source: Solstice 2187511136, // Source: Earned during the seasonal Revelry event. 2364515524, // Source: Dawning 2022 2473294025, // Source: Guardian Games 2023 2502262376, // Source: Earned during the seasonal Crimson Days event. 2797674516, // Source: Moments of Triumph 2021 3092212681, // Source: Dawning 2019 3095773956, // Source: Guardian Games 2025 3112857249, // Completed all 10 Moments of Triumph in Destiny's first year. 3190938946, // Source: Festival of the Lost 2019 3388021959, // Source: Guardian Games 3482766024, // Source: Festival of the Lost 2024 3693722471, // Source: Festival of the Lost 2020 3724111213, // Source: Solstice 2019 3736521079, // Reached level 1 in the Ages of Triumph record book. 3952847349, // Source: The Dawning. 4041583267, // Source: Festival of the Lost 4054646289, // Source: Earned during the seasonal Dawning event. ], }, eververse: { sourceHashes: [ 269962496, // Source: Eververse 860688654, // Source: Eververse 2882367429, // Source: Eververse\nComplete the "Vault of Glass" raid to unlock this in Eververse. 4036739795, // Source: Bright Engrams ], }, evidenceboard: { sourceHashes: [ 1309588429, // Source: "Chief Investigator" Triumph 2055289873, // Source: "The Evidence Board" Exotic Quest ], aliases: ['enclave'], }, exoticquest: { sourceHashes: [ 210885364, // Source: Flawless "Presage" Exotic Quest on Master Difficulty 281362298, // Source: Strider Exotic Quest 454251931, // Source: "What Remains" Exotic Quest 483798855, // Source: "The Final Strand" Exotic Quest 709680645, // Source: "Truly Satisfactory" Triumph 1141831282, // Source: "Of Queens and Worms" Exotic Quest 1302157812, // Source: Wild Card Exotic Quest 1388323447, // Source: Exotic Mission "The Whisper" 1476475066, // Source: "Firmware Update" Triumph 1730197643, // Source: //node.ovrd.AVALON// Exotic Quest 1823766625, // Source: "Vox Obscura" Exotic Quest 1957611613, // Source: An Exotic quest or challenge. 2055289873, // Source: "The Evidence Board" Exotic Quest 2068312112, // Source: Exotic Mission "Zero Hour" 2171520631, // Source: "Lost Lament" Exotic Quest 2296534980, // Source: Exotic Mission Encore 2745272818, // Source: "Presage" Exotic Quest 2856954949, // Source: "Let Loose Thy Talons" Exotic Quest 3237053501, // Source: Heliostat 3597879858, // Source: "Presage" Exotic Quest ], }, fwc: { sourceHashes: [ 3569603185, // Source: Earn rank-up packages from Lakshmi-2. ], enteredDCV: 20, }, gambit: { itemHashes: [ 180108390, // Kit and Kaboodle 180108391, // Dance the Demons Away 1335424933, // Gambit Suede 1335424934, // Gambit Chrome 1335424935, // Gambit Leather 1661191187, // Mistrust of Gifts 2026755633, // Breakneck 2224920148, // Gambit Blackguard 2224920149, // Gambit Steel 2394866220, // Keep on Drifting 2588647363, // Live for the Hustle 3001205424, // Ecliptic Distaff 3217477988, // Gambit Duds 4060882457, // Snakeskin Wrap (Ornament) ], sourceHashes: [ 186854335, // Source: Gambit 571102497, // Source: Associated Gambit Quest 594786771, // Source: Complete this weapon's associated Gambit quest. 887452441, // Source: Gambit Salvager's Salvo Armament 1127923611, // Source: 3 Gambit Rank Resets in a Season 1162859311, // Source: Complete the "Clean Getaway" quest. 1465057711, // Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit) 2170269026, // Source: Complete Gambit matches and earn rank-up packages from the Drifter. 2364933290, // Source: Gambit Seasonal Ritual Rank Reward 2601524261, // Source: Associated Gambit Quest 2843045413, // Source: Gambit 2883838366, // Source: Complete the "Breakneck" quest from the Drifter. 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit 3422985544, // Source: Associated Gambit Quest 3494247523, // Source: Complete the "Season 8: Keepin' On" quest. 3522070610, // Source: Gambit 3942778906, // Source: Drifter Rank Up Reputation ], aliases: ['drifter'], }, gambitprime: { itemHashes: [ 2868525740, // The Collector 2868525741, // The Invader 2868525742, // The Reaper 2868525743, // The Sentry 3808901541, // Viper Strike ], sourceHashes: [ 1952675042, // Source: Complete Gambit Prime matches and increase your rank. ], enteredDCV: 20, }, gardenofsalvation: { itemHashes: [ 4103414242, // Divinity ], sourceHashes: [ 1491707941, // Source: "Garden of Salvation" Raid ], aliases: ['gos', 'garden'], }, ghostsofthedeep: { itemHashes: [ 189194532, // No Survivors (Adept) 233402416, // New Pacific Epitaph (Adept) 291447487, // Cold Comfort 492673102, // New Pacific Epitaph 1050582210, // Greasy Luck (Adept) 1157220231, // No Survivors (Adept) 1685406703, // Greasy Luck 1817605554, // Cold Comfort (Adept) 2059741649, // New Pacific Epitaph 2126543269, // Cold Comfort (Adept) 2760833884, // Cold Comfort 2934305134, // Greasy Luck 3185151619, // New Pacific Epitaph (Adept) 3210739171, // Greasy Luck (Adept) 4193602194, // No Survivors 4228149269, // No Survivors ], sourceHashes: [ 3288974535, // Source: "Ghosts of the Deep" Dungeon ], aliases: ['gotd'], }, grasp: { sourceHashes: [ 675740011, // Source: "Grasp of Avarice" Dungeon ], }, gunsmith: { sourceHashes: [ 1459595344, // Source: Purchase from Banshee-44 or Ada-1 1788267693, // Source: Earn rank-up packages from Banshee-44. 2986841134, // Source: Salvager's Salvo Armament Quest 3512613235, // Source: "A Sacred Fusion" Quest ], aliases: ['banshee'], }, harbinger: { sourceHashes: [ 2856954949, // Source: "Let Loose Thy Talons" Exotic Quest ], }, haunted: { itemHashes: [ 1478986057, // Without Remorse 2778013407, // Firefright ], sourceHashes: [ 620369433, // Source: Season of the Haunted Triumph 976328308, // Source: The Derelict Leviathan 1283862526, // Source: Season of the Haunted Nightfall Grandmaster 2273761598, // Source: Season of the Haunted Activities 2676881949, // Source: Season of the Haunted ], enteredDCV: 20, }, heliostat: { sourceHashes: [ 3237053501, // Source: Heliostat ], }, heresy: { sourceHashes: [ 21494224, // Source: Offer the correct final answer in an uncharted space. 745481267, // Source: Intrinsic Iteration Triumph 1341921330, // Source: Episode: Heresy Activities 1792957897, // Source: "Efficient Challenger" Triumph 2607970476, // Source: Sundered Doctrine 2869564842, // Source: "Vengeful Knife" Triumph 3310034131, // Source: "Crossed Blades" Triumph 3358334503, // Source: "Boon Ghost Mod Collector" Triumph 3507911332, // Source: Episode: Heresy ], }, ikora: { sourceHashes: [ 3075817319, // Source: Earn rank-up packages from Ikora Rey. ], }, intothelight: { itemHashes: [ 205225492, // Hung Jury SR4 211732170, // Hammerhead 243425374, // Falling Guillotine 570866107, // Succession 2228325504, // Edge Transit 2480074702, // Forbearance 2499720827, // Midnight Coup 2533990645, // Blast Furnace 3098328572, // The Recluse 3757612024, // Luna's Howl 3851176026, // Elsie's Rifle 4043921923, // The Mountaintop ], sourceHashes: [ 1388323447, // Source: Exotic Mission "The Whisper" 1902517582, // Source: Where's Archie? 2068312112, // Source: Exotic Mission "Zero Hour" 2952071500, // Source: Into the Light ], aliases: ['itl'], }, io: { sourceHashes: [ 315474873, // Source: Complete activities and earn rank-up packages on Io. 1067250718, // Source: Adventure "Arecibo" on Io 1832642406, // Source: World Quest "Dynasty" on Io. 2392127416, // Source: Adventure "Cliffhanger" on Io 2717017239, // Source: Complete Nightfall strike "The Pyramidion." 3427537854, // Source: Adventure "Road Rage" on Io ], enteredDCV: 20, }, ironbanner: { itemHashes: [ 231533811, // Iron Strength 1162929425, // The Golden Standard 1448664466, // Iron Bone 1448664467, // Iron Gold 1661191199, // Grizzled Wolf 1987234560, // Iron Ruby 2448092902, // Rusted Iron ], sourceHashes: [ 561111210, // Source: Iron Banner Salvager's Salvo Armament 1027607603, // Source: Associated Iron Banner Quest 1312894505, // Source: Iron Banner 1828622510, // Source: Chance to acquire when you win Iron Banner matches. 1926923633, // Source: Lord Saladin Rank Up Reputation 2520862847, // Source: Iron Banner Iron-Handed Diplomacy 2648408612, // Acquired by competing in the Iron Banner when the wolves were loud. 3072862693, // Source: Complete Iron Banner matches and earn rank-up packages from Lord Saladin. ], }, kepler: { sourceHashes: [ 4284811963, // Source: Exploring Kepler ], }, kingsfall: { sourceHashes: [ 160129377, // Source: "King's Fall" Raid ], aliases: ['kf'], }, lastwish: { itemHashes: [ 70083888, // Nation of Beasts 424291879, // Age-Old Bond 501329015, // Chattering Bone 1851777734, // Apex Predator 2884596447, // The Supremacy 3388655311, // Tyranny of Heaven 3591141932, // Techeun Force 3668669364, // Dreaming Spectrum 3885259140, // Transfiguration ], sourceHashes: [ 2455011338, // Source: Last Wish raid. ], aliases: ['lw'], }, legendaryengram: { sourceHashes: [ 3334812276, // Source: Open Legendary engrams and earn faction rank-up packages. ], }, leviathan: { itemHashes: [ 3580904580, // Legend of Acrius ], sourceHashes: [ 2653618435, // Source: Leviathan raid. 2765304727, // Source: Leviathan raid on Prestige difficulty. 4009509410, // Source: Complete challenges in the Leviathan raid. ], enteredDCV: 20, }, lost: { sourceHashes: [ 164083100, // Source: Display of Supremacy, Weekly Challenge 3094114967, // Source: Season of the Lost Ritual Playlists ], enteredDCV: 20, }, lostsectors: { sourceHashes: [ 2203185162, // Source: Solo Expert and Master Lost Sectors ], }, mars: { sourceHashes: [ 1036506031, // Source: Complete activities and earn rank-up packages on Mars. 1299614150, // Source: [REDACTED] on Mars. 1924238751, // Source: Complete Nightfall strike "Will of the Thousands." 2310754348, // Source: World Quest "Data Recovery" on Mars. 2926805810, // Source: Complete Nightfall strike "Strange Terrain." 4137108180, // Source: Escalation Protocol on Mars. ], enteredDCV: 20, }, menagerie: { itemHashes: [ 1661191194, // A Hall of Delights 1661191195, // The Imperial Menagerie 3176509806, // Árma Mákhēs 3841416152, // Golden Empire 3841416153, // Goldleaf 3841416154, // Shadow Gilt 3841416155, // Cinderchar 3875444086, // The Emperor's Chosen ], sourceHashes: [ 2511152325, // Acquired from the Menagerie aboard the Leviathan. ], enteredDCV: 20, }, mercury: { sourceHashes: [ 148542898, // Source: Equip the full Mercury destination set on a Warlock. 1175566043, // Source: Complete Nightfall strike "A Garden World." 1400219831, // Source: Equip the full Mercury destination set on a Hunter. 1411886787, // Source: Equip the full Mercury destination set on a Titan. 1581680964, // Source: Complete Nightfall strike "Tree of Probabilities." 1618754228, // Source: Sundial Activity on Mercury 1654120320, // Source: Complete activities and earn rank-up packages on Mercury. 2487203690, // Source: Complete Nightfall strike "Tree of Probabilities." 3079246067, // Source: Complete Osiris' Lost Prophecies for Brother Vance on Mercury. 3964663093, // Source: Rare drop from high-scoring Nightfall strikes on Mercury. 4263201695, // Source: Complete Nightfall strike "A Garden World." ], enteredDCV: 20, }, moon: { sourceHashes: [ 1253026984, // Source: Among the lost Ghosts of the Moon. 1999000205, // Source: Exploring the Moon 3589340943, // Source: Altars of Sorrow ], }, neomuna: { itemHashes: [ 1123421440, // Epochal Integration 1311684613, // Dimensional Hypotrochoid 3635821806, // Phyllotactic Spiral 3920310144, // Volta Bracket ], sourceHashes: [ 281362298, // Source: Strider Exotic Quest 454251931, // Source: "What Remains" Exotic Quest 483798855, // Source: "The Final Strand" Exotic Quest 1750523507, // Source: Terminal Overload (Ahimsa Park) 2697389955, // Source: "Neomuna Sightseeing" Triumph 3041847664, // Source: Exploring Neomuna 3773376290, // Source: Terminal Overload (Zephyr Concourse) 4006434081, // Source: Terminal Overload 4110186790, // Source: Terminal Overload (Límíng Harbor) ], }, nessus: { sourceHashes: [ 164571094, // Source: World Quest "Exodus Black" on Nessus. 817015032, // Source: Complete Nightfall strike "The Inverted Spire." 1186140085, // Source: Adventure "Unbreakable" on Nessus 1289998337, // Source: Adventure "Hack the Planet" on Nessus 1906492169, // Source: Complete activities and earn rank-up packages on Nessus. 2040548068, // Source: Adventure "Release" on Nessus 2345202459, // Source: Adventure "Invitation from the Emperor" on Nessus 2553369674, // Source: Adventure "Exodus Siege" on Nessus 3022766747, // Source: Complete Nightfall strike "The Insight Terminus." 3067146211, // Source: Complete Nightfall strike "Exodus Crash." ], }, nightfall: { itemHashes: [ 42874240, // Uzume RR4 192784503, // Pre Astyanax IV 213264394, // Buzzard 233635202, // Cruel Mercy 267089201, // Warden's Law (Adept) 496556698, // Pre Astyanax IV (Adept) 555148853, // Wendigo GL3 (Adept) 566740455, // THE SWARM (Adept) 672957262, // Undercurrent (Adept) 772231794, // Hung Jury SR4 817909300, // Undercurrent (Adept) 912222548, // Soldier On 927835311, // Buzzard (Adept) 959037361, // Wild Style (Adept) 1056103557, // Shadow Price (Adept) 1064132738, // BrayTech Osprey (Adept) 1151688091, // Undercurrent 1332123064, // Wild Style 1354727549, // The Slammer (Adept) 1492522228, // Scintillation (Adept) 1586231351, // Mindbender's Ambition 1821529912, // Warden's Law 1854753404, // Wendigo GL3 1854753405, // The Militia's Birthright 1891996599, // Uzume RR4 (Adept) 1987790789, // After the Nightfall 2063217087, // Pre Astyanax IV (Adept) 2074041946, // Mindbender's Ambition (Adept) 2152484073, // Warden's Law 2298039571, // Rake Angle 2322926844, // Shadow Price 2347178967, // Cruel Mercy (Adept) 2450917538, // Uzume RR4 2591257541, // Scintillation 2697143634, // Lotus-Eater (Adept) 2759590322, // THE SWARM 2876244791, // The Palindrome 2883684343, // Hung Jury SR4 (Adept) 2889501828, // The Slammer 2914913838, // Loaded Question (Adept) 2932922810, // Pre Astyanax IV 3106557243, // PLUG ONE.1 (Adept) 3125454907, // Loaded Question 3183283212, // Wendigo GL3 3250744600, // Warden's Law (Adept) 3293524502, // PLUG ONE.1 3610521673, // Uzume RR4 (Adept) 3667553455, // BrayTech Osprey 3686538757, // Undercurrent 3832743906, // Hung Jury SR4 3915197957, // Wendigo GL3 (Adept) 3922217119, // Lotus-Eater 3997086838, // Rake Angle (Adept) 4074251943, // Hung Jury SR4 (Adept) 4077588826, // The Palindrome (Adept) 4162642204, // The Militia's Birthright (Adept) ], sourceHashes: [ 110159004, // Source: Complete Nightfall strike "Warden of Nothing." 277706045, // Source: Season of the Splicer Nightfall Grandmaster 354493557, // Source: Complete Nightfall strike "Savathûn's Song." 817015032, // Source: Complete Nightfall strike "The Inverted Spire." 827839814, // Source: Flawless Chest in Trials of Osiris or Grandmaster Nightfalls 860666126, // Source: Nightfall 1175566043, // Source: Complete Nightfall strike "A Garden World." 1283862526, // Source: Season of the Haunted Nightfall Grandmaster 1516560855, // Source: Season of the Seraph Grandmaster Nightfall 1581680964, // Source: Complete Nightfall strike "Tree of Probabilities." 1596489410, // Source: Season of the Risen Nightfall Grandmaster 1618699950, // Source: Season of the Lost Nightfall Grandmaster 1749037998, // Source: Nightfall 1850609592, // Source: Nightfall 1924238751, // Source: Complete Nightfall strike "Will of the Thousands." 1992319882, // Source: Grandmaster Nightfalls 2347293565, // Source: Complete Nightfall strike "The Arms Dealer." 2376909801, // Source: "Master" Triumph in Nightfalls 2487203690, // Source: Complete Nightfall strike "Tree of Probabilities." 2717017239, // Source: Complete Nightfall strike "The Pyramidion." 2805208672, // Source: Complete Nightfall strike "The Hollowed Lair." 2851783112, // Source: Complete Nightfall strike "Lake of Shadows." 2926805810, // Source: Complete Nightfall strike "Strange Terrain." 2982642634, // Source: Season of Plunder Grandmaster Nightfall 3022766747, // Source: Complete Nightfall strike "The Insight Terminus." 3067146211, // Source: Complete Nightfall strike "Exodus Crash." 3142874552, // Source: Nightfall 3229688794, // Source: Grandmaster Nightfall 3528789901, // Source: Season of the Chosen Nightfall Grandmaster 3874934421, // Source: Complete Nightfall strike "The Corrupted." 3964663093, // Source: Rare drop from high-scoring Nightfall strikes on Mercury. 4208190159, // Source: Complete a Nightfall strike. 4263201695, // Source: Complete Nightfall strike "A Garden World." ], }, nightmare: { sourceHashes: [ 550270332, // Source: Complete all Nightmare Hunt time trials on Master difficulty. 2778435282, // Source: Nightmare Hunts ], }, nm: { sourceHashes: [ 1464399708, // Source: Earn rank-up packages from Executor Hideo. ], enteredDCV: 20, }, paleheart: { sourceHashes: [ 941123623, // Pale Heart - Cayde's Stash 2327253880, // Source: Exploring the Pale Heart 3614199681, // Source: Pale Heart Triumph ], }, 'pinnacle-weapon': { itemHashes: [ 444627789, // Oxygen SR3 578459533, // Wendigo GL3 654608616, // Revoker 1050806815, // The Recluse 1584643826, // Hush 1600633250, // 21% Delirium 3098328572, // The Recluse 3354242550, // The Recluse 3907337522, // Oxygen SR3 3962575203, // Hush 4104613038, // Oxygen SR3 ], sourceHashes: [ 598662729, // Source: Reach Glory Rank "Legend" in the Crucible. 1162859311, // Source: Complete the "Clean Getaway" quest. 1244908294, // Source: Complete the "Loaded Question" quest from Zavala. 2317365255, // Source: Complete the "A Loud Racket" quest. 2883838366, // Source: Complete the "Breakneck" quest from the Drifter. ], }, pinnacleops: { sourceHashes: [ 1232061833, // Source: Pinnacle Ops ], }, pit: { sourceHashes: [ 1745960977, // Source: "Pit of Heresy" Dungeon ], }, plunder: { itemHashes: [ 820890091, // Planck's Stride 1298815317, // Brigand's Law ], sourceHashes: [ 790152021, // Source: Season of Plunder Triumph 2982642634, // Source: Season of Plunder Grandmaster Nightfall 3265560237, // Source: Cryptic Quatrains III 3308438907, // Source: Season of Plunder 3740731576, // Source: "A Rising Tide" Mission 4199401779, // Source: Season of Plunder Activities ], enteredDCV: 20, }, presage: { sourceHashes: [ 210885364, // Source: Flawless "Presage" Exotic Quest on Master Difficulty 2745272818, // Source: "Presage" Exotic Quest 3597879858, // Source: "Presage" Exotic Quest ], }, prestige: { sourceHashes: [ 2765304727, // Source: Leviathan raid on Prestige difficulty. 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. 4066007318, // Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty. ], enteredDCV: 20, }, prophecy: { sourceHashes: [ 506073192, // Source: "Prophecy" Dungeon ], }, psiops: { itemHashes: [ 2097055732, // Piece of Mind 4067556514, // Thoughtless ], sourceHashes: [ 450719423, // Source: Season of the Risen 2075569025, // PsiOps 2363489105, // Source: Season of the Risen Vendor or Triumphs 3563833902, // Source: Season of the Risen Triumphs ], enteredDCV: 24, }, rahool: { sourceHashes: [ 4011186136, // Exotic Armor Focusing ], }, raid: { sourceHashes: [ 160129377, // Source: "King's Fall" Raid 557146120, // Source: Complete a Guided Game as a guide or seeker. 596084342, // Source: "The Desert Perpetual" Raid 654652973, // Guide 25 Last Wish encounters 707740602, // Guide 10 Last Wish encounters 866530798, // Source: "Not a Scratch" Triumph 1007078046, // Source: "Vow of the Disciple" Raid 1405897559, // Source: "Deep Stone Crypt" Raid 1483048674, // Source: Complete the "Scourge of the Past" raid. 1491707941, // Source: "Garden of Salvation" Raid 1675483099, // Source: Leviathan, Spire of Stars raid lair. 1692165595, // Source: "Rock Bottom" Triumph 1897187034, // Source: "Crota's End" Raid 2065138144, // Source: "Vault of Glass" Raid 2085016678, // Source: Complete the "Scourge of the Past" raid within the first 24 hours after its launch. 2127551856, // Source: "The Desert Perpetual" Epic Raid 2399751101, // Acquired from the raid "Crown of Sorrow." 2455011338, // Source: Last Wish raid. 2653618435, // Source: Leviathan raid. 2700267533, // Source: "Salvation's Edge" Raid 2723305286, // Source: Raid Ring Promotional Event 2765304727, // Source: Leviathan raid on Prestige difficulty. 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. 2882367429, // Source: Eververse\nComplete the "Vault of Glass" raid to unlock this in Eververse. 2937902448, // Source: Leviathan, Eater of Worlds raid lair. 3098906085, // Source: Complete a Guided Game raid as a guide. 3147603678, // Acquired from the raid "Crown of Sorrow." 3190710249, // Source: "Root of Nightmares" Raid 3390269646, // Source: Guided Games Final Encounters 3807243511, // Source: Raid Chests 4009509410, // Source: Complete challenges in the Leviathan raid. 4066007318, // Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty. 4246883461, // Source: Found in the "Scourge of the Past" raid. ], itemHashes: [ 70083888, // Nation of Beasts 424291879, // Age-Old Bond 501329015, // Chattering Bone 947448544, // Shadow of Earth Shell 1661191193, // Crown of Sorrow 1851777734, // Apex Predator 2027598066, // Imperial Opulence 2027598067, // Imperial Dress 2557722678, // Midnight Smith 2884596447, // The Supremacy 3388655311, // Tyranny of Heaven 3580904580, // Legend of Acrius 3591141932, // Techeun Force 3668669364, // Dreaming Spectrum 3885259140, // Transfiguration 4103414242, // Divinity ], }, rasputin: { sourceHashes: [ 504657809, // Source: Season of the Seraph Activities 1126234343, // Source: Witness Rasputin's Full Power 1497107113, // Source: Seasonal Quest, "Seraph Warsat Network" 1516560855, // Source: Season of the Seraph Grandmaster Nightfall 2230358252, // Source: End-of-Season Event 2422551147, // Source: "Operation Seraph's Shield" Mission 3492941398, // Source: "The Lie" Quest 3567813252, // Source: Season of the Seraph Triumph 3574140916, // Source: Season of the Seraph 3937492340, // Source: Seraph Bounties ], enteredDCV: 20, }, reclaim: { sourceHashes: [ 2929839827, // Source: Reclaim ], }, renegades: { sourceHashes: [ 178383754, // Source: Renegades ], }, revenant: { sourceHashes: [ 792439255, // Source: Tonic Laboratory in the Last City 1605890568, // Source: Episode Revenant Seasonal Activities 2463956052, // Source: Vesper's Host 3906217258, // Source: Revenant Fortress ], }, riteofthenine: { itemHashes: [ 14929251, // Long Arm 492673102, // New Pacific Epitaph 749483159, // Prosecutor (Adept) 1050582210, // Greasy Luck (Adept) 1066598837, // Relentless (Adept) 1157220231, // No Survivors (Adept) 1460079227, // Liminal Vigil 1685406703, // Greasy Luck 1773934241, // Judgment 1904170910, // A Sudden Death 1987644603, // Judgment (Adept) 2126543269, // Cold Comfort (Adept) 2129814338, // Prosecutor 2477408004, // Wilderflight (Adept) 2730671571, // Terminus Horizon 2760833884, // Cold Comfort 2764074355, // A Sudden Death (Adept) 2982006965, // Wilderflight 3185151619, // New Pacific Epitaph (Adept) 3329218848, // Judgment (Adept) 3421639790, // Liminal Vigil (Adept) 3681280908, // Relentless 3692140710, // Long Arm (Adept) 4193602194, // No Survivors 4267192886, // Terminus Horizon (Adept) ], sourceHashes: [ 877404349, // Source: Rite of the Nine ], aliases: ['rotn'], }, 'ritual-weapon': { itemHashes: [ 805677041, // Buzzard 838556752, // Python 847329160, // Edgewise 1179141605, // Felwinter's Lie 1644680957, // Null Composure 2060863616, // Salvager's Salvo 2697058914, // Komodo-4FR 3001205424, // Ecliptic Distaff 3434944005, // Point of the Stag 3535742959, // Randy's Throwing Knife 4184808992, // Adored 4227181568, // Exit Strategy ], sourceHashes: [ 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit ], }, rivenslair: { itemHashes: [ 2563668388, // Scalar Potential 4153087276, // Appetence ], sourceHashes: [ 561126969, // Source: "Starcrossed" Mission 1664308183, // Source: Season of the Wish Activities 4278841194, // Source: Season of the Wish Triumphs ], aliases: ['coil'], enteredDCV: 24, }, rootofnightmares: { sourceHashes: [ 3190710249, // Source: "Root of Nightmares" Raid ], aliases: ['root', 'ron'], }, saint14: { sourceHashes: [ 2607739079, // Source: A Matter of Time 3404977524, // Source: Contribute to the Empyrean Restoration Effort 4046490681, // Source: Complete the "Global Resonance" Triumph 4267157320, // Source: ??????? ], enteredDCV: 20, }, salvationsedge: { sourceHashes: [ 2700267533, // Source: "Salvation's Edge" Raid ], }, scourgeofthepast: { itemHashes: [ 2557722678, // Midnight Smith ], sourceHashes: [ 1483048674, // Source: Complete the "Scourge of the Past" raid. 2085016678, // Source: Complete the "Scourge of the Past" raid within the first 24 hours after its launch. 4246883461, // Source: Found in the "Scourge of the Past" raid. ], aliases: ['scourge', 'sotp'], enteredDCV: 20, }, seasonpass: { sourceHashes: [ 333761108, // Source: Rewards Pass 450719423, // Source: Season of the Risen 794422188, // Source: Season of the Witch 813075729, // Source: Season of the Deep Vendor Reputation Reward 927967626, // Source: Season of the Deep 1560428737, // Source: Season of Defiance 1593696611, // Source: Season Pass Reward 1763998430, // Source: Season Pass 1838401392, // Source: Earned as a Season Pass reward. 2257836668, // Source: Season of the Deep Fishing 2379344669, // Source: Season Pass 2676881949, // Source: Season of the Haunted 2986594962, // Source: Season of the Wish 3308438907, // Source: Season of Plunder 3574140916, // Source: Season of the Seraph ], }, servitor: { itemHashes: [ 599895591, // Sojourner's Tale 2130875369, // Sojourner's Tale 2434225986, // Shattered Cipher ], sourceHashes: [ 139160732, // Source: Season of the Splicer 277706045, // Source: Season of the Splicer Nightfall Grandmaster 1600754038, // Source: Season of the Splicer Activities 2040801502, // Source: Season of the Splicer Triumph 2694738712, // Source: Season of the Splicer Quest 2967385539, // Source: Season of the Splicer Seasonal Challenges ], enteredDCV: 20, }, shatteredthrone: { itemHashes: [ 185321778, // The Eternal Return 814876684, // Wish-Ender 2844014413, // Pallas Galliot ], }, shipwright: { sourceHashes: [ 96303009, // Source: Purchased from Amanda Holliday. ], enteredDCV: 20, }, sonar: { itemHashes: [ 1081724548, // Rapacious Appetite 1769847435, // A Distant Pull 3016891299, // Different Times 3890055324, // Targeted Redaction 4066778670, // Thin Precipice ], sourceHashes: [ 813075729, // Source: Season of the Deep Vendor Reputation Reward 927967626, // Source: Season of the Deep 2257836668, // Source: Season of the Deep Fishing 2671038131, // Season of the Deep - WEAPONS 2755511565, // Source: Season of the Deep Triumph 2811716495, // Source: Season of the Deep Activities 2959452483, // Season of the Deep - WEAPONS ], enteredDCV: 24, }, spireofstars: { sourceHashes: [ 1675483099, // Source: Leviathan, Spire of Stars raid lair. 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. ], aliases: ['sos'], enteredDCV: 20, }, spireofthewatcher: { sourceHashes: [ 1597738585, // Source: "Spire of the Watcher" Dungeon ], aliases: ['sotw', 'watcher'], }, strikes: { itemHashes: [ 42874240, // Uzume RR4 192784503, // Pre Astyanax IV 213264394, // Buzzard 233635202, // Cruel Mercy 274843196, // Vanguard Unyielding 772231794, // Hung Jury SR4 781498181, // Persuader 1151688091, // Undercurrent 1296429091, // Deadpan Delivery 1332123064, // Wild Style 1661191186, // Disdain for Gold 1821529912, // Warden's Law 1854753404, // Wendigo GL3 1854753405, // The Militia's Birthright 1974641289, // Nightshade 1999754402, // The Showrunner 2152484073, // Warden's Law 2298039571, // Rake Angle 2322926844, // Shadow Price 2450917538, // Uzume RR4 2523776412, // Vanguard Burnished Steel 2523776413, // Vanguard Steel 2588647361, // Consequence of Duty 2591257541, // Scintillation 2759590322, // THE SWARM 2788911997, // Vanguard Divide 2788911998, // Vanguard Metallic 2788911999, // Vanguard Veteran 2876244791, // The Palindrome 2889501828, // The Slammer 2932922810, // Pre Astyanax IV 3001205424, // Ecliptic Distaff 3125454907, // Loaded Question 3183283212, // Wendigo GL3 3215252549, // Determination 3293524502, // PLUG ONE.1 3667553455, // BrayTech Osprey 3686538757, // Undercurrent 3832743906, // Hung Jury SR4 3922217119, // Lotus-Eater 4060882458, // Balistraria Wrap (Ornament) ], sourceHashes: [ 288436121, // Source: Associated Vanguard Quest 351235593, // Source: Eliminate Prison of Elders escapees found in strikes. 412991783, // Source: Strikes 539840256, // Source: Associated Vanguard Quest 1144274899, // Source: Complete this weapon's associated Vanguard quest. 1216155659, // Source: Complete the "Season 8: First Watch" quest. 1244908294, // Source: Complete the "Loaded Question" quest from Zavala. 1433518193, // Source: Vanguard Salvager's Salvo Armament Quest 1465057711, // Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit) 1564061133, // Source: Associated Vanguard Quest 2124937714, // Source: Zavala Rank Up Reputation 2317365255, // Source: Complete the "A Loud Racket" quest. 2335095658, // Source: Strikes 2527168932, // Source: Complete strikes and earn rank-up packages from Commander Zavala. 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit ], aliases: ['zavala'], }, sundereddoctrine: { itemHashes: [ 1303313141, // Unsworn ], sourceHashes: [ 2607970476, // Source: Sundered Doctrine ], aliases: ['sundered'], }, sundial: { sourceHashes: [ 1618754228, // Source: Sundial Activity on Mercury 2627087475, // Source: Obelisk Bounties and Resonance Rank Increases Across the System ], enteredDCV: 20, }, tangled: { itemHashes: [ 1226584228, // Tangled Rust 1226584229, // Tangled Bronze 4085986809, // Secret Treasure ], sourceHashes: [ 110159004, // Source: Complete Nightfall strike "Warden of Nothing." 798957490, // Source: Complete wanted escapee bounties for the Spider. 1771326504, // Source: Complete activities and earn rank-up packages on the Tangled Shore. 2805208672, // Source: Complete Nightfall strike "The Hollowed Lair." 4140654910, // Source: Eliminate all Barons on the Tangled Shore. ], enteredDCV: 20, }, throneworld: { itemHashes: [ 2721157927, // Tarnation ], sourceHashes: [ 1141831282, // Source: "Of Queens and Worms" Exotic Quest 1823766625, // Source: "Vox Obscura" Exotic Quest 3954922099, // Source: Exploring the Throne World ], }, titan: { sourceHashes: [ 194661944, // Source: Adventure "Siren Song" on Saturn's Moon, Titan 354493557, // Source: Complete Nightfall strike "Savathûn's Song." 482012099, // Source: Adventure "Thief of Thieves" on Saturn's Moon, Titan 636474187, // Source: Adventure "Deathless" on Saturn's Moon, Titan 3534706087, // Source: Complete activities and earn rank-up packages on Saturn's Moon, Titan. ], enteredDCV: 20, }, trials: { sourceHashes: [ 139599745, // Source: Earn seven wins on a single Trials ticket. 443793689, // Source: Win games on a completed Lighthouse Passage after earning a weekly win streak of five or higher. 486819617, // Trials of Osiris - WEAPONS 613791463, // Source: Saint-14 Rank Up Reputation 752988954, // Source: Flawless Chest in Trials of Osiris 827839814, // Source: Flawless Chest in Trials of Osiris or Grandmaster Nightfalls 1218637862, // Source: Open the Lighthouse chest after earning a weekly win streak of five or higher. 1607607347, // Source: Complete Trials tickets and earn rank-up packages from the Emissary of the Nine. 1923289424, // Source: Open the Lighthouse chest. 2857787138, // Source: Trials of Osiris 3390015730, // Source: Trials of Osiris Challenges 3471208558, // Source: Trials of Osiris Wins 3543690049, // Source: Complete a flawless Trials ticket. 3564069447, // Source: Flawless with a "Flight of the Pigeon" medal for each win ], }, umbral: { sourceHashes: [ 287889699, // Source: Umbral Engram Tutorial 1286883820, // Source: Prismatic Recaster ], enteredDCV: 20, }, vaultofglass: { sourceHashes: [ 2065138144, // Source: "Vault of Glass" Raid ], aliases: ['vog'], }, vespershost: { sourceHashes: [ 2463956052, // Source: Vesper's Host ], aliases: ['vesper'], }, vexoffensive: { itemHashes: [ 351285766, // Substitutional Alloy Greaves 377757362, // Substitutional Alloy Hood 509561140, // Substitutional Alloy Gloves 509561142, // Substitutional Alloy Gloves 509561143, // Substitutional Alloy Gloves 695795213, // Substitutional Alloy Helm 844110491, // Substitutional Alloy Gloves 1137424312, // Substitutional Alloy Cloak 1137424314, // Substitutional Alloy Cloak 1137424315, // Substitutional Alloy Cloak 1348357884, // Substitutional Alloy Gauntlets 1584183805, // Substitutional Alloy Cloak 1721943440, // Substitutional Alloy Boots 1721943441, // Substitutional Alloy Boots 1721943442, // Substitutional Alloy Boots 1855720513, // Substitutional Alloy Vest 1855720514, // Substitutional Alloy Vest 1855720515, // Substitutional Alloy Vest 2096778461, // Substitutional Alloy Strides 2096778462, // Substitutional Alloy Strides 2096778463, // Substitutional Alloy Strides 2468603405, // Substitutional Alloy Plate 2468603406, // Substitutional Alloy Plate 2468603407, // Substitutional Alloy Plate 2657028416, // Substitutional Alloy Vest 2687273800, // Substitutional Alloy Grips 2690973101, // Substitutional Alloy Hood 2690973102, // Substitutional Alloy Hood 2690973103, // Substitutional Alloy Hood 2742760292, // Substitutional Alloy Plate 2761292744, // Substitutional Alloy Bond 2815379657, // Substitutional Alloy Bond 2815379658, // Substitutional Alloy Bond 2815379659, // Substitutional Alloy Bond 2903026872, // Substitutional Alloy Helm 2903026873, // Substitutional Alloy Helm 2903026874, // Substitutional Alloy Helm 2942269704, // Substitutional Alloy Gauntlets 2942269705, // Substitutional Alloy Gauntlets 2942269707, // Substitutional Alloy Gauntlets 3166926328, // Substitutional Alloy Robes 3166926330, // Substitutional Alloy Robes 3166926331, // Substitutional Alloy Robes 3192738009, // Substitutional Alloy Greaves 3192738010, // Substitutional Alloy Greaves 3192738011, // Substitutional Alloy Greaves 3364258850, // Substitutional Alloy Strides 3680920565, // Substitutional Alloy Robes 3757338780, // Substitutional Alloy Mark 3757338782, // Substitutional Alloy Mark 3757338783, // Substitutional Alloy Mark 3911047865, // Substitutional Alloy Mark 4013678605, // Substitutional Alloy Boots 4026120124, // Substitutional Alloy Grips 4026120125, // Substitutional Alloy Grips 4026120127, // Substitutional Alloy Grips 4070722289, // Substitutional Alloy Mask 4078925540, // Substitutional Alloy Mask 4078925541, // Substitutional Alloy Mask 4078925542, // Substitutional Alloy Mask ], sourceHashes: [ 4122810030, // Source: Complete seasonal activities during Season of the Undying. ], enteredDCV: 20, }, vowofthedisciple: { sourceHashes: [ 1007078046, // Source: "Vow of the Disciple" Raid ], aliases: ['vow', 'votd'], }, warlordsruin: { sourceHashes: [ 613435025, // Source: "Warlord's Ruin" Dungeon ], }, wartable: { sourceHashes: [ 2653840925, // Source: Challenger's Proving VII Quest 3818317874, // Source: War Table Reputation Reset 4079816474, // Source: War Table ], enteredDCV: 24, }, wellspring: { sourceHashes: [ 82267399, // Source: "Warden of the Spring" Triumph 502279466, // Source: Wellspring Boss Vezuul, Lightflayer 2917218318, // Source: Wellspring Boss Golmag, Warden of the Spring 3359853911, // Source: Wellspring Boss Zeerik, Lightflayer 3411812408, // Source: "All the Spring's Riches" Triumph 3450213291, // Source: Wellspring Boss Bor'gong, Warden of the Spring ], }, wrathborn: { itemHashes: [ 197764097, // Wild Hunt Boots 238284968, // Wild Hunt Strides 251310542, // Wild Hunt Hood 317220729, // Wild Hunt Vestment 1148770067, // Wild Hunt Cloak 1276513983, // Wild Hunt Gloves 1458739906, // Wild Hunt Vest 2025716654, // Wild Hunt Grasps 2055947316, // Wild Hunt Bond 2279193565, // Wild Hunt Mark 2453357042, // Blast Battue 2545401128, // Wild Hunt Gauntlets 2776503072, // Royal Chase 3180809346, // Wild Hunt Greaves 3351935136, // Wild Hunt Plate 3887272785, // Wild Hunt Helm 4079117607, // Wild Hunt Mask ], sourceHashes: [ 841568343, // Source: "Hunt for the Wrathborn" Quest 3107094548, // Source: "Coup de Grâce" Mission ], enteredDCV: 20, }, }; export default D2Sources; ================================================ FILE: src/data/d2/source-info.ts ================================================ const D2Sources: { [key: string]: { itemHashes: number[]; sourceHashes: number[]; searchString: string[] }; } = { '30th': { itemHashes: [], sourceHashes: [ 443340273, // Source: Xûr's Treasure Hoard in Eternity 675740011, // Source: "Grasp of Avarice" Dungeon 1102533392, // Source: Xûr (Eternity) 1394793197, // Source: "Magnum Opus" Quest 2763252588, // Source: "And Out Fly the Wolves" Quest ], searchString: [], }, ada: { itemHashes: [ 417164956, // Jötunn 3211806999, // Izanagi's Burden 3588934839, // Le Monarque 3650581584, // New Age Black Armory 3650581585, // Refurbished Black Armory 3650581586, // Rasmussen Clan 3650581587, // House of Meyrin 3650581588, // Satou Tribe 3650581589, // Bergusian Night ], sourceHashes: [ 266896577, // Source: Solve the Norse glyph puzzle. 439994003, // Source: Complete the "Master Smith" Triumph. 925197669, // Source: Complete a Bergusia Forge ignition. 948753311, // Source: Found by completing Volundr Forge ignitions. 1286332045, // Source: Found by completing Izanami Forge ignitions. 1457456824, // Source: Complete the "Reunited Siblings" Triumph. 1465990789, // Source: Solve the Japanese glyph puzzle. 1596507419, // Source: Complete a Gofannon Forge ignition. 2062058385, // Source: Crafted in a Black Armory forge. 2384327872, // Source: Solve the French glyph puzzle. 2541753910, // Source: Complete the "Master Blaster" Triumph. 2966694626, // Source: Found by solving the mysteries behind the Black Armory's founding families. 3047033583, // Source: Returned the Obsidian Accelerator. 3257722699, // Source: Complete the "Clean Up on Aisle Five" Triumph. 3390164851, // Source: Found by turning in Black Armory bounties. 3764925750, // Source: Complete an Izanami Forge ignition. 4101102010, // Source: Found by completing Bergusia Forge ignitions. 4247521481, // Source: Complete the "Beautiful but Deadly" Triumph. 4290227252, // Source: Complete a Volundr Forge ignition. ], searchString: [], }, adventure: { itemHashes: [], sourceHashes: [ 194661944, // Source: Adventure "Siren Song" on Saturn's Moon, Titan 482012099, // Source: Adventure "Thief of Thieves" on Saturn's Moon, Titan 636474187, // Source: Adventure "Deathless" on Saturn's Moon, Titan 783399508, // Source: Adventure "Supply and Demand" in the European Dead Zone 790433146, // Source: Adventure "Dark Alliance" in the European Dead Zone 1067250718, // Source: Adventure "Arecibo" on Io 1186140085, // Source: Adventure "Unbreakable" on Nessus 1289998337, // Source: Adventure "Hack the Planet" on Nessus 1527887247, // Source: Adventure "Red Legion, Black Oil" in the European Dead Zone 1736997121, // Source: Adventure "Stop and Go" in the European Dead Zone 1861838843, // Source: Adventure "A Frame Job" in the European Dead Zone 2040548068, // Source: Adventure "Release" on Nessus 2096915131, // Source: Adventure "Poor Reception" in the European Dead Zone 2345202459, // Source: Adventure "Invitation from the Emperor" on Nessus 2392127416, // Source: Adventure "Cliffhanger" on Io 2553369674, // Source: Adventure "Exodus Siege" on Nessus 3427537854, // Source: Adventure "Road Rage" on Io 3754173885, // Source: Adventure "Getting Your Hands Dirty" in the European Dead Zone 4214471686, // Source: Adventure "Unsafe at Any Speed" in the European Dead Zone ], searchString: [], }, avalon: { itemHashes: [], sourceHashes: [ 709680645, // Source: "Truly Satisfactory" Triumph 1476475066, // Source: "Firmware Update" Triumph 1730197643, // Source: //node.ovrd.AVALON// Exotic Quest ], searchString: [], }, banshee: { itemHashes: [], sourceHashes: [ 1459595344, // Source: Purchase from Banshee-44 or Ada-1 1788267693, // Source: Earn rank-up packages from Banshee-44. 2986841134, // Source: Salvager's Salvo Armament Quest 3512613235, // Source: "A Sacred Fusion" Quest ], searchString: [], }, battlegrounds: { itemHashes: [ 2121785039, // Brass Attacks 3075224551, // Threaded Needle ], sourceHashes: [ 3391325445, // Source: Battlegrounds ], searchString: [], }, blackarmory: { itemHashes: [ 417164956, // Jötunn 3211806999, // Izanagi's Burden 3588934839, // Le Monarque 3650581584, // New Age Black Armory 3650581585, // Refurbished Black Armory 3650581586, // Rasmussen Clan 3650581587, // House of Meyrin 3650581588, // Satou Tribe 3650581589, // Bergusian Night ], sourceHashes: [ 266896577, // Source: Solve the Norse glyph puzzle. 439994003, // Source: Complete the "Master Smith" Triumph. 925197669, // Source: Complete a Bergusia Forge ignition. 948753311, // Source: Found by completing Volundr Forge ignitions. 1286332045, // Source: Found by completing Izanami Forge ignitions. 1457456824, // Source: Complete the "Reunited Siblings" Triumph. 1465990789, // Source: Solve the Japanese glyph puzzle. 1596507419, // Source: Complete a Gofannon Forge ignition. 2062058385, // Source: Crafted in a Black Armory forge. 2384327872, // Source: Solve the French glyph puzzle. 2541753910, // Source: Complete the "Master Blaster" Triumph. 2966694626, // Source: Found by solving the mysteries behind the Black Armory's founding families. 3047033583, // Source: Returned the Obsidian Accelerator. 3257722699, // Source: Complete the "Clean Up on Aisle Five" Triumph. 3390164851, // Source: Found by turning in Black Armory bounties. 3764925750, // Source: Complete an Izanami Forge ignition. 4101102010, // Source: Found by completing Bergusia Forge ignitions. 4247521481, // Source: Complete the "Beautiful but Deadly" Triumph. 4290227252, // Source: Complete a Volundr Forge ignition. ], searchString: [], }, brave: { itemHashes: [ 205225492, // Hung Jury SR4 211732170, // Hammerhead 243425374, // Falling Guillotine 570866107, // Succession 2228325504, // Edge Transit 2480074702, // Forbearance 2499720827, // Midnight Coup 2533990645, // Blast Furnace 3098328572, // The Recluse 3757612024, // Luna's Howl 3851176026, // Elsie's Rifle 4043921923, // The Mountaintop ], sourceHashes: [ 2952071500, // Source: Into the Light ], searchString: [], }, calus: { itemHashes: [ 947448544, // Shadow of Earth Shell 1661191192, // The Tribute Hall 1661191193, // Crown of Sorrow 1661191194, // A Hall of Delights 1661191195, // The Imperial Menagerie 2027598066, // Imperial Opulence 2027598067, // Imperial Dress 2816212794, // Bad Juju 3176509806, // Árma Mákhēs 3580904580, // Legend of Acrius 3841416152, // Golden Empire 3841416153, // Goldleaf 3841416154, // Shadow Gilt 3841416155, // Cinderchar 3875444086, // The Emperor's Chosen ], sourceHashes: [ 1675483099, // Source: Leviathan, Spire of Stars raid lair. 2399751101, // Acquired from the raid "Crown of Sorrow." 2511152325, // Acquired from the Menagerie aboard the Leviathan. 2653618435, // Source: Leviathan raid. 2765304727, // Source: Leviathan raid on Prestige difficulty. 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. 2937902448, // Source: Leviathan, Eater of Worlds raid lair. 3147603678, // Acquired from the raid "Crown of Sorrow." 4009509410, // Source: Complete challenges in the Leviathan raid. 4066007318, // Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty. ], searchString: [], }, campaign: { itemHashes: [], sourceHashes: [ 13912404, // Source: Unlock Your Arc Subclass 100617404, // Requires Titan Class 286427063, // Source: Fallen Empire Campaign 409652252, // Source: The Witch Queen Campaign 431243768, // Source: The Edge of Fate Campaign 460742691, // Requires Guardian Rank 6: Masterwork Weapons 569214265, // Source: Red War Campaign 633667627, // Requires Tier 4 or 5 Weapon 677167936, // Source: Complete the campaign as a Warlock. 712662541, // Requires Season 27 Tier 5 Weapon 736336644, // Source: "A Spark of Hope" Quest 901482731, // Source: Lightfall Campaign 918840100, // Source: Shadowkeep Campaign 923708784, // Requires Guardian Rank 7: Threats and Surges 958460845, // Source: The Final Shape Campaign 1076222895, // Source: Defeat bosses in Flashpoints. 1103518848, // Source: Earned over the course of the Warmind campaign. 1118966764, // Source: Dismantle an item with this shader applied to it. 1281387702, // Source: Unlock Your Void Subclass 1701477406, // Source: Flashpoint milestones; Legendary engrams. 2242939082, // Requires Hunter Class 2278847330, // Requires Guardian Rank 3 2308290458, // Requires 1,000 Warlock Kills 2552784968, // Requires Guardian Rank 2 2744321951, // Source: Complete a heroic Public Event. 2892963218, // Source: Earned while leveling. 2895784523, // Source: Pledge to all factions on a single character. 2929562373, // Source: Unlock Your Solar Subclass 2988465950, // Source: Planetary faction chests. 3099553329, // Source: Complete the campaign as a Titan. 3126774631, // Requires 1,000 Hunter Kills 3174947771, // Requires Guardian Rank 6: Vendor Challenges 3431853656, // Achieved a Grimoire score of over 5000 in Destiny. 3532642391, // Source: Forsaken Campaign 3704442923, // Source: Curse of Osiris Campaign 3936473457, // Requires Warlock Class 4008954452, // Requires Shaped or Enhanced Weapon 4288102251, // Requires 1,000 Titan Kills 4290499613, // Source: Complete the campaign as a Hunter. ], searchString: [], }, cayde6: { itemHashes: [], sourceHashes: [ 2206233229, // Source: Follow treasure maps. ], searchString: [], }, coil: { itemHashes: [ 2563668388, // Scalar Potential 4153087276, // Appetence ], sourceHashes: [ 561126969, // Source: "Starcrossed" Mission 1664308183, // Source: Season of the Wish Activities 4278841194, // Source: Season of the Wish Triumphs ], searchString: [], }, compass: { itemHashes: [], sourceHashes: [ 164083100, // Source: Display of Supremacy, Weekly Challenge 3100439379, // Source: Mission "Exorcism" ], searchString: [], }, conquest: { itemHashes: [], sourceHashes: [ 1331532890, // Source: Seasonal Conquest Triumph "Ultimate Victory" ], searchString: [], }, contact: { itemHashes: [], sourceHashes: [ 2039343154, // Source: Contact Public Event ], searchString: [], }, cos: { itemHashes: [ 947448544, // Shadow of Earth Shell 1661191193, // Crown of Sorrow 2027598066, // Imperial Opulence 2027598067, // Imperial Dress ], sourceHashes: [ 2399751101, // Acquired from the raid "Crown of Sorrow." 3147603678, // Acquired from the raid "Crown of Sorrow." ], searchString: [], }, crota: { itemHashes: [], sourceHashes: [ 1897187034, // Source: "Crota's End" Raid ], searchString: [], }, crotasend: { itemHashes: [], sourceHashes: [ 1897187034, // Source: "Crota's End" Raid ], searchString: [], }, crownofsorrow: { itemHashes: [ 947448544, // Shadow of Earth Shell 1661191193, // Crown of Sorrow 2027598066, // Imperial Opulence 2027598067, // Imperial Dress ], sourceHashes: [ 2399751101, // Acquired from the raid "Crown of Sorrow." 3147603678, // Acquired from the raid "Crown of Sorrow." ], searchString: [], }, crucible: { itemHashes: [ 2307365, // The Inquisitor (Adept) 51129316, // The Inquisitor 161675590, // Whistler's Whim (Adept) 303107619, // Tomorrow's Answer (Adept) 501345268, // Shayura's Wrath (Adept) 548809020, // Exalted Truth 627188188, // Eye of Sol 711889599, // Whistler's Whim (Adept) 769099721, // Devil in the Details 825554997, // The Inquisitor (Adept) 854379020, // Astral Horizon (Adept) 874623537, // Cataphract GL3 (Adept) 906840740, // Unwavering Duty 1141586039, // Unexpected Resurgence (Adept) 1201528146, // Exalted Truth (Adept) 1230660649, // Victory's Wreath 1292594730, // The Summoner (Adept) 1321626661, // Eye of Sol (Adept) 1401300690, // Eye of Sol 1574601402, // Whistler's Whim 1661191197, // Disdain for Glitter 1705843397, // Exalted Truth (Adept) 1711056134, // Incisor 1820994983, // The Summoner 1893967086, // Keen Thistle 1968410628, // The Prophet 1973107014, // Igneous Hammer 2022294213, // Shayura's Wrath 2059255495, // Eye of Sol (Adept) 2185327324, // The Inquisitor 2300143112, // Yesterday's Question 2314610827, // Igneous Hammer (Adept) 2330860573, // The Inquisitor (Adept) 2378785953, // Yesterday's Question (Adept) 2414564781, // Punctuation Marks 2420153991, // Made Shaxx Proud 2421180981, // Incisor (Adept) 2588739576, // Crucible Solemnity 2588739578, // Crucible Legacy 2588739579, // Crucible Metallic 2632846356, // Rain of Ashes 2653171212, // The Inquisitor 2653171213, // Astral Horizon 2738601016, // Cataphract GL3 2759251821, // Unwavering Duty (Adept) 2839600459, // Incisor (Adept) 3001205424, // Ecliptic Distaff 3009199534, // Tomorrow's Answer 3019024381, // The Prophet (Adept) 3102421004, // Exalted Truth 3165143747, // Whistler's Whim 3193598749, // The Immortal (Adept) 3332125295, // Aisha's Care (Adept) 3436626079, // Exalted Truth 3444632029, // Unwavering Duty (Adept) 3503560035, // Keen Thistle (Adept) 3624844116, // Unwavering Duty 3920882229, // Exalted Truth (Adept) 3928440584, // Crucible Carmine 3928440585, // Crucible Redjack 3969379530, // Aisha's Care 4005780578, // Unexpected Resurgence 4039572196, // The Immortal 4060882456, // Rubicund Wrap (Ornament) 4248997900, // Incisor ], sourceHashes: [ 164083100, // Source: Display of Supremacy, Weekly Challenge 454115234, // Source: Associated Crucible Quest 598662729, // Source: Reach Glory Rank "Legend" in the Crucible. 705363737, // Source: Heavy Metal: Supremacy 745186842, // Source: Associated Crucible Quest 897576623, // Source: Complete Crucible matches and earn rank-up packages from Lord Shaxx. 929025440, // Acquired by competing in the Crucible during the Prismatic Inferno. 1217831333, // Source: Associated Crucible Quest 1223492644, // Source: Complete the "Reconnaissance by Fire" quest. 1465057711, // Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit) 1494513645, // Source: Glory Matches in Crucible 2055470113, // Source: Chance to acquire when completing Crucible Survival matches after reaching Glory Rank "Mythic." 2537301256, // Source: Glory Rank of "Fabled" in Crucible 2558941813, // Source: Place Silver III Division or Higher in Ranked Crucible Playlists 2622122683, // Source: Lord Shaxx Rank Up Reputation 2641169841, // Source: Purchase from Lord Shaxx 2658055900, // Source: Complete the "Season 8: Battle Drills" quest. 2669524419, // Source: Crucible 2821852478, // Source: Complete this weapon's associated Crucible quest. 2915991372, // Source: Crucible 3020288414, // Source: Crucible 3226099405, // Source: Crucible Seasonal Ritual Rank Reward 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit 3466789677, // Source: Place Ascendant III Division or Higher in Ranked Crucible Playlists 3656787928, // Source: Crucible Salvager's Salvo Armament ], searchString: [], }, dcv: { itemHashes: [ 197764097, // Wild Hunt Boots 238284968, // Wild Hunt Strides 251310542, // Wild Hunt Hood 317220729, // Wild Hunt Vestment 351285766, // Substitutional Alloy Greaves 377757362, // Substitutional Alloy Hood 417164956, // Jötunn 509561140, // Substitutional Alloy Gloves 509561142, // Substitutional Alloy Gloves 509561143, // Substitutional Alloy Gloves 695795213, // Substitutional Alloy Helm 820890091, // Planck's Stride 844110491, // Substitutional Alloy Gloves 947448544, // Shadow of Earth Shell 1137424312, // Substitutional Alloy Cloak 1137424314, // Substitutional Alloy Cloak 1137424315, // Substitutional Alloy Cloak 1148770067, // Wild Hunt Cloak 1226584228, // Tangled Rust 1226584229, // Tangled Bronze 1276513983, // Wild Hunt Gloves 1298815317, // Brigand's Law 1348357884, // Substitutional Alloy Gauntlets 1458739906, // Wild Hunt Vest 1478986057, // Without Remorse 1584183805, // Substitutional Alloy Cloak 1661191192, // The Tribute Hall 1661191193, // Crown of Sorrow 1661191194, // A Hall of Delights 1661191195, // The Imperial Menagerie 1721943440, // Substitutional Alloy Boots 1721943441, // Substitutional Alloy Boots 1721943442, // Substitutional Alloy Boots 1855720513, // Substitutional Alloy Vest 1855720514, // Substitutional Alloy Vest 1855720515, // Substitutional Alloy Vest 2025716654, // Wild Hunt Grasps 2027598066, // Imperial Opulence 2027598067, // Imperial Dress 2055947316, // Wild Hunt Bond 2096778461, // Substitutional Alloy Strides 2096778462, // Substitutional Alloy Strides 2096778463, // Substitutional Alloy Strides 2279193565, // Wild Hunt Mark 2453357042, // Blast Battue 2468603405, // Substitutional Alloy Plate 2468603406, // Substitutional Alloy Plate 2468603407, // Substitutional Alloy Plate 2545401128, // Wild Hunt Gauntlets 2557722678, // Midnight Smith 2657028416, // Substitutional Alloy Vest 2687273800, // Substitutional Alloy Grips 2690973101, // Substitutional Alloy Hood 2690973102, // Substitutional Alloy Hood 2690973103, // Substitutional Alloy Hood 2742760292, // Substitutional Alloy Plate 2761292744, // Substitutional Alloy Bond 2776503072, // Royal Chase 2778013407, // Firefright 2815379657, // Substitutional Alloy Bond 2815379658, // Substitutional Alloy Bond 2815379659, // Substitutional Alloy Bond 2816212794, // Bad Juju 2868525740, // The Collector 2868525741, // The Invader 2868525742, // The Reaper 2868525743, // The Sentry 2903026872, // Substitutional Alloy Helm 2903026873, // Substitutional Alloy Helm 2903026874, // Substitutional Alloy Helm 2942269704, // Substitutional Alloy Gauntlets 2942269705, // Substitutional Alloy Gauntlets 2942269707, // Substitutional Alloy Gauntlets 3166926328, // Substitutional Alloy Robes 3166926330, // Substitutional Alloy Robes 3166926331, // Substitutional Alloy Robes 3176509806, // Árma Mákhēs 3180809346, // Wild Hunt Greaves 3192738009, // Substitutional Alloy Greaves 3192738010, // Substitutional Alloy Greaves 3192738011, // Substitutional Alloy Greaves 3211806999, // Izanagi's Burden 3351935136, // Wild Hunt Plate 3364258850, // Substitutional Alloy Strides 3580904580, // Legend of Acrius 3588934839, // Le Monarque 3650581584, // New Age Black Armory 3650581585, // Refurbished Black Armory 3650581586, // Rasmussen Clan 3650581587, // House of Meyrin 3650581588, // Satou Tribe 3650581589, // Bergusian Night 3680920565, // Substitutional Alloy Robes 3757338780, // Substitutional Alloy Mark 3757338782, // Substitutional Alloy Mark 3757338783, // Substitutional Alloy Mark 3808901541, // Viper Strike 3841416152, // Golden Empire 3841416153, // Goldleaf 3841416154, // Shadow Gilt 3841416155, // Cinderchar 3875444086, // The Emperor's Chosen 3887272785, // Wild Hunt Helm 3911047865, // Substitutional Alloy Mark 4013678605, // Substitutional Alloy Boots 4026120124, // Substitutional Alloy Grips 4026120125, // Substitutional Alloy Grips 4026120127, // Substitutional Alloy Grips 4070722289, // Substitutional Alloy Mask 4078925540, // Substitutional Alloy Mask 4078925541, // Substitutional Alloy Mask 4078925542, // Substitutional Alloy Mask 4079117607, // Wild Hunt Mask 4085986809, // Secret Treasure ], sourceHashes: [ 96303009, // Source: Purchased from Amanda Holliday. 110159004, // Source: Complete Nightfall strike "Warden of Nothing." 146504277, // Source: Earn rank-up packages from Arach Jalaal. 148542898, // Source: Equip the full Mercury destination set on a Warlock. 164083100, // Source: Display of Supremacy, Weekly Challenge 194661944, // Source: Adventure "Siren Song" on Saturn's Moon, Titan 266896577, // Source: Solve the Norse glyph puzzle. 315474873, // Source: Complete activities and earn rank-up packages on Io. 354493557, // Source: Complete Nightfall strike "Savathûn's Song." 439994003, // Source: Complete the "Master Smith" Triumph. 482012099, // Source: Adventure "Thief of Thieves" on Saturn's Moon, Titan 620369433, // Source: Season of the Haunted Triumph 636474187, // Source: Adventure "Deathless" on Saturn's Moon, Titan 783399508, // Source: Adventure "Supply and Demand" in the European Dead Zone 790152021, // Source: Season of Plunder Triumph 790433146, // Source: Adventure "Dark Alliance" in the European Dead Zone 798957490, // Source: Complete wanted escapee bounties for the Spider. 841568343, // Source: "Hunt for the Wrathborn" Quest 925197669, // Source: Complete a Bergusia Forge ignition. 948753311, // Source: Found by completing Volundr Forge ignitions. 976328308, // Source: The Derelict Leviathan 1036506031, // Source: Complete activities and earn rank-up packages on Mars. 1067250718, // Source: Adventure "Arecibo" on Io 1175566043, // Source: Complete Nightfall strike "A Garden World." 1186140085, // Source: Adventure "Unbreakable" on Nessus 1283862526, // Source: Season of the Haunted Nightfall Grandmaster 1286332045, // Source: Found by completing Izanami Forge ignitions. 1289998337, // Source: Adventure "Hack the Planet" on Nessus 1299614150, // Source: [REDACTED] on Mars. 1400219831, // Source: Equip the full Mercury destination set on a Hunter. 1411886787, // Source: Equip the full Mercury destination set on a Titan. 1457456824, // Source: Complete the "Reunited Siblings" Triumph. 1464399708, // Source: Earn rank-up packages from Executor Hideo. 1465990789, // Source: Solve the Japanese glyph puzzle. 1483048674, // Source: Complete the "Scourge of the Past" raid. 1527887247, // Source: Adventure "Red Legion, Black Oil" in the European Dead Zone 1581680964, // Source: Complete Nightfall strike "Tree of Probabilities." 1596507419, // Source: Complete a Gofannon Forge ignition. 1618754228, // Source: Sundial Activity on Mercury 1654120320, // Source: Complete activities and earn rank-up packages on Mercury. 1675483099, // Source: Leviathan, Spire of Stars raid lair. 1736997121, // Source: Adventure "Stop and Go" in the European Dead Zone 1771326504, // Source: Complete activities and earn rank-up packages on the Tangled Shore. 1832642406, // Source: World Quest "Dynasty" on Io. 1861838843, // Source: Adventure "A Frame Job" in the European Dead Zone 1924238751, // Source: Complete Nightfall strike "Will of the Thousands." 1952675042, // Source: Complete Gambit Prime matches and increase your rank. 2039343154, // Source: Contact Public Event 2040548068, // Source: Adventure "Release" on Nessus 2062058385, // Source: Crafted in a Black Armory forge. 2085016678, // Source: Complete the "Scourge of the Past" raid within the first 24 hours after its launch. 2096915131, // Source: Adventure "Poor Reception" in the European Dead Zone 2206233229, // Source: Follow treasure maps. 2273761598, // Source: Season of the Haunted Activities 2310754348, // Source: World Quest "Data Recovery" on Mars. 2345202459, // Source: Adventure "Invitation from the Emperor" on Nessus 2384327872, // Source: Solve the French glyph puzzle. 2392127416, // Source: Adventure "Cliffhanger" on Io 2399751101, // Acquired from the raid "Crown of Sorrow." 2487203690, // Source: Complete Nightfall strike "Tree of Probabilities." 2511152325, // Acquired from the Menagerie aboard the Leviathan. 2541753910, // Source: Complete the "Master Blaster" Triumph. 2553369674, // Source: Adventure "Exodus Siege" on Nessus 2627087475, // Source: Obelisk Bounties and Resonance Rank Increases Across the System 2653618435, // Source: Leviathan raid. 2676881949, // Source: Season of the Haunted 2717017239, // Source: Complete Nightfall strike "The Pyramidion." 2765304727, // Source: Leviathan raid on Prestige difficulty. 2805208672, // Source: Complete Nightfall strike "The Hollowed Lair." 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. 2926805810, // Source: Complete Nightfall strike "Strange Terrain." 2937902448, // Source: Leviathan, Eater of Worlds raid lair. 2966694626, // Source: Found by solving the mysteries behind the Black Armory's founding families. 2982642634, // Source: Season of Plunder Grandmaster Nightfall 3047033583, // Source: Returned the Obsidian Accelerator. 3079246067, // Source: Complete Osiris' Lost Prophecies for Brother Vance on Mercury. 3094114967, // Source: Season of the Lost Ritual Playlists 3107094548, // Source: "Coup de Grâce" Mission 3147603678, // Acquired from the raid "Crown of Sorrow." 3257722699, // Source: Complete the "Clean Up on Aisle Five" Triumph. 3265560237, // Source: Cryptic Quatrains III 3308438907, // Source: Season of Plunder 3390164851, // Source: Found by turning in Black Armory bounties. 3427537854, // Source: Adventure "Road Rage" on Io 3534706087, // Source: Complete activities and earn rank-up packages on Saturn's Moon, Titan. 3569603185, // Source: Earn rank-up packages from Lakshmi-2. 3740731576, // Source: "A Rising Tide" Mission 3754173885, // Source: Adventure "Getting Your Hands Dirty" in the European Dead Zone 3764925750, // Source: Complete an Izanami Forge ignition. 3964663093, // Source: Rare drop from high-scoring Nightfall strikes on Mercury. 4009509410, // Source: Complete challenges in the Leviathan raid. 4066007318, // Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty. 4101102010, // Source: Found by completing Bergusia Forge ignitions. 4122810030, // Source: Complete seasonal activities during Season of the Undying. 4137108180, // Source: Escalation Protocol on Mars. 4140654910, // Source: Eliminate all Barons on the Tangled Shore. 4199401779, // Source: Season of Plunder Activities 4214471686, // Source: Adventure "Unsafe at Any Speed" in the European Dead Zone 4246883461, // Source: Found in the "Scourge of the Past" raid. 4247521481, // Source: Complete the "Beautiful but Deadly" Triumph. 4263201695, // Source: Complete Nightfall strike "A Garden World." 4290227252, // Source: Complete a Volundr Forge ignition. ], searchString: [ 'mercury', 'mars', 'titan', 'io', 'leviathan', 'ep', 'blackarmory', 'menagerie', 'eow', 'sos', 'scourge', 'crownofsorrow', ], }, deepstonecrypt: { itemHashes: [], sourceHashes: [ 866530798, // Source: "Not a Scratch" Triumph 1405897559, // Source: "Deep Stone Crypt" Raid 1692165595, // Source: "Rock Bottom" Triumph ], searchString: [], }, deluxe: { itemHashes: [], sourceHashes: [ 639650067, // Source: Limited Edition of Destiny 2. 1358645302, // Source: Unlocked by a special offer. 1412777465, // Source: Forsaken Refer-a-Friend 1743434737, // Source: Destiny 2 "Forsaken" preorder bonus gift. 1866448829, // Source: Deluxe Edition Bonus 2968206374, // Source: Earned as a Deluxe Edition bonus. 2985242208, // Source: Earned from a charity promotion. 3173463761, // Source: Pre-order Bonus 3212282221, // Source: Forsaken Annual Pass 3672287903, // Source: The Witch Queen Digital Deluxe Edition 4069355515, // Source: Handed out at US events in 2019. 4166998204, // Source: Earned as a pre-order bonus. ], searchString: [], }, desertperpetual: { itemHashes: [], sourceHashes: [ 596084342, // Source: "The Desert Perpetual" Raid 2127551856, // Source: "The Desert Perpetual" Epic Raid ], searchString: [], }, do: { itemHashes: [], sourceHashes: [ 146504277, // Source: Earn rank-up packages from Arach Jalaal. ], searchString: [], }, dreaming: { itemHashes: [ 185321779, // Ennead 3352019292, // Secret Victories ], sourceHashes: [ 2559145507, // Source: Complete activities in the Dreaming City. 3874934421, // Source: Complete Nightfall strike "The Corrupted." ], searchString: [], }, drifter: { itemHashes: [ 180108390, // Kit and Kaboodle 180108391, // Dance the Demons Away 1335424933, // Gambit Suede 1335424934, // Gambit Chrome 1335424935, // Gambit Leather 1661191187, // Mistrust of Gifts 2026755633, // Breakneck 2224920148, // Gambit Blackguard 2224920149, // Gambit Steel 2394866220, // Keep on Drifting 2588647363, // Live for the Hustle 3001205424, // Ecliptic Distaff 3217477988, // Gambit Duds 4060882457, // Snakeskin Wrap (Ornament) ], sourceHashes: [ 186854335, // Source: Gambit 571102497, // Source: Associated Gambit Quest 594786771, // Source: Complete this weapon's associated Gambit quest. 887452441, // Source: Gambit Salvager's Salvo Armament 1127923611, // Source: 3 Gambit Rank Resets in a Season 1162859311, // Source: Complete the "Clean Getaway" quest. 1465057711, // Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit) 2170269026, // Source: Complete Gambit matches and earn rank-up packages from the Drifter. 2364933290, // Source: Gambit Seasonal Ritual Rank Reward 2601524261, // Source: Associated Gambit Quest 2843045413, // Source: Gambit 2883838366, // Source: Complete the "Breakneck" quest from the Drifter. 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit 3422985544, // Source: Associated Gambit Quest 3494247523, // Source: Complete the "Season 8: Keepin' On" quest. 3522070610, // Source: Gambit 3942778906, // Source: Drifter Rank Up Reputation ], searchString: [], }, dsc: { itemHashes: [], sourceHashes: [ 866530798, // Source: "Not a Scratch" Triumph 1405897559, // Source: "Deep Stone Crypt" Raid 1692165595, // Source: "Rock Bottom" Triumph ], searchString: [], }, duality: { itemHashes: [], sourceHashes: [ 1282207663, // Source: Dungeon "Duality" ], searchString: [], }, dungeon: { itemHashes: [ 14929251, // Long Arm 185321778, // The Eternal Return 189194532, // No Survivors (Adept) 233402416, // New Pacific Epitaph (Adept) 291447487, // Cold Comfort 492673102, // New Pacific Epitaph 749483159, // Prosecutor (Adept) 814876684, // Wish-Ender 1050582210, // Greasy Luck (Adept) 1066598837, // Relentless (Adept) 1157220231, // No Survivors (Adept) 1303313141, // Unsworn 1460079227, // Liminal Vigil 1685406703, // Greasy Luck 1773934241, // Judgment 1817605554, // Cold Comfort (Adept) 1904170910, // A Sudden Death 1987644603, // Judgment (Adept) 2059741649, // New Pacific Epitaph 2126543269, // Cold Comfort (Adept) 2129814338, // Prosecutor 2477408004, // Wilderflight (Adept) 2730671571, // Terminus Horizon 2760833884, // Cold Comfort 2764074355, // A Sudden Death (Adept) 2844014413, // Pallas Galliot 2934305134, // Greasy Luck 2982006965, // Wilderflight 3185151619, // New Pacific Epitaph (Adept) 3210739171, // Greasy Luck (Adept) 3329218848, // Judgment (Adept) 3421639790, // Liminal Vigil (Adept) 3681280908, // Relentless 3692140710, // Long Arm (Adept) 4193602194, // No Survivors 4228149269, // No Survivors 4267192886, // Terminus Horizon (Adept) ], sourceHashes: [ 506073192, // Source: "Prophecy" Dungeon 613435025, // Source: "Warlord's Ruin" Dungeon 675740011, // Source: "Grasp of Avarice" Dungeon 877404349, // Source: Rite of the Nine 1282207663, // Source: Dungeon "Duality" 1597738585, // Source: "Spire of the Watcher" Dungeon 1745960977, // Source: "Pit of Heresy" Dungeon 2463956052, // Source: Vesper's Host 2607970476, // Source: Sundered Doctrine 3247513834, // Source: Equilibrium 3288974535, // Source: "Ghosts of the Deep" Dungeon ], searchString: [], }, echoes: { itemHashes: [], sourceHashes: [ 536806855, // Source: Episode: Echoes 2306801178, // Source: Episode: Echoes Activities 2514060836, // Source: Episode: Echoes Enigma Protocol Activity 2631398023, // Source: Radiolite Bay Deposits ], searchString: [], }, edgeoffate: { itemHashes: [], sourceHashes: [ 431243768, // Source: The Edge of Fate Campaign 4034415948, // Source: The Edge of Fate Activities ], searchString: [], }, edz: { itemHashes: [], sourceHashes: [ 783399508, // Source: Adventure "Supply and Demand" in the European Dead Zone 790433146, // Source: Adventure "Dark Alliance" in the European Dead Zone 1373723300, // Source: Complete activities and earn rank-up packages in the EDZ. 1527887247, // Source: Adventure "Red Legion, Black Oil" in the European Dead Zone 1736997121, // Source: Adventure "Stop and Go" in the European Dead Zone 1861838843, // Source: Adventure "A Frame Job" in the European Dead Zone 2096915131, // Source: Adventure "Poor Reception" in the European Dead Zone 3754173885, // Source: Adventure "Getting Your Hands Dirty" in the European Dead Zone 4214471686, // Source: Adventure "Unsafe at Any Speed" in the European Dead Zone 4292996207, // Source: World Quest "Enhance!" in the European Dead Zone. ], searchString: [], }, enclave: { itemHashes: [], sourceHashes: [ 1309588429, // Source: "Chief Investigator" Triumph 2055289873, // Source: "The Evidence Board" Exotic Quest ], searchString: [], }, eow: { itemHashes: [], sourceHashes: [ 2937902448, // Source: Leviathan, Eater of Worlds raid lair. 4066007318, // Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty. ], searchString: [], }, ep: { itemHashes: [], sourceHashes: [ 4137108180, // Source: Escalation Protocol on Mars. ], searchString: [], }, equilibrium: { itemHashes: [], sourceHashes: [ 3247513834, // Source: Equilibrium ], searchString: [], }, europa: { itemHashes: [], sourceHashes: [ 286427063, // Source: Fallen Empire Campaign 1148859274, // Source: Exploring Europa 1492981395, // Source: "The Stasis Prototype" Quest 2171520631, // Source: "Lost Lament" Exotic Quest 3125456997, // Source: Europan Tour 3965815470, // Source: Higher Difficulty Empire Hunts ], searchString: [], }, events: { itemHashes: [ 425681240, // Acosmic 601948197, // Zephyr 689294985, // Jurassic Green 1280894514, // Mechabre 2477980485, // Mechabre 2603335652, // Jurassic Green 2869466318, // BrayTech Werewolf 3400256755, // Zephyr 3558681245, // BrayTech Werewolf 3559361670, // The Title ], sourceHashes: [ 32323943, // Source: Moments of Triumph 151416041, // Source: Solstice 464727567, // Source: Dawning 2021 547767158, // Source: Dawning 2018 611838069, // Source: Guardian Games 629617846, // Source: Dawning 2020 641018908, // Source: Solstice 2018 772619302, // Completed all 8 Moments of Triumph in Destiny's second year. 894030814, // Source: Heavy Metal Event 923678151, // Source: Upgraded Event Card Reward 1054169368, // Source: Festival of the Lost 2021 1225476079, // Source: Moments of Triumph 2022 1232863328, // Source: Moments of Triumph 2024 1360005982, // Completed a Moment of Triumph in Destiny's second year. 1397119901, // Completed a Moment of Triumph in Destiny's first year. 1416471099, // Source: Moments of Triumph 2023 1462687159, // Reached level 5 in the Ages of Triumph record book. 1505938361, // Source: Call to Arms Event 1568732528, // Source: Guardian Games 2024 1666677522, // Source: Solstice 1677921161, // Source: Festival of the Lost 2018. 1919933822, // Source: Festival of the Lost 2020 1953779156, // Source: Events 2006303146, // Source: Guardian Games 2022 2011810450, // Source: Season 13 Guardian Games 2045032171, // Source: Arms Week Event 2050870152, // Source: Solstice 2187511136, // Source: Earned during the seasonal Revelry event. 2364515524, // Source: Dawning 2022 2473294025, // Source: Guardian Games 2023 2502262376, // Source: Earned during the seasonal Crimson Days event. 2797674516, // Source: Moments of Triumph 2021 3092212681, // Source: Dawning 2019 3095773956, // Source: Guardian Games 2025 3112857249, // Completed all 10 Moments of Triumph in Destiny's first year. 3190938946, // Source: Festival of the Lost 2019 3388021959, // Source: Guardian Games 3482766024, // Source: Festival of the Lost 2024 3693722471, // Source: Festival of the Lost 2020 3724111213, // Source: Solstice 2019 3736521079, // Reached level 1 in the Ages of Triumph record book. 3952847349, // Source: The Dawning. 4041583267, // Source: Festival of the Lost 4054646289, // Source: Earned during the seasonal Dawning event. ], searchString: ['dawning', 'crimsondays', 'solstice', 'fotl', 'revelry', 'games'], }, eververse: { itemHashes: [], sourceHashes: [ 269962496, // Source: Eververse 860688654, // Source: Eververse 2882367429, // Source: Eververse\nComplete the "Vault of Glass" raid to unlock this in Eververse. 4036739795, // Source: Bright Engrams ], searchString: [], }, evidenceboard: { itemHashes: [], sourceHashes: [ 1309588429, // Source: "Chief Investigator" Triumph 2055289873, // Source: "The Evidence Board" Exotic Quest ], searchString: [], }, exoticquest: { itemHashes: [], sourceHashes: [ 210885364, // Source: Flawless "Presage" Exotic Quest on Master Difficulty 281362298, // Source: Strider Exotic Quest 454251931, // Source: "What Remains" Exotic Quest 483798855, // Source: "The Final Strand" Exotic Quest 709680645, // Source: "Truly Satisfactory" Triumph 1141831282, // Source: "Of Queens and Worms" Exotic Quest 1302157812, // Source: Wild Card Exotic Quest 1388323447, // Source: Exotic Mission "The Whisper" 1476475066, // Source: "Firmware Update" Triumph 1730197643, // Source: //node.ovrd.AVALON// Exotic Quest 1823766625, // Source: "Vox Obscura" Exotic Quest 1957611613, // Source: An Exotic quest or challenge. 2055289873, // Source: "The Evidence Board" Exotic Quest 2068312112, // Source: Exotic Mission "Zero Hour" 2171520631, // Source: "Lost Lament" Exotic Quest 2296534980, // Source: Exotic Mission Encore 2745272818, // Source: "Presage" Exotic Quest 2856954949, // Source: "Let Loose Thy Talons" Exotic Quest 3237053501, // Source: Heliostat 3597879858, // Source: "Presage" Exotic Quest ], searchString: [], }, fwc: { itemHashes: [], sourceHashes: [ 3569603185, // Source: Earn rank-up packages from Lakshmi-2. ], searchString: [], }, gambit: { itemHashes: [ 180108390, // Kit and Kaboodle 180108391, // Dance the Demons Away 1335424933, // Gambit Suede 1335424934, // Gambit Chrome 1335424935, // Gambit Leather 1661191187, // Mistrust of Gifts 2026755633, // Breakneck 2224920148, // Gambit Blackguard 2224920149, // Gambit Steel 2394866220, // Keep on Drifting 2588647363, // Live for the Hustle 3001205424, // Ecliptic Distaff 3217477988, // Gambit Duds 4060882457, // Snakeskin Wrap (Ornament) ], sourceHashes: [ 186854335, // Source: Gambit 571102497, // Source: Associated Gambit Quest 594786771, // Source: Complete this weapon's associated Gambit quest. 887452441, // Source: Gambit Salvager's Salvo Armament 1127923611, // Source: 3 Gambit Rank Resets in a Season 1162859311, // Source: Complete the "Clean Getaway" quest. 1465057711, // Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit) 2170269026, // Source: Complete Gambit matches and earn rank-up packages from the Drifter. 2364933290, // Source: Gambit Seasonal Ritual Rank Reward 2601524261, // Source: Associated Gambit Quest 2843045413, // Source: Gambit 2883838366, // Source: Complete the "Breakneck" quest from the Drifter. 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit 3422985544, // Source: Associated Gambit Quest 3494247523, // Source: Complete the "Season 8: Keepin' On" quest. 3522070610, // Source: Gambit 3942778906, // Source: Drifter Rank Up Reputation ], searchString: [], }, gambitprime: { itemHashes: [ 2868525740, // The Collector 2868525741, // The Invader 2868525742, // The Reaper 2868525743, // The Sentry 3808901541, // Viper Strike ], sourceHashes: [ 1952675042, // Source: Complete Gambit Prime matches and increase your rank. ], searchString: [], }, garden: { itemHashes: [ 4103414242, // Divinity ], sourceHashes: [ 1491707941, // Source: "Garden of Salvation" Raid ], searchString: [], }, gardenofsalvation: { itemHashes: [ 4103414242, // Divinity ], sourceHashes: [ 1491707941, // Source: "Garden of Salvation" Raid ], searchString: [], }, ghostsofthedeep: { itemHashes: [ 189194532, // No Survivors (Adept) 233402416, // New Pacific Epitaph (Adept) 291447487, // Cold Comfort 492673102, // New Pacific Epitaph 1050582210, // Greasy Luck (Adept) 1157220231, // No Survivors (Adept) 1685406703, // Greasy Luck 1817605554, // Cold Comfort (Adept) 2059741649, // New Pacific Epitaph 2126543269, // Cold Comfort (Adept) 2760833884, // Cold Comfort 2934305134, // Greasy Luck 3185151619, // New Pacific Epitaph (Adept) 3210739171, // Greasy Luck (Adept) 4193602194, // No Survivors 4228149269, // No Survivors ], sourceHashes: [ 3288974535, // Source: "Ghosts of the Deep" Dungeon ], searchString: [], }, gos: { itemHashes: [ 4103414242, // Divinity ], sourceHashes: [ 1491707941, // Source: "Garden of Salvation" Raid ], searchString: [], }, gotd: { itemHashes: [ 189194532, // No Survivors (Adept) 233402416, // New Pacific Epitaph (Adept) 291447487, // Cold Comfort 492673102, // New Pacific Epitaph 1050582210, // Greasy Luck (Adept) 1157220231, // No Survivors (Adept) 1685406703, // Greasy Luck 1817605554, // Cold Comfort (Adept) 2059741649, // New Pacific Epitaph 2126543269, // Cold Comfort (Adept) 2760833884, // Cold Comfort 2934305134, // Greasy Luck 3185151619, // New Pacific Epitaph (Adept) 3210739171, // Greasy Luck (Adept) 4193602194, // No Survivors 4228149269, // No Survivors ], sourceHashes: [ 3288974535, // Source: "Ghosts of the Deep" Dungeon ], searchString: [], }, grasp: { itemHashes: [], sourceHashes: [ 675740011, // Source: "Grasp of Avarice" Dungeon ], searchString: [], }, gunsmith: { itemHashes: [], sourceHashes: [ 1459595344, // Source: Purchase from Banshee-44 or Ada-1 1788267693, // Source: Earn rank-up packages from Banshee-44. 2986841134, // Source: Salvager's Salvo Armament Quest 3512613235, // Source: "A Sacred Fusion" Quest ], searchString: [], }, harbinger: { itemHashes: [], sourceHashes: [ 2856954949, // Source: "Let Loose Thy Talons" Exotic Quest ], searchString: [], }, haunted: { itemHashes: [ 1478986057, // Without Remorse 2778013407, // Firefright ], sourceHashes: [ 620369433, // Source: Season of the Haunted Triumph 976328308, // Source: The Derelict Leviathan 1283862526, // Source: Season of the Haunted Nightfall Grandmaster 2273761598, // Source: Season of the Haunted Activities 2676881949, // Source: Season of the Haunted ], searchString: [], }, heliostat: { itemHashes: [], sourceHashes: [ 3237053501, // Source: Heliostat ], searchString: [], }, heresy: { itemHashes: [], sourceHashes: [ 21494224, // Source: Offer the correct final answer in an uncharted space. 745481267, // Source: Intrinsic Iteration Triumph 1341921330, // Source: Episode: Heresy Activities 1792957897, // Source: "Efficient Challenger" Triumph 2607970476, // Source: Sundered Doctrine 2869564842, // Source: "Vengeful Knife" Triumph 3310034131, // Source: "Crossed Blades" Triumph 3358334503, // Source: "Boon Ghost Mod Collector" Triumph 3507911332, // Source: Episode: Heresy ], searchString: [], }, ikora: { itemHashes: [], sourceHashes: [ 3075817319, // Source: Earn rank-up packages from Ikora Rey. ], searchString: [], }, intothelight: { itemHashes: [ 205225492, // Hung Jury SR4 211732170, // Hammerhead 243425374, // Falling Guillotine 570866107, // Succession 2228325504, // Edge Transit 2480074702, // Forbearance 2499720827, // Midnight Coup 2533990645, // Blast Furnace 3098328572, // The Recluse 3757612024, // Luna's Howl 3851176026, // Elsie's Rifle 4043921923, // The Mountaintop ], sourceHashes: [ 1388323447, // Source: Exotic Mission "The Whisper" 1902517582, // Source: Where's Archie? 2068312112, // Source: Exotic Mission "Zero Hour" 2952071500, // Source: Into the Light ], searchString: [], }, io: { itemHashes: [], sourceHashes: [ 315474873, // Source: Complete activities and earn rank-up packages on Io. 1067250718, // Source: Adventure "Arecibo" on Io 1832642406, // Source: World Quest "Dynasty" on Io. 2392127416, // Source: Adventure "Cliffhanger" on Io 2717017239, // Source: Complete Nightfall strike "The Pyramidion." 3427537854, // Source: Adventure "Road Rage" on Io ], searchString: [], }, ironbanner: { itemHashes: [ 231533811, // Iron Strength 1162929425, // The Golden Standard 1448664466, // Iron Bone 1448664467, // Iron Gold 1661191199, // Grizzled Wolf 1987234560, // Iron Ruby 2448092902, // Rusted Iron ], sourceHashes: [ 561111210, // Source: Iron Banner Salvager's Salvo Armament 1027607603, // Source: Associated Iron Banner Quest 1312894505, // Source: Iron Banner 1828622510, // Source: Chance to acquire when you win Iron Banner matches. 1926923633, // Source: Lord Saladin Rank Up Reputation 2520862847, // Source: Iron Banner Iron-Handed Diplomacy 2648408612, // Acquired by competing in the Iron Banner when the wolves were loud. 3072862693, // Source: Complete Iron Banner matches and earn rank-up packages from Lord Saladin. ], searchString: [], }, itl: { itemHashes: [ 205225492, // Hung Jury SR4 211732170, // Hammerhead 243425374, // Falling Guillotine 570866107, // Succession 2228325504, // Edge Transit 2480074702, // Forbearance 2499720827, // Midnight Coup 2533990645, // Blast Furnace 3098328572, // The Recluse 3757612024, // Luna's Howl 3851176026, // Elsie's Rifle 4043921923, // The Mountaintop ], sourceHashes: [ 1388323447, // Source: Exotic Mission "The Whisper" 1902517582, // Source: Where's Archie? 2068312112, // Source: Exotic Mission "Zero Hour" 2952071500, // Source: Into the Light ], searchString: [], }, kepler: { itemHashes: [], sourceHashes: [ 4284811963, // Source: Exploring Kepler ], searchString: [], }, kf: { itemHashes: [], sourceHashes: [ 160129377, // Source: "King's Fall" Raid ], searchString: [], }, kingsfall: { itemHashes: [], sourceHashes: [ 160129377, // Source: "King's Fall" Raid ], searchString: [], }, lastwish: { itemHashes: [ 70083888, // Nation of Beasts 424291879, // Age-Old Bond 501329015, // Chattering Bone 1851777734, // Apex Predator 2884596447, // The Supremacy 3388655311, // Tyranny of Heaven 3591141932, // Techeun Force 3668669364, // Dreaming Spectrum 3885259140, // Transfiguration ], sourceHashes: [ 2455011338, // Source: Last Wish raid. ], searchString: [], }, legendaryengram: { itemHashes: [], sourceHashes: [ 3334812276, // Source: Open Legendary engrams and earn faction rank-up packages. ], searchString: [], }, leviathan: { itemHashes: [ 3580904580, // Legend of Acrius ], sourceHashes: [ 2653618435, // Source: Leviathan raid. 2765304727, // Source: Leviathan raid on Prestige difficulty. 4009509410, // Source: Complete challenges in the Leviathan raid. ], searchString: [], }, limited: { itemHashes: [], sourceHashes: [ 639650067, // Source: Limited Edition of Destiny 2. 1358645302, // Source: Unlocked by a special offer. 1412777465, // Source: Forsaken Refer-a-Friend 1743434737, // Source: Destiny 2 "Forsaken" preorder bonus gift. 1866448829, // Source: Deluxe Edition Bonus 2968206374, // Source: Earned as a Deluxe Edition bonus. 2985242208, // Source: Earned from a charity promotion. 3173463761, // Source: Pre-order Bonus 3212282221, // Source: Forsaken Annual Pass 3672287903, // Source: The Witch Queen Digital Deluxe Edition 4069355515, // Source: Handed out at US events in 2019. 4166998204, // Source: Earned as a pre-order bonus. ], searchString: [], }, lost: { itemHashes: [], sourceHashes: [ 164083100, // Source: Display of Supremacy, Weekly Challenge 3094114967, // Source: Season of the Lost Ritual Playlists ], searchString: [], }, lostsectors: { itemHashes: [], sourceHashes: [ 2203185162, // Source: Solo Expert and Master Lost Sectors ], searchString: [], }, lw: { itemHashes: [ 70083888, // Nation of Beasts 424291879, // Age-Old Bond 501329015, // Chattering Bone 1851777734, // Apex Predator 2884596447, // The Supremacy 3388655311, // Tyranny of Heaven 3591141932, // Techeun Force 3668669364, // Dreaming Spectrum 3885259140, // Transfiguration ], sourceHashes: [ 2455011338, // Source: Last Wish raid. ], searchString: [], }, mars: { itemHashes: [], sourceHashes: [ 1036506031, // Source: Complete activities and earn rank-up packages on Mars. 1299614150, // Source: [REDACTED] on Mars. 1924238751, // Source: Complete Nightfall strike "Will of the Thousands." 2310754348, // Source: World Quest "Data Recovery" on Mars. 2926805810, // Source: Complete Nightfall strike "Strange Terrain." 4137108180, // Source: Escalation Protocol on Mars. ], searchString: [], }, menagerie: { itemHashes: [ 1661191194, // A Hall of Delights 1661191195, // The Imperial Menagerie 3176509806, // Árma Mákhēs 3841416152, // Golden Empire 3841416153, // Goldleaf 3841416154, // Shadow Gilt 3841416155, // Cinderchar 3875444086, // The Emperor's Chosen ], sourceHashes: [ 2511152325, // Acquired from the Menagerie aboard the Leviathan. ], searchString: [], }, mercury: { itemHashes: [], sourceHashes: [ 148542898, // Source: Equip the full Mercury destination set on a Warlock. 1175566043, // Source: Complete Nightfall strike "A Garden World." 1400219831, // Source: Equip the full Mercury destination set on a Hunter. 1411886787, // Source: Equip the full Mercury destination set on a Titan. 1581680964, // Source: Complete Nightfall strike "Tree of Probabilities." 1618754228, // Source: Sundial Activity on Mercury 1654120320, // Source: Complete activities and earn rank-up packages on Mercury. 2487203690, // Source: Complete Nightfall strike "Tree of Probabilities." 3079246067, // Source: Complete Osiris' Lost Prophecies for Brother Vance on Mercury. 3964663093, // Source: Rare drop from high-scoring Nightfall strikes on Mercury. 4263201695, // Source: Complete Nightfall strike "A Garden World." ], searchString: [], }, moon: { itemHashes: [], sourceHashes: [ 1253026984, // Source: Among the lost Ghosts of the Moon. 1999000205, // Source: Exploring the Moon 3589340943, // Source: Altars of Sorrow ], searchString: [], }, neomuna: { itemHashes: [ 1123421440, // Epochal Integration 1311684613, // Dimensional Hypotrochoid 3635821806, // Phyllotactic Spiral 3920310144, // Volta Bracket ], sourceHashes: [ 281362298, // Source: Strider Exotic Quest 454251931, // Source: "What Remains" Exotic Quest 483798855, // Source: "The Final Strand" Exotic Quest 1750523507, // Source: Terminal Overload (Ahimsa Park) 2697389955, // Source: "Neomuna Sightseeing" Triumph 3041847664, // Source: Exploring Neomuna 3773376290, // Source: Terminal Overload (Zephyr Concourse) 4006434081, // Source: Terminal Overload 4110186790, // Source: Terminal Overload (Límíng Harbor) ], searchString: [], }, nessus: { itemHashes: [], sourceHashes: [ 164571094, // Source: World Quest "Exodus Black" on Nessus. 817015032, // Source: Complete Nightfall strike "The Inverted Spire." 1186140085, // Source: Adventure "Unbreakable" on Nessus 1289998337, // Source: Adventure "Hack the Planet" on Nessus 1906492169, // Source: Complete activities and earn rank-up packages on Nessus. 2040548068, // Source: Adventure "Release" on Nessus 2345202459, // Source: Adventure "Invitation from the Emperor" on Nessus 2553369674, // Source: Adventure "Exodus Siege" on Nessus 3022766747, // Source: Complete Nightfall strike "The Insight Terminus." 3067146211, // Source: Complete Nightfall strike "Exodus Crash." ], searchString: [], }, nightfall: { itemHashes: [ 42874240, // Uzume RR4 192784503, // Pre Astyanax IV 213264394, // Buzzard 233635202, // Cruel Mercy 267089201, // Warden's Law (Adept) 496556698, // Pre Astyanax IV (Adept) 555148853, // Wendigo GL3 (Adept) 566740455, // THE SWARM (Adept) 672957262, // Undercurrent (Adept) 772231794, // Hung Jury SR4 817909300, // Undercurrent (Adept) 912222548, // Soldier On 927835311, // Buzzard (Adept) 959037361, // Wild Style (Adept) 1056103557, // Shadow Price (Adept) 1064132738, // BrayTech Osprey (Adept) 1151688091, // Undercurrent 1332123064, // Wild Style 1354727549, // The Slammer (Adept) 1492522228, // Scintillation (Adept) 1586231351, // Mindbender's Ambition 1821529912, // Warden's Law 1854753404, // Wendigo GL3 1854753405, // The Militia's Birthright 1891996599, // Uzume RR4 (Adept) 1987790789, // After the Nightfall 2063217087, // Pre Astyanax IV (Adept) 2074041946, // Mindbender's Ambition (Adept) 2152484073, // Warden's Law 2298039571, // Rake Angle 2322926844, // Shadow Price 2347178967, // Cruel Mercy (Adept) 2450917538, // Uzume RR4 2591257541, // Scintillation 2697143634, // Lotus-Eater (Adept) 2759590322, // THE SWARM 2876244791, // The Palindrome 2883684343, // Hung Jury SR4 (Adept) 2889501828, // The Slammer 2914913838, // Loaded Question (Adept) 2932922810, // Pre Astyanax IV 3106557243, // PLUG ONE.1 (Adept) 3125454907, // Loaded Question 3183283212, // Wendigo GL3 3250744600, // Warden's Law (Adept) 3293524502, // PLUG ONE.1 3610521673, // Uzume RR4 (Adept) 3667553455, // BrayTech Osprey 3686538757, // Undercurrent 3832743906, // Hung Jury SR4 3915197957, // Wendigo GL3 (Adept) 3922217119, // Lotus-Eater 3997086838, // Rake Angle (Adept) 4074251943, // Hung Jury SR4 (Adept) 4077588826, // The Palindrome (Adept) 4162642204, // The Militia's Birthright (Adept) ], sourceHashes: [ 110159004, // Source: Complete Nightfall strike "Warden of Nothing." 277706045, // Source: Season of the Splicer Nightfall Grandmaster 354493557, // Source: Complete Nightfall strike "Savathûn's Song." 817015032, // Source: Complete Nightfall strike "The Inverted Spire." 827839814, // Source: Flawless Chest in Trials of Osiris or Grandmaster Nightfalls 860666126, // Source: Nightfall 1175566043, // Source: Complete Nightfall strike "A Garden World." 1283862526, // Source: Season of the Haunted Nightfall Grandmaster 1516560855, // Source: Season of the Seraph Grandmaster Nightfall 1581680964, // Source: Complete Nightfall strike "Tree of Probabilities." 1596489410, // Source: Season of the Risen Nightfall Grandmaster 1618699950, // Source: Season of the Lost Nightfall Grandmaster 1749037998, // Source: Nightfall 1850609592, // Source: Nightfall 1924238751, // Source: Complete Nightfall strike "Will of the Thousands." 1992319882, // Source: Grandmaster Nightfalls 2347293565, // Source: Complete Nightfall strike "The Arms Dealer." 2376909801, // Source: "Master" Triumph in Nightfalls 2487203690, // Source: Complete Nightfall strike "Tree of Probabilities." 2717017239, // Source: Complete Nightfall strike "The Pyramidion." 2805208672, // Source: Complete Nightfall strike "The Hollowed Lair." 2851783112, // Source: Complete Nightfall strike "Lake of Shadows." 2926805810, // Source: Complete Nightfall strike "Strange Terrain." 2982642634, // Source: Season of Plunder Grandmaster Nightfall 3022766747, // Source: Complete Nightfall strike "The Insight Terminus." 3067146211, // Source: Complete Nightfall strike "Exodus Crash." 3142874552, // Source: Nightfall 3229688794, // Source: Grandmaster Nightfall 3528789901, // Source: Season of the Chosen Nightfall Grandmaster 3874934421, // Source: Complete Nightfall strike "The Corrupted." 3964663093, // Source: Rare drop from high-scoring Nightfall strikes on Mercury. 4208190159, // Source: Complete a Nightfall strike. 4263201695, // Source: Complete Nightfall strike "A Garden World." ], searchString: [], }, nightmare: { itemHashes: [], sourceHashes: [ 550270332, // Source: Complete all Nightmare Hunt time trials on Master difficulty. 2778435282, // Source: Nightmare Hunts ], searchString: [], }, nm: { itemHashes: [], sourceHashes: [ 1464399708, // Source: Earn rank-up packages from Executor Hideo. ], searchString: [], }, paleheart: { itemHashes: [], sourceHashes: [ 941123623, // Pale Heart - Cayde's Stash 2327253880, // Source: Exploring the Pale Heart 3614199681, // Source: Pale Heart Triumph ], searchString: [], }, 'pinnacle-weapon': { itemHashes: [ 444627789, // Oxygen SR3 578459533, // Wendigo GL3 654608616, // Revoker 1050806815, // The Recluse 1584643826, // Hush 1600633250, // 21% Delirium 3098328572, // The Recluse 3354242550, // The Recluse 3907337522, // Oxygen SR3 3962575203, // Hush 4104613038, // Oxygen SR3 ], sourceHashes: [ 598662729, // Source: Reach Glory Rank "Legend" in the Crucible. 1162859311, // Source: Complete the "Clean Getaway" quest. 1244908294, // Source: Complete the "Loaded Question" quest from Zavala. 2317365255, // Source: Complete the "A Loud Racket" quest. 2883838366, // Source: Complete the "Breakneck" quest from the Drifter. ], searchString: [], }, pinnacleops: { itemHashes: [], sourceHashes: [ 1232061833, // Source: Pinnacle Ops ], searchString: [], }, pit: { itemHashes: [], sourceHashes: [ 1745960977, // Source: "Pit of Heresy" Dungeon ], searchString: [], }, plunder: { itemHashes: [ 820890091, // Planck's Stride 1298815317, // Brigand's Law ], sourceHashes: [ 790152021, // Source: Season of Plunder Triumph 2982642634, // Source: Season of Plunder Grandmaster Nightfall 3265560237, // Source: Cryptic Quatrains III 3308438907, // Source: Season of Plunder 3740731576, // Source: "A Rising Tide" Mission 4199401779, // Source: Season of Plunder Activities ], searchString: [], }, presage: { itemHashes: [], sourceHashes: [ 210885364, // Source: Flawless "Presage" Exotic Quest on Master Difficulty 2745272818, // Source: "Presage" Exotic Quest 3597879858, // Source: "Presage" Exotic Quest ], searchString: [], }, prestige: { itemHashes: [], sourceHashes: [ 2765304727, // Source: Leviathan raid on Prestige difficulty. 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. 4066007318, // Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty. ], searchString: [], }, prophecy: { itemHashes: [], sourceHashes: [ 506073192, // Source: "Prophecy" Dungeon ], searchString: [], }, psiops: { itemHashes: [ 2097055732, // Piece of Mind 4067556514, // Thoughtless ], sourceHashes: [ 450719423, // Source: Season of the Risen 2075569025, // PsiOps 2363489105, // Source: Season of the Risen Vendor or Triumphs 3563833902, // Source: Season of the Risen Triumphs ], searchString: [], }, rahool: { itemHashes: [], sourceHashes: [ 4011186136, // Exotic Armor Focusing ], searchString: [], }, raid: { itemHashes: [ 70083888, // Nation of Beasts 424291879, // Age-Old Bond 501329015, // Chattering Bone 947448544, // Shadow of Earth Shell 1661191193, // Crown of Sorrow 1851777734, // Apex Predator 2027598066, // Imperial Opulence 2027598067, // Imperial Dress 2557722678, // Midnight Smith 2884596447, // The Supremacy 3388655311, // Tyranny of Heaven 3580904580, // Legend of Acrius 3591141932, // Techeun Force 3668669364, // Dreaming Spectrum 3885259140, // Transfiguration 4103414242, // Divinity ], sourceHashes: [ 160129377, // Source: "King's Fall" Raid 557146120, // Source: Complete a Guided Game as a guide or seeker. 596084342, // Source: "The Desert Perpetual" Raid 654652973, // Guide 25 Last Wish encounters 707740602, // Guide 10 Last Wish encounters 866530798, // Source: "Not a Scratch" Triumph 1007078046, // Source: "Vow of the Disciple" Raid 1405897559, // Source: "Deep Stone Crypt" Raid 1483048674, // Source: Complete the "Scourge of the Past" raid. 1491707941, // Source: "Garden of Salvation" Raid 1675483099, // Source: Leviathan, Spire of Stars raid lair. 1692165595, // Source: "Rock Bottom" Triumph 1897187034, // Source: "Crota's End" Raid 2065138144, // Source: "Vault of Glass" Raid 2085016678, // Source: Complete the "Scourge of the Past" raid within the first 24 hours after its launch. 2127551856, // Source: "The Desert Perpetual" Epic Raid 2399751101, // Acquired from the raid "Crown of Sorrow." 2455011338, // Source: Last Wish raid. 2653618435, // Source: Leviathan raid. 2700267533, // Source: "Salvation's Edge" Raid 2723305286, // Source: Raid Ring Promotional Event 2765304727, // Source: Leviathan raid on Prestige difficulty. 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. 2882367429, // Source: Eververse\nComplete the "Vault of Glass" raid to unlock this in Eververse. 2937902448, // Source: Leviathan, Eater of Worlds raid lair. 3098906085, // Source: Complete a Guided Game raid as a guide. 3147603678, // Acquired from the raid "Crown of Sorrow." 3190710249, // Source: "Root of Nightmares" Raid 3390269646, // Source: Guided Games Final Encounters 3807243511, // Source: Raid Chests 4009509410, // Source: Complete challenges in the Leviathan raid. 4066007318, // Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty. 4246883461, // Source: Found in the "Scourge of the Past" raid. ], searchString: [], }, rasputin: { itemHashes: [], sourceHashes: [ 504657809, // Source: Season of the Seraph Activities 1126234343, // Source: Witness Rasputin's Full Power 1497107113, // Source: Seasonal Quest, "Seraph Warsat Network" 1516560855, // Source: Season of the Seraph Grandmaster Nightfall 2230358252, // Source: End-of-Season Event 2422551147, // Source: "Operation Seraph's Shield" Mission 3492941398, // Source: "The Lie" Quest 3567813252, // Source: Season of the Seraph Triumph 3574140916, // Source: Season of the Seraph 3937492340, // Source: Seraph Bounties ], searchString: [], }, reclaim: { itemHashes: [], sourceHashes: [ 2929839827, // Source: Reclaim ], searchString: [], }, renegades: { itemHashes: [], sourceHashes: [ 178383754, // Source: Renegades ], searchString: [], }, revenant: { itemHashes: [], sourceHashes: [ 792439255, // Source: Tonic Laboratory in the Last City 1605890568, // Source: Episode Revenant Seasonal Activities 2463956052, // Source: Vesper's Host 3906217258, // Source: Revenant Fortress ], searchString: [], }, riteofthenine: { itemHashes: [ 14929251, // Long Arm 492673102, // New Pacific Epitaph 749483159, // Prosecutor (Adept) 1050582210, // Greasy Luck (Adept) 1066598837, // Relentless (Adept) 1157220231, // No Survivors (Adept) 1460079227, // Liminal Vigil 1685406703, // Greasy Luck 1773934241, // Judgment 1904170910, // A Sudden Death 1987644603, // Judgment (Adept) 2126543269, // Cold Comfort (Adept) 2129814338, // Prosecutor 2477408004, // Wilderflight (Adept) 2730671571, // Terminus Horizon 2760833884, // Cold Comfort 2764074355, // A Sudden Death (Adept) 2982006965, // Wilderflight 3185151619, // New Pacific Epitaph (Adept) 3329218848, // Judgment (Adept) 3421639790, // Liminal Vigil (Adept) 3681280908, // Relentless 3692140710, // Long Arm (Adept) 4193602194, // No Survivors 4267192886, // Terminus Horizon (Adept) ], sourceHashes: [ 877404349, // Source: Rite of the Nine ], searchString: [], }, 'ritual-weapon': { itemHashes: [ 805677041, // Buzzard 838556752, // Python 847329160, // Edgewise 1179141605, // Felwinter's Lie 1644680957, // Null Composure 2060863616, // Salvager's Salvo 2697058914, // Komodo-4FR 3001205424, // Ecliptic Distaff 3434944005, // Point of the Stag 3535742959, // Randy's Throwing Knife 4184808992, // Adored 4227181568, // Exit Strategy ], sourceHashes: [ 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit ], searchString: [], }, rivenslair: { itemHashes: [ 2563668388, // Scalar Potential 4153087276, // Appetence ], sourceHashes: [ 561126969, // Source: "Starcrossed" Mission 1664308183, // Source: Season of the Wish Activities 4278841194, // Source: Season of the Wish Triumphs ], searchString: [], }, ron: { itemHashes: [], sourceHashes: [ 3190710249, // Source: "Root of Nightmares" Raid ], searchString: [], }, root: { itemHashes: [], sourceHashes: [ 3190710249, // Source: "Root of Nightmares" Raid ], searchString: [], }, rootofnightmares: { itemHashes: [], sourceHashes: [ 3190710249, // Source: "Root of Nightmares" Raid ], searchString: [], }, rotn: { itemHashes: [ 14929251, // Long Arm 492673102, // New Pacific Epitaph 749483159, // Prosecutor (Adept) 1050582210, // Greasy Luck (Adept) 1066598837, // Relentless (Adept) 1157220231, // No Survivors (Adept) 1460079227, // Liminal Vigil 1685406703, // Greasy Luck 1773934241, // Judgment 1904170910, // A Sudden Death 1987644603, // Judgment (Adept) 2126543269, // Cold Comfort (Adept) 2129814338, // Prosecutor 2477408004, // Wilderflight (Adept) 2730671571, // Terminus Horizon 2760833884, // Cold Comfort 2764074355, // A Sudden Death (Adept) 2982006965, // Wilderflight 3185151619, // New Pacific Epitaph (Adept) 3329218848, // Judgment (Adept) 3421639790, // Liminal Vigil (Adept) 3681280908, // Relentless 3692140710, // Long Arm (Adept) 4193602194, // No Survivors 4267192886, // Terminus Horizon (Adept) ], sourceHashes: [ 877404349, // Source: Rite of the Nine ], searchString: [], }, saint14: { itemHashes: [], sourceHashes: [ 2607739079, // Source: A Matter of Time 3404977524, // Source: Contribute to the Empyrean Restoration Effort 4046490681, // Source: Complete the "Global Resonance" Triumph 4267157320, // Source: ??????? ], searchString: [], }, salvationsedge: { itemHashes: [], sourceHashes: [ 2700267533, // Source: "Salvation's Edge" Raid ], searchString: [], }, scourge: { itemHashes: [ 2557722678, // Midnight Smith ], sourceHashes: [ 1483048674, // Source: Complete the "Scourge of the Past" raid. 2085016678, // Source: Complete the "Scourge of the Past" raid within the first 24 hours after its launch. 4246883461, // Source: Found in the "Scourge of the Past" raid. ], searchString: [], }, scourgeofthepast: { itemHashes: [ 2557722678, // Midnight Smith ], sourceHashes: [ 1483048674, // Source: Complete the "Scourge of the Past" raid. 2085016678, // Source: Complete the "Scourge of the Past" raid within the first 24 hours after its launch. 4246883461, // Source: Found in the "Scourge of the Past" raid. ], searchString: [], }, seasonpass: { itemHashes: [], sourceHashes: [ 333761108, // Source: Rewards Pass 450719423, // Source: Season of the Risen 794422188, // Source: Season of the Witch 813075729, // Source: Season of the Deep Vendor Reputation Reward 927967626, // Source: Season of the Deep 1560428737, // Source: Season of Defiance 1593696611, // Source: Season Pass Reward 1763998430, // Source: Season Pass 1838401392, // Source: Earned as a Season Pass reward. 2257836668, // Source: Season of the Deep Fishing 2379344669, // Source: Season Pass 2676881949, // Source: Season of the Haunted 2986594962, // Source: Season of the Wish 3308438907, // Source: Season of Plunder 3574140916, // Source: Season of the Seraph ], searchString: [], }, servitor: { itemHashes: [ 599895591, // Sojourner's Tale 2130875369, // Sojourner's Tale 2434225986, // Shattered Cipher ], sourceHashes: [ 139160732, // Source: Season of the Splicer 277706045, // Source: Season of the Splicer Nightfall Grandmaster 1600754038, // Source: Season of the Splicer Activities 2040801502, // Source: Season of the Splicer Triumph 2694738712, // Source: Season of the Splicer Quest 2967385539, // Source: Season of the Splicer Seasonal Challenges ], searchString: [], }, shatteredthrone: { itemHashes: [ 185321778, // The Eternal Return 814876684, // Wish-Ender 2844014413, // Pallas Galliot ], sourceHashes: [], searchString: [], }, shaxx: { itemHashes: [ 2307365, // The Inquisitor (Adept) 51129316, // The Inquisitor 161675590, // Whistler's Whim (Adept) 303107619, // Tomorrow's Answer (Adept) 501345268, // Shayura's Wrath (Adept) 548809020, // Exalted Truth 627188188, // Eye of Sol 711889599, // Whistler's Whim (Adept) 769099721, // Devil in the Details 825554997, // The Inquisitor (Adept) 854379020, // Astral Horizon (Adept) 874623537, // Cataphract GL3 (Adept) 906840740, // Unwavering Duty 1141586039, // Unexpected Resurgence (Adept) 1201528146, // Exalted Truth (Adept) 1230660649, // Victory's Wreath 1292594730, // The Summoner (Adept) 1321626661, // Eye of Sol (Adept) 1401300690, // Eye of Sol 1574601402, // Whistler's Whim 1661191197, // Disdain for Glitter 1705843397, // Exalted Truth (Adept) 1711056134, // Incisor 1820994983, // The Summoner 1893967086, // Keen Thistle 1968410628, // The Prophet 1973107014, // Igneous Hammer 2022294213, // Shayura's Wrath 2059255495, // Eye of Sol (Adept) 2185327324, // The Inquisitor 2300143112, // Yesterday's Question 2314610827, // Igneous Hammer (Adept) 2330860573, // The Inquisitor (Adept) 2378785953, // Yesterday's Question (Adept) 2414564781, // Punctuation Marks 2420153991, // Made Shaxx Proud 2421180981, // Incisor (Adept) 2588739576, // Crucible Solemnity 2588739578, // Crucible Legacy 2588739579, // Crucible Metallic 2632846356, // Rain of Ashes 2653171212, // The Inquisitor 2653171213, // Astral Horizon 2738601016, // Cataphract GL3 2759251821, // Unwavering Duty (Adept) 2839600459, // Incisor (Adept) 3001205424, // Ecliptic Distaff 3009199534, // Tomorrow's Answer 3019024381, // The Prophet (Adept) 3102421004, // Exalted Truth 3165143747, // Whistler's Whim 3193598749, // The Immortal (Adept) 3332125295, // Aisha's Care (Adept) 3436626079, // Exalted Truth 3444632029, // Unwavering Duty (Adept) 3503560035, // Keen Thistle (Adept) 3624844116, // Unwavering Duty 3920882229, // Exalted Truth (Adept) 3928440584, // Crucible Carmine 3928440585, // Crucible Redjack 3969379530, // Aisha's Care 4005780578, // Unexpected Resurgence 4039572196, // The Immortal 4060882456, // Rubicund Wrap (Ornament) 4248997900, // Incisor ], sourceHashes: [ 164083100, // Source: Display of Supremacy, Weekly Challenge 454115234, // Source: Associated Crucible Quest 598662729, // Source: Reach Glory Rank "Legend" in the Crucible. 705363737, // Source: Heavy Metal: Supremacy 745186842, // Source: Associated Crucible Quest 897576623, // Source: Complete Crucible matches and earn rank-up packages from Lord Shaxx. 929025440, // Acquired by competing in the Crucible during the Prismatic Inferno. 1217831333, // Source: Associated Crucible Quest 1223492644, // Source: Complete the "Reconnaissance by Fire" quest. 1465057711, // Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit) 1494513645, // Source: Glory Matches in Crucible 2055470113, // Source: Chance to acquire when completing Crucible Survival matches after reaching Glory Rank "Mythic." 2537301256, // Source: Glory Rank of "Fabled" in Crucible 2558941813, // Source: Place Silver III Division or Higher in Ranked Crucible Playlists 2622122683, // Source: Lord Shaxx Rank Up Reputation 2641169841, // Source: Purchase from Lord Shaxx 2658055900, // Source: Complete the "Season 8: Battle Drills" quest. 2669524419, // Source: Crucible 2821852478, // Source: Complete this weapon's associated Crucible quest. 2915991372, // Source: Crucible 3020288414, // Source: Crucible 3226099405, // Source: Crucible Seasonal Ritual Rank Reward 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit 3466789677, // Source: Place Ascendant III Division or Higher in Ranked Crucible Playlists 3656787928, // Source: Crucible Salvager's Salvo Armament ], searchString: [], }, shipwright: { itemHashes: [], sourceHashes: [ 96303009, // Source: Purchased from Amanda Holliday. ], searchString: [], }, sonar: { itemHashes: [ 1081724548, // Rapacious Appetite 1769847435, // A Distant Pull 3016891299, // Different Times 3890055324, // Targeted Redaction 4066778670, // Thin Precipice ], sourceHashes: [ 813075729, // Source: Season of the Deep Vendor Reputation Reward 927967626, // Source: Season of the Deep 2257836668, // Source: Season of the Deep Fishing 2671038131, // Season of the Deep - WEAPONS 2755511565, // Source: Season of the Deep Triumph 2811716495, // Source: Season of the Deep Activities 2959452483, // Season of the Deep - WEAPONS ], searchString: [], }, sos: { itemHashes: [], sourceHashes: [ 1675483099, // Source: Leviathan, Spire of Stars raid lair. 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. ], searchString: [], }, sotp: { itemHashes: [ 2557722678, // Midnight Smith ], sourceHashes: [ 1483048674, // Source: Complete the "Scourge of the Past" raid. 2085016678, // Source: Complete the "Scourge of the Past" raid within the first 24 hours after its launch. 4246883461, // Source: Found in the "Scourge of the Past" raid. ], searchString: [], }, sotw: { itemHashes: [], sourceHashes: [ 1597738585, // Source: "Spire of the Watcher" Dungeon ], searchString: [], }, spireofstars: { itemHashes: [], sourceHashes: [ 1675483099, // Source: Leviathan, Spire of Stars raid lair. 2812190367, // Source: Leviathan, Spire of Stars raid lair on Prestige difficulty. ], searchString: [], }, spireofthewatcher: { itemHashes: [], sourceHashes: [ 1597738585, // Source: "Spire of the Watcher" Dungeon ], searchString: [], }, strikes: { itemHashes: [ 42874240, // Uzume RR4 192784503, // Pre Astyanax IV 213264394, // Buzzard 233635202, // Cruel Mercy 274843196, // Vanguard Unyielding 772231794, // Hung Jury SR4 781498181, // Persuader 1151688091, // Undercurrent 1296429091, // Deadpan Delivery 1332123064, // Wild Style 1661191186, // Disdain for Gold 1821529912, // Warden's Law 1854753404, // Wendigo GL3 1854753405, // The Militia's Birthright 1974641289, // Nightshade 1999754402, // The Showrunner 2152484073, // Warden's Law 2298039571, // Rake Angle 2322926844, // Shadow Price 2450917538, // Uzume RR4 2523776412, // Vanguard Burnished Steel 2523776413, // Vanguard Steel 2588647361, // Consequence of Duty 2591257541, // Scintillation 2759590322, // THE SWARM 2788911997, // Vanguard Divide 2788911998, // Vanguard Metallic 2788911999, // Vanguard Veteran 2876244791, // The Palindrome 2889501828, // The Slammer 2932922810, // Pre Astyanax IV 3001205424, // Ecliptic Distaff 3125454907, // Loaded Question 3183283212, // Wendigo GL3 3215252549, // Determination 3293524502, // PLUG ONE.1 3667553455, // BrayTech Osprey 3686538757, // Undercurrent 3832743906, // Hung Jury SR4 3922217119, // Lotus-Eater 4060882458, // Balistraria Wrap (Ornament) ], sourceHashes: [ 288436121, // Source: Associated Vanguard Quest 351235593, // Source: Eliminate Prison of Elders escapees found in strikes. 412991783, // Source: Strikes 539840256, // Source: Associated Vanguard Quest 1144274899, // Source: Complete this weapon's associated Vanguard quest. 1216155659, // Source: Complete the "Season 8: First Watch" quest. 1244908294, // Source: Complete the "Loaded Question" quest from Zavala. 1433518193, // Source: Vanguard Salvager's Salvo Armament Quest 1465057711, // Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit) 1564061133, // Source: Associated Vanguard Quest 2124937714, // Source: Zavala Rank Up Reputation 2317365255, // Source: Complete the "A Loud Racket" quest. 2335095658, // Source: Strikes 2527168932, // Source: Complete strikes and earn rank-up packages from Commander Zavala. 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit ], searchString: [], }, sundered: { itemHashes: [ 1303313141, // Unsworn ], sourceHashes: [ 2607970476, // Source: Sundered Doctrine ], searchString: [], }, sundereddoctrine: { itemHashes: [ 1303313141, // Unsworn ], sourceHashes: [ 2607970476, // Source: Sundered Doctrine ], searchString: [], }, sundial: { itemHashes: [], sourceHashes: [ 1618754228, // Source: Sundial Activity on Mercury 2627087475, // Source: Obelisk Bounties and Resonance Rank Increases Across the System ], searchString: [], }, tangled: { itemHashes: [ 1226584228, // Tangled Rust 1226584229, // Tangled Bronze 4085986809, // Secret Treasure ], sourceHashes: [ 110159004, // Source: Complete Nightfall strike "Warden of Nothing." 798957490, // Source: Complete wanted escapee bounties for the Spider. 1771326504, // Source: Complete activities and earn rank-up packages on the Tangled Shore. 2805208672, // Source: Complete Nightfall strike "The Hollowed Lair." 4140654910, // Source: Eliminate all Barons on the Tangled Shore. ], searchString: [], }, throneworld: { itemHashes: [ 2721157927, // Tarnation ], sourceHashes: [ 1141831282, // Source: "Of Queens and Worms" Exotic Quest 1823766625, // Source: "Vox Obscura" Exotic Quest 3954922099, // Source: Exploring the Throne World ], searchString: [], }, titan: { itemHashes: [], sourceHashes: [ 194661944, // Source: Adventure "Siren Song" on Saturn's Moon, Titan 354493557, // Source: Complete Nightfall strike "Savathûn's Song." 482012099, // Source: Adventure "Thief of Thieves" on Saturn's Moon, Titan 636474187, // Source: Adventure "Deathless" on Saturn's Moon, Titan 3534706087, // Source: Complete activities and earn rank-up packages on Saturn's Moon, Titan. ], searchString: [], }, trials: { itemHashes: [], sourceHashes: [ 139599745, // Source: Earn seven wins on a single Trials ticket. 443793689, // Source: Win games on a completed Lighthouse Passage after earning a weekly win streak of five or higher. 486819617, // Trials of Osiris - WEAPONS 613791463, // Source: Saint-14 Rank Up Reputation 752988954, // Source: Flawless Chest in Trials of Osiris 827839814, // Source: Flawless Chest in Trials of Osiris or Grandmaster Nightfalls 1218637862, // Source: Open the Lighthouse chest after earning a weekly win streak of five or higher. 1607607347, // Source: Complete Trials tickets and earn rank-up packages from the Emissary of the Nine. 1923289424, // Source: Open the Lighthouse chest. 2857787138, // Source: Trials of Osiris 3390015730, // Source: Trials of Osiris Challenges 3471208558, // Source: Trials of Osiris Wins 3543690049, // Source: Complete a flawless Trials ticket. 3564069447, // Source: Flawless with a "Flight of the Pigeon" medal for each win ], searchString: [], }, umbral: { itemHashes: [], sourceHashes: [ 287889699, // Source: Umbral Engram Tutorial 1286883820, // Source: Prismatic Recaster ], searchString: [], }, vaultofglass: { itemHashes: [], sourceHashes: [ 2065138144, // Source: "Vault of Glass" Raid ], searchString: [], }, vesper: { itemHashes: [], sourceHashes: [ 2463956052, // Source: Vesper's Host ], searchString: [], }, vespershost: { itemHashes: [], sourceHashes: [ 2463956052, // Source: Vesper's Host ], searchString: [], }, vexoffensive: { itemHashes: [ 351285766, // Substitutional Alloy Greaves 377757362, // Substitutional Alloy Hood 509561140, // Substitutional Alloy Gloves 509561142, // Substitutional Alloy Gloves 509561143, // Substitutional Alloy Gloves 695795213, // Substitutional Alloy Helm 844110491, // Substitutional Alloy Gloves 1137424312, // Substitutional Alloy Cloak 1137424314, // Substitutional Alloy Cloak 1137424315, // Substitutional Alloy Cloak 1348357884, // Substitutional Alloy Gauntlets 1584183805, // Substitutional Alloy Cloak 1721943440, // Substitutional Alloy Boots 1721943441, // Substitutional Alloy Boots 1721943442, // Substitutional Alloy Boots 1855720513, // Substitutional Alloy Vest 1855720514, // Substitutional Alloy Vest 1855720515, // Substitutional Alloy Vest 2096778461, // Substitutional Alloy Strides 2096778462, // Substitutional Alloy Strides 2096778463, // Substitutional Alloy Strides 2468603405, // Substitutional Alloy Plate 2468603406, // Substitutional Alloy Plate 2468603407, // Substitutional Alloy Plate 2657028416, // Substitutional Alloy Vest 2687273800, // Substitutional Alloy Grips 2690973101, // Substitutional Alloy Hood 2690973102, // Substitutional Alloy Hood 2690973103, // Substitutional Alloy Hood 2742760292, // Substitutional Alloy Plate 2761292744, // Substitutional Alloy Bond 2815379657, // Substitutional Alloy Bond 2815379658, // Substitutional Alloy Bond 2815379659, // Substitutional Alloy Bond 2903026872, // Substitutional Alloy Helm 2903026873, // Substitutional Alloy Helm 2903026874, // Substitutional Alloy Helm 2942269704, // Substitutional Alloy Gauntlets 2942269705, // Substitutional Alloy Gauntlets 2942269707, // Substitutional Alloy Gauntlets 3166926328, // Substitutional Alloy Robes 3166926330, // Substitutional Alloy Robes 3166926331, // Substitutional Alloy Robes 3192738009, // Substitutional Alloy Greaves 3192738010, // Substitutional Alloy Greaves 3192738011, // Substitutional Alloy Greaves 3364258850, // Substitutional Alloy Strides 3680920565, // Substitutional Alloy Robes 3757338780, // Substitutional Alloy Mark 3757338782, // Substitutional Alloy Mark 3757338783, // Substitutional Alloy Mark 3911047865, // Substitutional Alloy Mark 4013678605, // Substitutional Alloy Boots 4026120124, // Substitutional Alloy Grips 4026120125, // Substitutional Alloy Grips 4026120127, // Substitutional Alloy Grips 4070722289, // Substitutional Alloy Mask 4078925540, // Substitutional Alloy Mask 4078925541, // Substitutional Alloy Mask 4078925542, // Substitutional Alloy Mask ], sourceHashes: [ 4122810030, // Source: Complete seasonal activities during Season of the Undying. ], searchString: [], }, vog: { itemHashes: [], sourceHashes: [ 2065138144, // Source: "Vault of Glass" Raid ], searchString: [], }, votd: { itemHashes: [], sourceHashes: [ 1007078046, // Source: "Vow of the Disciple" Raid ], searchString: [], }, vow: { itemHashes: [], sourceHashes: [ 1007078046, // Source: "Vow of the Disciple" Raid ], searchString: [], }, vowofthedisciple: { itemHashes: [], sourceHashes: [ 1007078046, // Source: "Vow of the Disciple" Raid ], searchString: [], }, warlordsruin: { itemHashes: [], sourceHashes: [ 613435025, // Source: "Warlord's Ruin" Dungeon ], searchString: [], }, wartable: { itemHashes: [], sourceHashes: [ 2653840925, // Source: Challenger's Proving VII Quest 3818317874, // Source: War Table Reputation Reset 4079816474, // Source: War Table ], searchString: [], }, watcher: { itemHashes: [], sourceHashes: [ 1597738585, // Source: "Spire of the Watcher" Dungeon ], searchString: [], }, wellspring: { itemHashes: [], sourceHashes: [ 82267399, // Source: "Warden of the Spring" Triumph 502279466, // Source: Wellspring Boss Vezuul, Lightflayer 2917218318, // Source: Wellspring Boss Golmag, Warden of the Spring 3359853911, // Source: Wellspring Boss Zeerik, Lightflayer 3411812408, // Source: "All the Spring's Riches" Triumph 3450213291, // Source: Wellspring Boss Bor'gong, Warden of the Spring ], searchString: [], }, wrathborn: { itemHashes: [ 197764097, // Wild Hunt Boots 238284968, // Wild Hunt Strides 251310542, // Wild Hunt Hood 317220729, // Wild Hunt Vestment 1148770067, // Wild Hunt Cloak 1276513983, // Wild Hunt Gloves 1458739906, // Wild Hunt Vest 2025716654, // Wild Hunt Grasps 2055947316, // Wild Hunt Bond 2279193565, // Wild Hunt Mark 2453357042, // Blast Battue 2545401128, // Wild Hunt Gauntlets 2776503072, // Royal Chase 3180809346, // Wild Hunt Greaves 3351935136, // Wild Hunt Plate 3887272785, // Wild Hunt Helm 4079117607, // Wild Hunt Mask ], sourceHashes: [ 841568343, // Source: "Hunt for the Wrathborn" Quest 3107094548, // Source: "Coup de Grâce" Mission ], searchString: [], }, zavala: { itemHashes: [ 42874240, // Uzume RR4 192784503, // Pre Astyanax IV 213264394, // Buzzard 233635202, // Cruel Mercy 274843196, // Vanguard Unyielding 772231794, // Hung Jury SR4 781498181, // Persuader 1151688091, // Undercurrent 1296429091, // Deadpan Delivery 1332123064, // Wild Style 1661191186, // Disdain for Gold 1821529912, // Warden's Law 1854753404, // Wendigo GL3 1854753405, // The Militia's Birthright 1974641289, // Nightshade 1999754402, // The Showrunner 2152484073, // Warden's Law 2298039571, // Rake Angle 2322926844, // Shadow Price 2450917538, // Uzume RR4 2523776412, // Vanguard Burnished Steel 2523776413, // Vanguard Steel 2588647361, // Consequence of Duty 2591257541, // Scintillation 2759590322, // THE SWARM 2788911997, // Vanguard Divide 2788911998, // Vanguard Metallic 2788911999, // Vanguard Veteran 2876244791, // The Palindrome 2889501828, // The Slammer 2932922810, // Pre Astyanax IV 3001205424, // Ecliptic Distaff 3125454907, // Loaded Question 3183283212, // Wendigo GL3 3215252549, // Determination 3293524502, // PLUG ONE.1 3667553455, // BrayTech Osprey 3686538757, // Undercurrent 3832743906, // Hung Jury SR4 3922217119, // Lotus-Eater 4060882458, // Balistraria Wrap (Ornament) ], sourceHashes: [ 288436121, // Source: Associated Vanguard Quest 351235593, // Source: Eliminate Prison of Elders escapees found in strikes. 412991783, // Source: Strikes 539840256, // Source: Associated Vanguard Quest 1144274899, // Source: Complete this weapon's associated Vanguard quest. 1216155659, // Source: Complete the "Season 8: First Watch" quest. 1244908294, // Source: Complete the "Loaded Question" quest from Zavala. 1433518193, // Source: Vanguard Salvager's Salvo Armament Quest 1465057711, // Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit) 1564061133, // Source: Associated Vanguard Quest 2124937714, // Source: Zavala Rank Up Reputation 2317365255, // Source: Complete the "A Loud Racket" quest. 2335095658, // Source: Strikes 2527168932, // Source: Complete strikes and earn rank-up packages from Commander Zavala. 3299964501, // Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists 3348906688, // Source: Ranks in Vanguard Strikes, Crucible, or Gambit ], searchString: [], }, }; export default D2Sources; ================================================ FILE: src/data/d2/source-to-season-v2.json ================================================ { "11666839": 2, "13912404": 1, "21494224": 26, "32323943": 11, "43842395": 22, "92433064": 18, "100617404": 2, "110159004": 4, "139160732": 15, "139599745": 1, "148542898": 2, "160129377": 18, "164083100": 15, "178383754": 28, "210885364": 13, "266896577": 5, "286427063": 12, "287889699": 13, "351235593": 4, "354493557": 2, "406406003": 14, "409652252": 16, "443340273": 15, "443793689": 26, "462484651": 14, "464727567": 15, "508245276": 13, "539840256": 10, "547767158": 2, "550270332": 8, "557146120": 1, "561126969": 23, "569214265": 1, "594760007": 12, "596084342": 27, "613435025": 23, "613791463": 22, "629617846": 12, "633667627": 27, "641018908": 3, "654652973": 4, "675740011": 15, "677167936": 1, "707740602": 4, "709680645": 20, "712662541": 27, "736336644": 4, "745186842": 9, "745481267": 26, "772619302": 1, "794422188": 22, "817015032": 2, "877404349": 26, "887452441": 13, "901482731": 20, "925197669": 5, "929025440": 7, "958460845": 24, "1007078046": 16, "1054169368": 15, "1076222895": 1, "1102533392": 15, "1103518848": 3, "1126234343": 10, "1127923611": 4, "1148859274": 12, "1175566043": 2, "1216155659": 8, "1217831333": 10, "1225476079": 19, "1253026984": 8, "1266018974": 25, "1281387702": 1, "1282207663": 17, "1331532890": 27, "1360005982": 1, "1397119901": 1, "1400219831": 2, "1405897559": 12, "1411886787": 2, "1412777465": 4, "1433518193": 13, "1457456824": 5, "1459595344": 8, "1462687159": 1, "1465057711": 15, "1465990789": 5, "1483048674": 5, "1497107113": 10, "1505938361": 27, "1564061133": 9, "1568732528": 23, "1581680964": 2, "1581731027": 24, "1596507419": 5, "1597738585": 19, "1605890568": 25, "1618754228": 9, "1654120320": 2, "1666677522": 17, "1677921161": 4, "1723452413": 12, "1745960977": 8, "1751739544": 20, "1763998430": 15, "1792957897": 26, "1897187034": 22, "1919933822": 11, "1923289424": 10, "1924238751": 3, "1926923633": 22, "1943976384": 26, "1992319882": 20, "1995616326": 14, "2011810450": 13, "2039343154": 11, "2045032171": 27, "2050870152": 24, "2085016678": 5, "2124937714": 22, "2127551856": 27, "2187511136": 6, "2206233229": 1, "2223404774": 1, "2230358252": 11, "2242939082": 2, "2278847330": 20, "2292685703": 11, "2296534980": 24, "2308290458": 2, "2335095658": 11, "2347293565": 2, "2353223954": 11, "2363489105": 16, "2364515524": 19, "2364933290": 14, "2379344669": 11, "2384327872": 5, "2463956052": 25, "2487203690": 2, "2514060836": 24, "2541753910": 5, "2585665369": 23, "2601524261": 9, "2607739079": 10, "2607970476": 26, "2622122683": 22, "2648408612": 7, "2658055900": 8, "2669524419": 11, "2675385179": 4, "2694738712": 14, "2700267533": 24, "2717017239": 2, "2723305286": 11, "2744321951": 1, "2755511565": 21, "2765304727": 1, "2778435282": 8, "2797674516": 15, "2805208672": 4, "2851783112": 4, "2856954949": 12, "2926805810": 3, "2927095256": 28, "2929562373": 1, "2929839827": 27, "2937902448": 2, "2952071500": 23, "2966694626": 5, "2967385539": 14, "2986594962": 23, "2988465950": 1, "3022766747": 4, "3041847664": 20, "3047033583": 5, "3067146211": 2, "3079246067": 2, "3094114967": 15, "3095773956": 26, "3098906085": 4, "3099553329": 1, "3100467592": 1, "3112857249": 1, "3126774631": 2, "3174947771": 20, "3190710249": 20, "3190938946": 8, "3226099405": 14, "3237053501": 27, "3247513834": 28, "3257722699": 5, "3277652589": 15, "3288974535": 21, "3390269646": 3, "3404977524": 9, "3422985544": 10, "3431853656": 1, "3466789677": 28, "3494247523": 8, "3522070610": 11, "3563833902": 16, "3564069447": 21, "3567813252": 19, "3656787928": 13, "3693722471": 11, "3704442923": 2, "3724111213": 7, "3736521079": 1, "3740731576": 18, "3747711246": 22, "3764925750": 5, "3807243511": 12, "3829951162": 27, "3874934421": 4, "3936473457": 2, "3942778906": 22, "3954922099": 16, "4006434081": 20, "4008954452": 25, "4009509410": 1, "4066007318": 2, "4079816474": 13, "4122810030": 8, "4173145322": 16, "4208190159": 1, "4247521481": 5, "4263201695": 2, "4267157320": 9, "4278841194": 23, "4284811963": 27, "4288102251": 2, "4290227252": 5, "4290499613": 1 } ================================================ FILE: src/data/d2/sources.json ================================================ { "10464158": "Source: Acquired from Xûr", "11666839": "Source: High-Difficulty Activities or Dismantled Exotic or Legendary Gear", "13912404": "Source: Unlock Your Arc Subclass", "21494224": "Source: Offer the correct final answer in an uncharted space.", "32323943": "Source: Moments of Triumph", "43842395": "Source: Low chance to drop from defeating combatants or opening chests.", "82267399": "Source: \"Warden of the Spring\" Triumph", "92433064": "Source: Star Chart Reputation Reset", "96303009": "Source: Purchased from Amanda Holliday.", "100617404": "Requires Titan Class", "110159004": "Source: Complete Nightfall strike \"Warden of Nothing.\"", "139160732": "Source: Season of the Splicer", "139599745": "Source: Earn seven wins on a single Trials ticket.", "146504277": "Source: Earn rank-up packages from Arach Jalaal.", "148542898": "Source: Equip the full Mercury destination set on a Warlock.", "151416041": "Source: Solstice", "160129377": "Source: \"King's Fall\" Raid", "164083100": "Source: Display of Supremacy, Weekly Challenge", "164571094": "Source: World Quest \"Exodus Black\" on Nessus.", "178383754": "Source: Renegades", "186854335": "Source: Gambit", "194661944": "Source: Adventure \"Siren Song\" on Saturn's Moon, Titan", "210885364": "Source: Flawless \"Presage\" Exotic Quest on Master Difficulty", "266896577": "Source: Solve the Norse glyph puzzle.", "269962496": "Source: Eververse", "276398507": "Source: High-Difficulty Activities", "277706045": "Source: Season of the Splicer Nightfall Grandmaster", "281362298": "Source: Strider Exotic Quest", "286427063": "Source: Fallen Empire Campaign", "287889699": "Source: Umbral Engram Tutorial", "288436121": "Source: Associated Vanguard Quest", "315474873": "Source: Complete activities and earn rank-up packages on Io.", "333761108": "Source: Rewards Pass", "339937514": "Source: Xûr at the Tower", "351235593": "Source: Eliminate Prison of Elders escapees found in strikes.", "354493557": "Source: Complete Nightfall strike \"Savathûn's Song.\"", "406406003": "Source: Exchange for Synthcord at the Loom", "409652252": "Source: The Witch Queen Campaign", "412991783": "Source: Strikes", "431243768": "Source: The Edge of Fate Campaign", "439994003": "Source: Complete the \"Master Smith\" Triumph.", "443340273": "Source: Xûr's Treasure Hoard in Eternity", "443793689": "Source: Win games on a completed Lighthouse Passage after earning a weekly win streak of five or higher.", "450719423": "Source: Season of the Risen", "454115234": "Source: Associated Crucible Quest", "454251931": "Source: \"What Remains\" Exotic Quest", "460742691": "Requires Guardian Rank 6: Masterwork Weapons", "462484651": "Source: Threader Bounties from Ada-1", "464727567": "Source: Dawning 2021", "482012099": "Source: Adventure \"Thief of Thieves\" on Saturn's Moon, Titan", "483798855": "Source: \"The Final Strand\" Exotic Quest", "486819617": "", "502279466": "Source: Wellspring Boss Vezuul, Lightflayer", "504657809": "Source: Season of the Seraph Activities", "506073192": "Source: \"Prophecy\" Dungeon", "508245276": "Source: Season of the Hunt", "536806855": "Source: Episode: Echoes", "539840256": "Source: Associated Vanguard Quest", "547767158": "Source: Dawning 2018", "550270332": "Source: Complete all Nightmare Hunt time trials on Master difficulty.", "557146120": "Source: Complete a Guided Game as a guide or seeker.", "561111210": "Source: Iron Banner Salvager's Salvo Armament", "561126969": "Source: \"Starcrossed\" Mission", "569214265": "Source: Red War Campaign", "571102497": "Source: Associated Gambit Quest", "594760007": "Source: \"Forging Your Own Path\" Quest", "594786771": "Source: Complete this weapon's associated Gambit quest.", "596084342": "Source: \"The Desert Perpetual\" Raid", "598662729": "Source: Reach Glory Rank \"Legend\" in the Crucible.", "611838069": "Source: Guardian Games", "613435025": "Source: \"Warlord's Ruin\" Dungeon", "613791463": "Source: Saint-14 Rank Up Reputation", "620369433": "Source: Season of the Haunted Triumph", "629617846": "Source: Dawning 2020", "633667627": "Requires Tier 4 or 5 Weapon", "636474187": "Source: Adventure \"Deathless\" on Saturn's Moon, Titan", "639650067": "Source: Limited Edition of Destiny 2.", "641018908": "Source: Solstice 2018", "654652973": "Guide 25 Last Wish encounters", "675740011": "Source: \"Grasp of Avarice\" Dungeon", "677167936": "Source: Complete the campaign as a Warlock.", "705363737": "Source: Heavy Metal: Supremacy", "707740602": "Guide 10 Last Wish encounters", "709680645": "Source: \"Truly Satisfactory\" Triumph", "712662541": "Requires Season 27 Tier 5 Weapon", "736336644": "Source: \"A Spark of Hope\" Quest", "745186842": "Source: Associated Crucible Quest", "745481267": "Source: Intrinsic Iteration Triumph", "752988954": "Source: Flawless Chest in Trials of Osiris", "772619302": "Completed all 8 Moments of Triumph in Destiny's second year.", "783399508": "Source: Adventure \"Supply and Demand\" in the European Dead Zone", "790152021": "Source: Season of Plunder Triumph", "790433146": "Source: Adventure \"Dark Alliance\" in the European Dead Zone", "792439255": "Source: Tonic Laboratory in the Last City", "794422188": "Source: Season of the Witch", "798957490": "Source: Complete wanted escapee bounties for the Spider.", "813075729": "Source: Season of the Deep Vendor Reputation Reward", "817015032": "Source: Complete Nightfall strike \"The Inverted Spire.\"", "827839814": "Source: Flawless Chest in Trials of Osiris or Grandmaster Nightfalls", "840425455": "Source: \"Legendary Trifecta\" Triumph", "841568343": "Source: \"Hunt for the Wrathborn\" Quest", "860666126": "Source: Nightfall", "860688654": "Source: Eververse", "866530798": "Source: \"Not a Scratch\" Triumph", "877404349": "Source: Rite of the Nine", "887452441": "Source: Gambit Salvager's Salvo Armament", "894030814": "Source: Heavy Metal Event", "897576623": "Source: Complete Crucible matches and earn rank-up packages from Lord Shaxx.", "901482731": "Source: Lightfall Campaign", "918840100": "Source: Shadowkeep Campaign", "923678151": "Source: Upgraded Event Card Reward", "923708784": "Requires Guardian Rank 7: Threats and Surges", "925197669": "Source: Complete a Bergusia Forge ignition.", "927967626": "Source: Season of the Deep", "929025440": "Acquired by competing in the Crucible during the Prismatic Inferno.", "941123623": "", "948753311": "Source: Found by completing Volundr Forge ignitions.", "958460845": "Source: The Final Shape Campaign", "976328308": "Source: The Derelict Leviathan", "1007078046": "Source: \"Vow of the Disciple\" Raid", "1027607603": "Source: Associated Iron Banner Quest", "1035822060": "Source: Randomly rewarded from completing Vanguard Ops, Crucible, or Gambit playlist activities.", "1036506031": "Source: Complete activities and earn rank-up packages on Mars.", "1054169368": "Source: Festival of the Lost 2021", "1067250718": "Source: Adventure \"Arecibo\" on Io", "1076222895": "Source: Defeat bosses in Flashpoints.", "1078340058": "Past Is Prologue", "1085506849": "Source: \"We Stand Unbroken\" Quest, Week 8", "1102533392": "Source: Xûr (Eternity)", "1103518848": "Source: Earned over the course of the Warmind campaign.", "1118966764": "Source: Dismantle an item with this shader applied to it.", "1126234343": "Source: Witness Rasputin's Full Power", "1127923611": "Source: 3 Gambit Rank Resets in a Season", "1141831282": "Source: \"Of Queens and Worms\" Exotic Quest", "1144274899": "Source: Complete this weapon's associated Vanguard quest.", "1148859274": "Source: Exploring Europa", "1162859311": "Source: Complete the \"Clean Getaway\" quest.", "1175566043": "Source: Complete Nightfall strike \"A Garden World.\"", "1186140085": "Source: Adventure \"Unbreakable\" on Nessus", "1216155659": "Source: Complete the \"Season 8: First Watch\" quest.", "1217831333": "Source: Associated Crucible Quest", "1218637862": "Source: Open the Lighthouse chest after earning a weekly win streak of five or higher.", "1223492644": "Source: Complete the \"Reconnaissance by Fire\" quest.", "1225476079": "Source: Moments of Triumph 2022", "1232061833": "Source: Pinnacle Ops", "1232863328": "Source: Moments of Triumph 2024", "1244908294": "Source: Complete the \"Loaded Question\" quest from Zavala.", "1253026984": "Source: Among the lost Ghosts of the Moon.", "1266018974": "A collection of memories made real by the Traveler's Light.", "1281387702": "Source: Unlock Your Void Subclass", "1282207663": "Source: Dungeon \"Duality\"", "1283862526": "Source: Season of the Haunted Nightfall Grandmaster", "1286332045": "Source: Found by completing Izanami Forge ignitions.", "1286883820": "Source: Prismatic Recaster", "1289998337": "Source: Adventure \"Hack the Planet\" on Nessus", "1299614150": "Source: [REDACTED] on Mars.", "1302157812": "Source: Wild Card Exotic Quest", "1309588429": "Source: \"Chief Investigator\" Triumph", "1312894505": "Source: Iron Banner", "1331532890": "Source: Seasonal Conquest Triumph \"Ultimate Victory\"", "1341921330": "Source: Episode: Heresy Activities", "1358645302": "Source: Unlocked by a special offer.", "1360005982": "Completed a Moment of Triumph in Destiny's second year.", "1373504223": "Acquired from completing a Season of Opulence Triumph.", "1373723300": "Source: Complete activities and earn rank-up packages in the EDZ.", "1388323447": "Source: Exotic Mission \"The Whisper\"", "1394793197": "Source: \"Magnum Opus\" Quest", "1397119901": "Completed a Moment of Triumph in Destiny's first year.", "1400219831": "Source: Equip the full Mercury destination set on a Hunter.", "1405897559": "Source: \"Deep Stone Crypt\" Raid", "1411886787": "Source: Equip the full Mercury destination set on a Titan.", "1412777465": "Source: Forsaken Refer-a-Friend", "1416471099": "Source: Moments of Triumph 2023", "1433518193": "Source: Vanguard Salvager's Salvo Armament Quest", "1457456824": "Source: Complete the \"Reunited Siblings\" Triumph.", "1459595344": "Source: Purchase from Banshee-44 or Ada-1", "1462687159": "Reached level 5 in the Ages of Triumph record book.", "1464399708": "Source: Earn rank-up packages from Executor Hideo.", "1465057711": "Source: Standard Ritual Playlist. (Vanguard Ops, Crucible, Gambit)", "1465990789": "Source: Solve the Japanese glyph puzzle.", "1476475066": "Source: \"Firmware Update\" Triumph", "1483048674": "Source: Complete the \"Scourge of the Past\" raid.", "1491707941": "Source: \"Garden of Salvation\" Raid", "1492981395": "Source: \"The Stasis Prototype\" Quest", "1494513645": "Source: Glory Matches in Crucible", "1497107113": "Source: Seasonal Quest, \"Seraph Warsat Network\"", "1505938361": "Source: Call to Arms Event", "1516560855": "Source: Season of the Seraph Grandmaster Nightfall", "1527887247": "Source: Adventure \"Red Legion, Black Oil\" in the European Dead Zone", "1560428737": "Source: Season of Defiance", "1563875874": "Source: Exotic engrams; extremely rare world drops.", "1564061133": "Source: Associated Vanguard Quest", "1568732528": "Source: Guardian Games 2024", "1581680964": "Source: Complete Nightfall strike \"Tree of Probabilities.\"", "1581731027": "Awoken and Ahamkara magics mingle in the mist rising from this censer as it swings back and forth, back and forth…", "1593696611": "Source: Season Pass Reward", "1596489410": "Source: Season of the Risen Nightfall Grandmaster", "1596507419": "Source: Complete a Gofannon Forge ignition.", "1597738585": "Source: \"Spire of the Watcher\" Dungeon", "1600754038": "Source: Season of the Splicer Activities", "1605890568": "Source: Episode Revenant Seasonal Activities", "1607607347": "Source: Complete Trials tickets and earn rank-up packages from the Emissary of the Nine.", "1618699950": "Source: Season of the Lost Nightfall Grandmaster", "1618754228": "Source: Sundial Activity on Mercury", "1654120320": "Source: Complete activities and earn rank-up packages on Mercury.", "1664308183": "Source: Season of the Wish Activities", "1666677522": "Source: Solstice", "1675483099": "Source: Leviathan, Spire of Stars raid lair.", "1677921161": "Source: Festival of the Lost 2018.", "1692165595": "Source: \"Rock Bottom\" Triumph", "1701477406": "Source: Flashpoint milestones; Legendary engrams.", "1723452413": "Source: Season of Arrivals", "1730197643": "Source: //node.ovrd.AVALON// Exotic Quest", "1736997121": "Source: Adventure \"Stop and Go\" in the European Dead Zone", "1743434737": "Source: Destiny 2 \"Forsaken\" preorder bonus gift.", "1745960977": "Source: \"Pit of Heresy\" Dungeon", "1749037998": "Source: Nightfall", "1750523507": "Source: Terminal Overload (Ahimsa Park)", "1751739544": "Source: \"We Stand Unbroken\" Quest, Week 5", "1763998430": "Source: Season Pass", "1771326504": "Source: Complete activities and earn rank-up packages on the Tangled Shore.", "1788267693": "Source: Earn rank-up packages from Banshee-44.", "1792957897": "Source: \"Efficient Challenger\" Triumph", "1823766625": "Source: \"Vox Obscura\" Exotic Quest", "1828622510": "Source: Chance to acquire when you win Iron Banner matches.", "1832642406": "Source: World Quest \"Dynasty\" on Io.", "1838401392": "Source: Earned as a Season Pass reward.", "1850609592": "Source: Nightfall", "1861838843": "Source: Adventure \"A Frame Job\" in the European Dead Zone", "1866448829": "Source: Deluxe Edition Bonus", "1897187034": "Source: \"Crota's End\" Raid", "1902517582": "Source: Where's Archie?", "1906492169": "Source: Complete activities and earn rank-up packages on Nessus.", "1919933822": "Source: Festival of the Lost 2020", "1923289424": "Source: Open the Lighthouse chest.", "1924238751": "Source: Complete Nightfall strike \"Will of the Thousands.\"", "1926923633": "Source: Lord Saladin Rank Up Reputation", "1943976384": "A tonic receptacle of Eliksni make, traditionally crafted for high-ranking officials. \n\nGiven to you by Eido of House Light in honor of your role as Slayer Baron.", "1952675042": "Source: Complete Gambit Prime matches and increase your rank.", "1953779156": "Source: Events", "1957611613": "Source: An Exotic quest or challenge.", "1992319882": "Source: Grandmaster Nightfalls", "1995616326": "Source: Season of the Chosen", "1999000205": "Source: Exploring the Moon", "2006303146": "Source: Guardian Games 2022", "2011810450": "Source: Season 13 Guardian Games", "2039343154": "Source: Contact Public Event", "2040548068": "Source: Adventure \"Release\" on Nessus", "2040801502": "Source: Season of the Splicer Triumph", "2045032171": "Source: Arms Week Event", "2050870152": "Source: Solstice", "2055289873": "Source: \"The Evidence Board\" Exotic Quest", "2055470113": "Source: Chance to acquire when completing Crucible Survival matches after reaching Glory Rank \"Mythic.\"", "2062058385": "Source: Crafted in a Black Armory forge.", "2065138144": "Source: \"Vault of Glass\" Raid", "2068312112": "Source: Exotic Mission \"Zero Hour\"", "2075569025": "Source: \"Operation Elbrus\" Mission", "2085016678": "Source: Complete the \"Scourge of the Past\" raid within the first 24 hours after its launch.", "2096915131": "Source: Adventure \"Poor Reception\" in the European Dead Zone", "2124937714": "Source: Zavala Rank Up Reputation", "2127551856": "Source: \"The Desert Perpetual\" Epic Raid", "2170269026": "Source: Complete Gambit matches and earn rank-up packages from the Drifter.", "2171520631": "Source: \"Lost Lament\" Exotic Quest", "2187511136": "Source: Earned during the seasonal Revelry event.", "2203185162": "Source: Solo Expert and Master Lost Sectors", "2206233229": "Source: Follow treasure maps.", "2223404774": "Source: Defeat Combatants, Loot Chests, or Complete Activities", "2230358252": "Source: End-of-Season Event", "2242939082": "Requires Hunter Class", "2257836668": "Source: Season of the Deep Fishing", "2273761598": "Source: Season of the Haunted Activities", "2278847330": "Requires Guardian Rank 3", "2292685703": "Source: \"Xenology\" Quest from Xûr", "2296534980": "Source: Exotic Mission Encore", "2306801178": "Source: Episode: Echoes Activities", "2308290458": "Requires 1,000 Warlock Kills", "2310754348": "Source: World Quest \"Data Recovery\" on Mars.", "2317365255": "Source: Complete the \"A Loud Racket\" quest.", "2327253880": "Source: Exploring the Pale Heart", "2335095658": "Source: Strikes", "2345202459": "Source: Adventure \"Invitation from the Emperor\" on Nessus", "2347293565": "Source: Complete Nightfall strike \"The Arms Dealer.\"", "2353223954": "Source: Season of the Worthy.", "2363489105": "Source: Season of the Risen Vendor or Triumphs", "2364515524": "Source: Dawning 2022", "2364933290": "Source: Gambit Seasonal Ritual Rank Reward", "2376909801": "Source: \"Master\" Triumph in Nightfalls", "2379344669": "Source: Season Pass", "2384327872": "Source: Solve the French glyph puzzle.", "2387628034": "Random Perks: This item cannot be reacquired from Collections.", "2392127416": "Source: Adventure \"Cliffhanger\" on Io", "2399751101": "Acquired from the raid \"Crown of Sorrow.\"", "2422551147": "Source: \"Operation Seraph's Shield\" Mission", "2455011338": "Source: Last Wish raid.", "2463956052": "Source: Vesper's Host", "2473294025": "Source: Guardian Games 2023", "2487203690": "Source: Complete Nightfall strike \"Tree of Probabilities.\"", "2502262376": "Source: Earned during the seasonal Crimson Days event.", "2511152325": "Acquired from the Menagerie aboard the Leviathan.", "2514060836": "Source: Episode: Echoes Enigma Protocol Activity", "2520862847": "Source: Iron Banner Iron-Handed Diplomacy", "2527168932": "Source: Complete strikes and earn rank-up packages from Commander Zavala.", "2537301256": "Source: Glory Rank of \"Fabled\" in Crucible", "2541753910": "Source: Complete the \"Master Blaster\" Triumph.", "2552784968": "Requires Guardian Rank 2", "2553369674": "Source: Adventure \"Exodus Siege\" on Nessus", "2558941813": "Source: Place Silver III Division or Higher in Ranked Crucible Playlists", "2559145507": "Source: Complete activities in the Dreaming City.", "2585665369": "A foreboding staff bearing engravings of Hive runes and bound with mystical charms.", "2601524261": "Source: Associated Gambit Quest", "2607739079": "Source: A Matter of Time", "2607970476": "Source: Sundered Doctrine", "2622122683": "Source: Lord Shaxx Rank Up Reputation", "2627087475": "Source: Obelisk Bounties and Resonance Rank Increases Across the System", "2631398023": "Source: Radiolite Bay Deposits", "2641169841": "Source: Purchase from Lord Shaxx", "2648408612": "Acquired by competing in the Iron Banner when the wolves were loud.", "2653618435": "Source: Leviathan raid.", "2653840925": "Source: Challenger's Proving VII Quest", "2658055900": "Source: Complete the \"Season 8: Battle Drills\" quest.", "2669524419": "Source: Crucible", "2671038131": "", "2675385179": "Source: Purchase from Suraya Hawthorne", "2676881949": "Source: Season of the Haunted", "2694738712": "Source: Season of the Splicer Quest", "2697389955": "Source: \"Neomuna Sightseeing\" Triumph", "2700267533": "Source: \"Salvation's Edge\" Raid", "2717017239": "Source: Complete Nightfall strike \"The Pyramidion.\"", "2723305286": "Source: Raid Ring Promotional Event", "2744321951": "Source: Complete a heroic Public Event.", "2745272818": "Source: \"Presage\" Exotic Quest", "2755511565": "Source: Season of the Deep Triumph", "2763252588": "Source: \"And Out Fly the Wolves\" Quest", "2765304727": "Source: Leviathan raid on Prestige difficulty.", "2778435282": "Source: Nightmare Hunts", "2797674516": "Source: Moments of Triumph 2021", "2805208672": "Source: Complete Nightfall strike \"The Hollowed Lair.\"", "2811716495": "Source: Season of the Deep Activities", "2812190367": "Source: Leviathan, Spire of Stars raid lair on Prestige difficulty.", "2821852478": "Source: Complete this weapon's associated Crucible quest.", "2843045413": "Source: Gambit", "2851783112": "Source: Complete Nightfall strike \"Lake of Shadows.\"", "2856954949": "Source: \"Let Loose Thy Talons\" Exotic Quest", "2857787138": "Source: Trials of Osiris", "2869564842": "Source: \"Vengeful Knife\" Triumph", "2882367429": "Source: Eververse\nComplete the \"Vault of Glass\" raid to unlock this in Eververse.", "2883838366": "Source: Complete the \"Breakneck\" quest from the Drifter.", "2892963218": "Source: Earned while leveling.", "2895784523": "Source: Pledge to all factions on a single character.", "2915991372": "Source: Crucible", "2917218318": "Source: Wellspring Boss Golmag, Warden of the Spring", "2926805810": "Source: Complete Nightfall strike \"Strange Terrain.\"", "2927095256": "Things didn't get interesting until the first apology.", "2929562373": "Source: Unlock Your Solar Subclass", "2929839827": "Source: Reclaim", "2937902448": "Source: Leviathan, Eater of Worlds raid lair.", "2952071500": "Source: Into the Light", "2959452483": "", "2966694626": "Source: Found by solving the mysteries behind the Black Armory's founding families.", "2967385539": "Source: Season of the Splicer Seasonal Challenges", "2968206374": "Source: Earned as a Deluxe Edition bonus.", "2982642634": "Source: Season of Plunder Grandmaster Nightfall", "2985242208": "Source: Earned from a charity promotion.", "2986594962": "Source: Season of the Wish", "2986841134": "Source: Salvager's Salvo Armament Quest", "2988465950": "Source: Planetary faction chests.", "3020288414": "Source: Crucible", "3022766747": "Source: Complete Nightfall strike \"The Insight Terminus.\"", "3041847664": "Source: Exploring Neomuna", "3047033583": "Source: Returned the Obsidian Accelerator.", "3067146211": "Source: Complete Nightfall strike \"Exodus Crash.\"", "3072862693": "Source: Complete Iron Banner matches and earn rank-up packages from Lord Saladin.", "3075817319": "Source: Earn rank-up packages from Ikora Rey.", "3079246067": "Source: Complete Osiris' Lost Prophecies for Brother Vance on Mercury.", "3092212681": "Source: Dawning 2019", "3094114967": "Source: Season of the Lost Ritual Playlists", "3095773956": "Source: Guardian Games 2025", "3098906085": "Source: Complete a Guided Game raid as a guide.", "3099553329": "Source: Complete the campaign as a Titan.", "3100439379": "Source: Mission \"Exorcism\"", "3100467592": "Source: Seasonal Challenges or Repeatable Bounties", "3107094548": "Source: \"Coup de Grâce\" Mission", "3112857249": "Completed all 10 Moments of Triumph in Destiny's first year.", "3125456997": "Source: Europan Tour", "3126774631": "Requires 1,000 Hunter Kills", "3142874552": "Source: Nightfall", "3147603678": "Acquired from the raid \"Crown of Sorrow.\"", "3173463761": "Source: Pre-order Bonus", "3174947771": "Requires Guardian Rank 6: Vendor Challenges", "3190710249": "Source: \"Root of Nightmares\" Raid", "3190938946": "Source: Festival of the Lost 2019", "3212282221": "Source: Forsaken Annual Pass", "3226099405": "Source: Crucible Seasonal Ritual Rank Reward", "3229688794": "Source: Grandmaster Nightfall", "3237053501": "Source: Heliostat", "3247513834": "Source: Equilibrium", "3257722699": "Source: Complete the \"Clean Up on Aisle Five\" Triumph.", "3265560237": "Source: Cryptic Quatrains III", "3277652589": "Source: Starhorse", "3288974535": "Source: \"Ghosts of the Deep\" Dungeon", "3299964501": "Source: Earn Ranks in Vanguard, Crucible, or Gambit Playlists", "3308438907": "Source: Season of Plunder", "3310034131": "Source: \"Crossed Blades\" Triumph", "3334812276": "Source: Open Legendary engrams and earn faction rank-up packages.", "3348906688": "Source: Ranks in Vanguard Strikes, Crucible, or Gambit", "3358334503": "Source: \"Boon Ghost Mod Collector\" Triumph", "3359853911": "Source: Wellspring Boss Zeerik, Lightflayer", "3388021959": "Source: Guardian Games", "3389857033": "Earn Crucible Engrams during Season 3 to unlock and equip this ornament.", "3390015730": "Source: Trials of Osiris Challenges", "3390164851": "Source: Found by turning in Black Armory bounties.", "3390269646": "Source: Guided Games Final Encounters", "3391325445": "Source: Battlegrounds", "3404977524": "Source: Contribute to the Empyrean Restoration Effort", "3411812408": "Source: \"All the Spring's Riches\" Triumph", "3422985544": "Source: Associated Gambit Quest", "3427537854": "Source: Adventure \"Road Rage\" on Io", "3431853656": "Achieved a Grimoire score of over 5000 in Destiny.", "3450213291": "Source: Wellspring Boss Bor'gong, Warden of the Spring", "3466789677": "Source: Place Ascendant III Division or Higher in Ranked Crucible Playlists", "3471208558": "Source: Trials of Osiris Wins", "3482766024": "Source: Festival of the Lost 2024", "3492941398": "Source: \"The Lie\" Quest", "3494247523": "Source: Complete the \"Season 8: Keepin' On\" quest.", "3507911332": "Source: Episode: Heresy", "3512613235": "Source: \"A Sacred Fusion\" Quest", "3522070610": "Source: Gambit", "3528789901": "Source: Season of the Chosen Nightfall Grandmaster", "3532642391": "Source: Forsaken Campaign", "3534706087": "Source: Complete activities and earn rank-up packages on Saturn's Moon, Titan.", "3543690049": "Source: Complete a flawless Trials ticket.", "3563833902": "Source: Season of the Risen Triumphs", "3564069447": "Source: Flawless with a \"Flight of the Pigeon\" medal for each win", "3567813252": "Source: Season of the Seraph Triumph", "3569603185": "Source: Earn rank-up packages from Lakshmi-2.", "3574140916": "Source: Season of the Seraph", "3589340943": "Source: Altars of Sorrow", "3597879858": "Source: \"Presage\" Exotic Quest", "3614199681": "Source: Pale Heart Triumph", "3656787928": "Source: Crucible Salvager's Salvo Armament", "3672287903": "Source: The Witch Queen Digital Deluxe Edition", "3693722471": "Source: Festival of the Lost 2020", "3704442923": "Source: Curse of Osiris Campaign", "3724111213": "Source: Solstice 2019", "3736521079": "Reached level 1 in the Ages of Triumph record book.", "3740731576": "Source: \"A Rising Tide\" Mission", "3747711246": "Source: Defeat Combatants or Open Chests", "3754173885": "Source: Adventure \"Getting Your Hands Dirty\" in the European Dead Zone", "3764925750": "Source: Complete an Izanami Forge ignition.", "3773376290": "Source: Terminal Overload (Zephyr Concourse)", "3807243511": "Source: Raid Chests", "3818317874": "Source: War Table Reputation Reset", "3829951162": "\"The world is not built on the laws they love… Not on peace, but by victory at any means.\" –The Winnower", "3845969330": "Source: Xûr", "3874934421": "Source: Complete Nightfall strike \"The Corrupted.\"", "3906217258": "Source: Revenant Fortress", "3936473457": "Requires Warlock Class", "3937492340": "Source: Seraph Bounties", "3942778906": "Source: Drifter Rank Up Reputation", "3952847349": "Source: The Dawning.", "3954922099": "Source: Exploring the Throne World", "3964663093": "Source: Rare drop from high-scoring Nightfall strikes on Mercury.", "3965815470": "Source: Higher Difficulty Empire Hunts", "4006434081": "Source: Terminal Overload", "4008954452": "Requires Shaped or Enhanced Weapon", "4009509410": "Source: Complete challenges in the Leviathan raid.", "4011186136": "Exotic Armor Focusing", "4034415948": "Source: The Edge of Fate Activities", "4036739795": "Source: Bright Engrams", "4041583267": "Source: Festival of the Lost", "4046490681": "Source: Complete the \"Global Resonance\" Triumph", "4054646289": "Source: Earned during the seasonal Dawning event.", "4066007318": "Source: Leviathan, Eater of Worlds raid lair on Prestige difficulty.", "4069355515": "Source: Handed out at US events in 2019.", "4079816474": "Source: War Table", "4101102010": "Source: Found by completing Bergusia Forge ignitions.", "4110186790": "Source: Terminal Overload (Límíng Harbor)", "4122810030": "Source: Complete seasonal activities during Season of the Undying.", "4137108180": "Source: Escalation Protocol on Mars.", "4140654910": "Source: Eliminate all Barons on the Tangled Shore.", "4164377529": "Source: Exotic Archive at the Tower", "4166998204": "Source: Earned as a pre-order bonus.", "4173145322": "Source: Season of the Lost", "4199401779": "Source: Season of Plunder Activities", "4208190159": "Source: Complete a Nightfall strike.", "4214471686": "Source: Adventure \"Unsafe at Any Speed\" in the European Dead Zone", "4246883461": "Source: Found in the \"Scourge of the Past\" raid.", "4247521481": "Source: Complete the \"Beautiful but Deadly\" Triumph.", "4263201695": "Source: Complete Nightfall strike \"A Garden World.\"", "4267157320": "Source: ???????", "4278841194": "Source: Season of the Wish Triumphs", "4284811963": "Source: Exploring Kepler", "4288102251": "Requires 1,000 Titan Kills", "4290227252": "Source: Complete a Volundr Forge ignition.", "4290499613": "Source: Complete the campaign as a Hunter.", "4292996207": "Source: World Quest \"Enhance!\" in the European Dead Zone." } ================================================ FILE: src/data/d2/special-vendors-strings.json ================================================ { "alreadyAcquiredFailureString": { "vendorHash": 1054786807, "index": 1 } } ================================================ FILE: src/data/d2/spider-mats.json ================================================ [ 443031982, 1633854071, 2993288448 ] ================================================ FILE: src/data/d2/spider-purchaseables-to-mats.json ================================================ { "1812969468": 3853748946, "3106913645": 4257549984, "3664001560": 3159615086 } ================================================ FILE: src/data/d2/subclass-plug-category-hashes.json ================================================ [ 39076551, 81856188, 180411040, 185594100, 204703343, 227647633, 404070091, 511532732, 605941486, 900498880, 902963970, 1281712906, 1387605624, 1422809918, 1458470025, 1662395848, 1681184239, 1684765285, 1774026300, 1861253111, 1970675705, 2111409167, 2200902275, 2285394316, 2415307576, 2430016289, 2434874031, 2613010961, 2789335173, 2822977079, 2831653331, 2850085618, 2905530840, 2997411645, 3052104375, 3119191718, 3151809860, 3205146347, 3287837048, 3324969927, 3369359206, 3460332466, 3468785159, 3728449707, 3904090216, 3990226434, 4141244538, 4145425829, 4225254304 ] ================================================ FILE: src/data/d2/trait-definition-ids.json ================================================ { "12026609": "item.weapon.scout_rifle", "37177486": "keywords.buffs.solar.flare_bauble", "37938188": "keywords.debuffs.stasis.shatter", "106947924": "keywords.buffs.stasis.frost_armor", "117031016": "releases.v420.season", "130863397": "item.weapon.grenade_launcher", "151064318": "item.package", "157469667": "keywords.buffs.solar.empower", "170945933": "item.quest.onramp", "192828432": "foundry.omolon", "195373008": "item.weapon.bow", "201433599": "inventory_filtering.bounty", "345967499": "keywords.buffs.prism.transcendence", "370766376": "item.quest.exotic", "374319058": "item.armor.chest", "446244952": "item.weapon.trace_rifle", "482679394": "item.subclass.light", "500105683": "item.quest.playlists", "500183315": "keywords.debuffs.arc.blind", "520867389": "item.quest.new_light", "567594262": "item.weapon", "577926988": "item.plug.aspect", "655301426": "keywords.buffs.void.invisibility", "661041410": "releases.v710.season", "686448803": "releases.v950", "687504889": "releases.v720.season", "753559279": "releases.v910", "763053052": "item.quest.annual.v900", "791530618": "item.weapon.exotic", "823756278": "releases.v600.annual", "853784306": "activities.gambit", "856705125": "item.spawnfx", "866931116": "releases.v730.season", "888082966": "item.emote", "888940472": "item.weapon.glaive", "904453863": "item.quest.frontier.behemoth", "929402123": "item.quest.annual.v600", "945613349": "keywords.debuffs.strand.infest", "963390771": "foundry.veist", "977620370": "releases.v350.season", "1030789163": "item.boost", "1056186694": "item.quest.event", "1075323345": "item.armor.head", "1096356879": "keywords.debuffs.solar.scorch", "1143070403": "item.weapon.machinegun", "1160263324": "releases.v460.season", "1221030001": "faction.new_monarchy", "1345630660": "faction.future_war_cult", "1348188306": "releases.v800.season", "1357347767": "releases.v450.season", "1385893620": "releases.v400.annual", "1416106830": "releases.v400.season", "1514833946": "keywords.buffs.prism.dark_debuffs", "1531673855": "item.weapon.sword", "1573004294": "releases.v480.season", "1577394840": "keywords.buffs.strand.tangle", "1648572040": "item.weapon.pulse_rifle", "1781288324": "activities.mamba", "1821231131": "foundry.tex_mechanica", "1851377542": "item.armor.arms", "1858131755": "releases.v900.core", "1861210184": "inventory_filtering.quest", "1866367371": "foundry.daito", "1891050213": "keywords.buffs.prism.dark_buffs", "1968436740": "item.armor.legs", "2034403781": "item.weapon.sidearm", "2052231686": "releases.v910.core", "2062186907": "item.consumable", "2100142349": "item.weapon.linear_fusion_rifle", "2114179114": "item.weapon.shotgun", "2184280643": "releases.v500.annual", "2208921643": "releases.v630.season", "2210483526": "foundry.hakke", "2217328812": "foundry.fotc", "2326993577": "releases.v470.season", "2387836362": "item.quest.past", "2405803211": "releases.v490.season", "2443101659": "item.bounty", "2455696884": "item.emblem", "2485406866": "keywords.buffs.void.overshield", "2519102437": "keywords.debuffs.strand.sever", "2570676179": "item.ghost", "2572971238": "releases.v620.season", "2578642829": "keywords.debuffs.void.suppression", "2582082890": "item.finisher", "2606653893": "releases.v700.annual", "2652561225": "item.shader", "2656809369": "releases.v540.season", "2659552777": "item.weapon.submachinegun", "2677200345": "releases.v300.annual", "2679722414": "keywords.debuffs.strand.suspend", "2712954769": "mamba_role.defender", "2713325501": "keywords.buffs.prism.light_buffs", "2716563063": "activities.iron_banner", "2724747993": "keywords.buffs.strand.threadling", "2725534325": "releases.v900.dlc", "2729780558": "item.weapon.auto_rifle", "2752740613": "releases.v500.season", "2773025918": "releases.v950.dlc", "2774395792": "item.quest.annual.v500", "2799343944": "item.quest.frontier.apollo", "2833630124": "item.plug.fragment", "2868778669": "releases.v610.season", "2878306895": "item.quest.current_release", "2891203715": "item.weapon.fusion_rifle", "2893978702": "item.engram", "2906302736": "releases.v800.annual", "2908763903": "item.quest.annual.v460", "2935077680": "keywords.buffs.arc.static_charge", "2944045106": "activities.black_armory", "2951764300": "faction.crucible", "2968599152": "keywords.debuffs.stasis.freeze", "2973844452": "item.quest.campaign", "2976021378": "item.quest.annual.v700", "2987314010": "releases.v950.core", "3011401061": "item.quest.annual.v800", "3023190802": "keywords.buffs.prism.light_debuffs", "3034243664": "inventory_filtering.quest.featured", "3078132110": "keywords.buffs.void.devour", "3090596947": "mamba_role.invader", "3173573497": "keywords.buffs.strand.body_armor", "3221118171": "keywords.debuffs.arc.jolt", "3224025418": "item.subclass.dark", "3263723277": "keywords.buffs.solar.cure", "3268862716": "keywords.debuffs.solar.detonation", "3291013836": "keywords.buffs.arc.supercharged", "3300229618": "item.weapon.sniper_rifle", "3328352616": "keywords.buffs.void.breach_bauble", "3331226384": "faction.dead_orbit", "3336638905": "keywords.debuffs.void.weaken", "3353022846": "releases.v530.season", "3359893241": "faction.vanguard", "3361847320": "releases.v510.season", "3367459877": "item.armor.class", "3385340084": "keywords.buffs.stasis.crystal", "3439101959": "activities.trials", "3460933757": "mamba_role.killer", "3475344486": "foundry.field_forged", "3477257717": "item.ornament.armor", "3488482714": "keywords.buffs.solar.restoration", "3553898659": "item.aura", "3596220576": "releases.v600.season", "3602983853": "item.weapon.hand_cannon", "3607584152": "item.ship", "3619103539": "releases.v410.season", "3671004794": "item.quest.seasonal", "3690635686": "foundry.suros", "3750900718": "releases.v310.season", "3791840693": "mamba_role.collector", "3820193993": "item.subclass.prism", "3824458961": "keywords.buffs.arc.ionic_trace", "3828004164": "item.ornament.weapon", "3833926855": "releases.v700.season", "3870807100": "releases.v820.season", "3906525419": "item.currency", "3925016055": "item.weapon.rocket_launcher", "3977049418": "item.vehicle", "3990406773": "releases.v320.season", "4020167523": "releases.v520.season", "4036726046": "item.exotic_catalyst", "4043161234": "keywords.buffs.stasis.shard", "4062709591": "releases.v810.season", "4105407564": "keywords.debuffs.void.volatile", "4118304139": "item.ghost_hologram", "4239423954": "keywords.debuffs.stasis.slow", "4252733117": "item.armor.exotic" } ================================================ FILE: src/data/d2/trait-to-enhanced-trait.json ================================================ { "5699512": 1834828808, "11551321": 3097973565, "11612903": 2422968039, "16392701": 2732340361, "25606670": 3143051906, "31345821": 639190697, "47981717": 1370847713, "52780822": 2338887882, "65797256": 1939575288, "95528736": 956288240, "106909392": 1783397184, "113508675": 177189259, "124408337": 1171887445, "198336270": 2216535938, "201365942": 3523746922, "202670084": 1871180748, "243981275": 984655331, "247725512": 2938480696, "269888150": 125241418, "280464955": 3920370755, "307683764": 1341464476, "332133599": 3156458303, "354401740": 3678483220, "365154968": 2939589096, "409831596": 137682932, "436053704": 1498024248, "438098033": 2748801589, "454085387": 2529412083, "460017080": 4045335048, "466087222": 905499626, "469285294": 3335686050, "509074078": 843318450, "555281244": 332733060, "557221067": 2896748467, "563069569": 2167611941, "580685494": 2719770986, "588594999": 2717805783, "591790007": 3315935575, "594346985": 2364577709, "599007201": 4267445637, "657025441": 4196156997, "671806388": 595108252, "679225683": 1807273211, "684456054": 2785334058, "689005463": 1279785143, "691659142": 536173722, "692399809": 4070834277, "699525795": 3719974635, "706527188": 1961144892, "732557052": 3256371876, "744594675": 2139363611, "769709866": 3210123846, "776531651": 2541826827, "791862061": 1488599449, "807993149": 1454612425, "831391274": 102912326, "839105230": 3048246338, "852209214": 3837163218, "862848869": 3358678417, "880644845": 3120212825, "908147344": 1663363584, "923806249": 3422796781, "934928524": 2691822036, "938542991": 766579919, "951095735": 3878366039, "957782887": 4116820839, "960810156": 1077019636, "960997401": 3244229373, "968510818": 424370782, "972757866": 1685378950, "981914802": 564443854, "990298390": 3845057226, "1012699414": 223754954, "1015611457": 2923251173, "1017229899": 3696140595, "1047830412": 3311319252, "1048183818": 3431277734, "1050127423": 501821599, "1067908860": 2913786532, "1069117426": 2552577806, "1080094173": 2126519017, "1087426260": 1065371964, "1089671869": 496047945, "1092016998": 226831738, "1115162408": 511499160, "1119449540": 561986700, "1126264488": 2476672792, "1134488199": 2904194311, "1164078247": 2957461671, "1168162263": 1347741687, "1194056669": 3955464425, "1195158366": 838873202, "1196733167": 2276384431, "1202133695": 2181638943, "1202584452": 2138673996, "1220787353": 1465283453, "1226351311": 3251326479, "1261178282": 3885243590, "1263609309": 2154730217, "1275731761": 2091001077, "1300023272": 2371713880, "1309453587": 4280607931, "1332244541": 1987021001, "1354429876": 848860060, "1359896290": 3060983774, "1365187766": 4200236906, "1380061575": 1198287879, "1380253176": 3781548872, "1392496348": 3966296580, "1421772400": 3291427296, "1427256713": 254337357, "1428297954": 2014892510, "1431678320": 550838496, "1439600632": 2282937672, "1441682018": 1639399262, "1451849462": 4175584682, "1464081238": 2813913866, "1467527085": 1932356121, "1478423395": 373417387, "1482024992": 2860123632, "1483536627": 3629421851, "1500996326": 3819272442, "1511100135": 65604327, "1517798362": 1011551830, "1523832109": 461595545, "1546142478": 983853954, "1546637391": 3418165135, "1556840489": 3442762221, "1561002382": 494745090, "1570042021": 2748258257, "1570399071": 1661646271, "1583705720": 1409206216, "1600092898": 828056542, "1607056502": 250902314, "1631667848": 402033656, "1645158859": 3755250867, "1673863459": 4236235115, "1683379515": 2216471363, "1685431615": 3406782879, "1687452232": 2777563320, "1702976605": 2523469161, "1749209109": 4085959009, "1754714824": 834262328, "1771339417": 395388285, "1771897777": 3648644469, "1774574192": 777774560, "1776069409": 2385083333, "1782407750": 2241043034, "1784898267": 1301083363, "1799762209": 1008128453, "1806718225": 1507295317, "1820235745": 4245865861, "1821614984": 1942880504, "1840239774": 1124871858, "1844523823": 3471392239, "1866048759": 3118144663, "1870851715": 183938251, "1885400500": 1968098204, "1888882410": 3684445958, "1890040155": 234526563, "1890422124": 1523649716, "1906137414": 1278990682, "1909124648": 3590359704, "1918398421": 3422375969, "1934171651": 3020906571, "1946186791": 4273542183, "1954620775": 1562897767, "1955165503": 2617553311, "1966281507": 1592669035, "1968497646": 2172195298, "1996142143": 3605260959, "2010801679": 3797647183, "2030760728": 2856365672, "2039302152": 2396422520, "2048641572": 1926441324, "2077819806": 1618208178, "2109543898": 2799030358, "2120661319": 1064478151, "2136073194": 600923142, "2152442764": 1209885908, "2154191829": 1111219233, "2172504645": 1172098417, "2173046394": 1720528630, "2209918983": 2000295559, "2213355989": 2002547233, "2224838837": 3102670337, "2244851822": 2860991074, "2250679103": 2873651103, "2272927194": 711628886, "2284787283": 3563868667, "2289534249": 3914874605, "2298656195": 674546187, "2319119708": 2140591236, "2349202967": 3653484343, "2360347339": 4096144819, "2360754333": 2459015849, "2387244414": 1748774930, "2396489472": 598607952, "2416023159": 3050799639, "2420895100": 1890997540, "2428997981": 64332393, "2437618208": 1043703792, "2450788523": 4207509907, "2451262963": 1609056795, "2458213969": 2570477205, "2551157718": 23371658, "2579169598": 2073244114, "2586829431": 2217963031, "2590710093": 4183521337, "2594592626": 2037312142, "2597494435": 943022827, "2610012052": 4290541820, "2621346526": 2495011826, "2652708987": 617966211, "2679249093": 788178929, "2680121939": 3839493627, "2697390518": 1847323754, "2713678205": 3355117577, "2720533289": 3042219245, "2726471870": 1172413778, "2749775325": 2073750249, "2779035018": 1171147302, "2801223209": 3956406253, "2822142346": 2108812838, "2827049491": 4270598587, "2839173408": 1186131696, "2846385770": 509594246, "2848615171": 169755979, "2859149211": 1135494563, "2866798147": 3974407819, "2869569095": 2308090567, "2870095399": 1802305063, "2888557110": 1905598698, "2890807135": 3581929919, "2896038713": 2402480669, "2922950962": 2078520142, "2946784966": 987673306, "2955427634": 2783367502, "2969185026": 2800182654, "2978966579": 344235611, "2980589453": 4063561849, "2985827016": 1439751480, "3016987351": 1145640183, "3017780555": 4223976499, "3018146897": 4100396181, "3038247973": 64866129, "3047969693": 624891305, "3078487919": 3744057135, "3081867624": 2963273944, "3096702027": 1658784563, "3108830275": 855168139, "3142289711": 3267421167, "3161816588": 191144788, "3177308360": 1002206008, "3194351027": 2275087323, "3201496230": 1545231802, "3215448563": 3299896859, "3218042543": 2633604463, "3230963543": 1709045111, "3250034553": 1147230557, "3252937884": 122204356, "3300816228": 3528046508, "3301904089": 409713597, "3311977193": 1796628141, "3324494224": 2562668800, "3326204863": 197518367, "3350417888": 1537607344, "3365897133": 1551736345, "3371775011": 4185607275, "3373736292": 3061646252, "3400784728": 573122728, "3414324643": 2662236651, "3418782618": 1563455254, "3425386926": 288411554, "3429800428": 52802868, "3436462433": 2658083589, "3438534621": 1529576681, "3454102905": 2223800157, "3465198467": 290984395, "3492396210": 3807404750, "3511092054": 711234314, "3513791699": 162561147, "3523296417": 1906147653, "3525010810": 3250572790, "3526486541": 1738338041, "3592538738": 2675361166, "3603807747": 2523856971, "3619207468": 3198323828, "3625355092": 3923000764, "3640170453": 1817983009, "3643424744": 2682205016, "3650930298": 405677814, "3661387068": 3399528164, "3666208348": 3959569796, "3673922083": 2889515627, "3700496672": 2840833776, "3705817207": 1646437399, "3708227201": 781192741, "3713215006": 3555252274, "3713579091": 2671305723, "3721627275": 714384243, "3722653512": 2898218424, "3731491409": 680897173, "3751912585": 331667533, "3764420437": 3192447905, "3768438372": 4201007276, "3773585912": 2817677128, "3786062476": 2953575380, "3796465595": 274905539, "3798852852": 582768348, "3800201097": 3674673997, "3809316345": 2052299741, "3824105627": 1183436451, "3827198035": 1649480699, "3828510309": 1771736209, "3846229553": 1621842933, "3868766766": 3081681954, "3891536761": 1549370717, "3911709647": 483923727, "3913600130": 147913470, "3917450714": 4039411798, "3927722942": 1167468626, "3932949589": 2039482529, "3938834702": 3416609154, "3966416502": 717070634, "3977735242": 406461158, "3978468247": 906981559, "3987942396": 2817499044, "3988215619": 54024075, "3989629871": 1831023215, "3993379141": 1164602481, "4008116374": 1292971594, "4042220767": 883665727, "4049631843": 859780267, "4067834857": 1777340333, "4071163871": 494941759, "4082225868": 1161469972, "4088731040": 3128149872, "4090651448": 111235976, "4104185692": 3007133316, "4132187021": 2581144697, "4139916815": 4232410959, "4152709778": 3609952942, "4190679284": 820036316, "4196967425": 2398680997, "4198902903": 222783511, "4259401308": 155346308, "4264904777": 3299164941, "4274614370": 4274613598, "4293542123": 2675184851 } ================================================ FILE: src/data/d2/universal-ornament-aux-sets.json ================================================ { "0": { "aux-0": [ 880861204, 2067155809, 141761093, 3663467820, 744142366 ], "aux-1": [ 2166685180, 2984466361, 1514122509, 1816178788, 1169613062 ] }, "1": { "aux-0": [ 389344040, 2044322464, 1731215697, 971580893, 1024752258 ], "aux-1": [ 1961861544, 2261200416, 3303733201, 1188458845, 2597269762 ], "aux-2": [ 476513004, 1540650548, 2013981053, 2750411529, 1122245142 ], "aux-3": [ 3154193408, 3987309016, 3053891303, 259522459, 2339497078 ] }, "2": { "aux-0": [ 3128930155, 889513448, 3670132590, 2422973919, 236847737 ], "aux-1": [ 1634414641, 2214399070, 1016461220, 3997262569, 1018337679 ] } } ================================================ FILE: src/data/d2/universal-ornament-plugset-hashes.json ================================================ [ 294325177, 642154071, 667102694, 802691474, 1761049781, 1986461264, 2820688990, 2988261973, 3062299199, 3069332449, 3119404522, 3327950547, 3345233278, 3878297035, 4207172850 ] ================================================ FILE: src/data/d2/unreferenced-collections-items.json ================================================ { "256984752": 424291879, "301231525": 3388655311, "1279318107": 70083888, "1534387872": 1851777734, "2589931342": 3885259140, "3471068543": 2884596447, "3849455378": 3591141932, "4274523514": 501329015 } ================================================ FILE: src/data/d2/unstackable-mods.json ================================================ [ 56663992, 84503918, 534479613, 539051925, 579997810, 688956976, 802695661, 865380761, 877723168, 1097608874, 1125986156, 1170405455, 1301391064, 1305848463, 1389309840, 1560574695, 1565861116, 1627901452, 1909657913, 1947468772, 1996040463, 2023980600, 2045123179, 2158846614, 2175577211, 2199590568, 2245839670, 2257238439, 2436471653, 2649291407, 2734674728, 2815817957, 2874957617, 3064687909, 3174771856, 3736152098, 3829100654, 3994043492, 4004774872, 4004774873, 4004774874, 4004774875, 4004774876, 4004774877, 4081595582, 4134680615, 4193902249, 4243059257 ] ================================================ FILE: src/data/d2/voice-dim-valid-perks.json ================================================ [ "50VAL Telescopic", "9RECT Telescopic", "ATA Scout", "ATB Long Range", "ATC Rex", "ATD Raptor", "Absorption Cells", "Abyssal Extractors", "Accelerated Assault", "Accelerated Coils", "Accelerated Heatsink", "Accomplice", "Accurized Rounds", "Acrobat's Focus", "Actual Grandeur", "Adagio", "Adamantine Brace", "Adaptive Burst", "Adaptive Frame", "Adaptive Glaive", "Adaptive Munitions", "Adaptive Ordnance", "Adhesive Ordnance", "Adrenaline Junkie", "Advanced Reflexes", "Aeon Energy", "Ager's Call", "Aggregate Charge", "Aggressive Burst", "Aggressive Frame", "Aggressive Glaive", "Ahamkara's Eye", "Air Assault", "Air Trigger", "Air-Cooled Core", "Alacrity", "Alchemical Etchings", "All at Once", "All-Star", "Alloy Casing", "Alloy Magazine", "Amalgamation Rounds", "Ambitious Assassin", "Ambush", "Ambush SLH25", "Ancillary Ordinance", "And Another Thing", "Anticipation", "Apollonic Tangent", "Appended Mag", "Aquila SS4", "Arc Alignment", "Arc Conductor", "Arc Traps", "Archer's Gambit", "Archer's Tempo", "Area Denial Frame", "Armor of the Colossus", "Armor-Piercing Rounds", "Arrowhead Brake", "Artifice Armor", "Ascending Amplitude", "Assassin's Blade", "Assault Barricade", "Assault Mag", "Atomizing Rounds", "Attrition Orbs", "Augmented Drum", "Auto-Loading Holster", "Auto-Loading Link", "Backup Plan", "Bait and Switch", "Balanced Guard", "Balanced Heat Weapon", "Banned Weapon", "Banshee's Wail", "Barrel Constrictor", "Barrel Shroud", "Barri-nade", "Base Barrel", "Base Battery", "Base Frame", "Base Grip", "Base Launch Tube", "Base Magazine", "Battle-Hearth", "Be the Danger", "Beacon Rounds", "Beacons of Empowerment", "Bewildering Burst", "Beyond The Veil", "Big Frigid Glaive", "Big Game Hunter", "Big-Game Hunter", "Binary Orbit", "Biotic Enhancements", "Bipod", "Bitterspite", "Black Hole", "Black Powder", "Blast Distributor", "Blastwave Striders", "Bleak Domain", "Blessing of Order", "Blessing of the Sky", "Blood Magic", "Blunt Execution Rounds", "Bolt Scavenger", "Bolt Thrower", "Bottomless Appetite", "Bottomless Grief", "Box Breathing", "Bray Inheritance", "Bray Legacy", "Break the Bank", "Breakthrough", "Bring the Heat", "Broadhead", "Broadside", "Built to Blast", "Bullseye Bolster", "Burning Ambition", "Burning Fists", "Burning Souls", "Burst Guard", "Business Time", "Butterfly", "Candle PS", "Cascade Point", "Cast No Shadows", "Caster Frame", "Causality Arrows", "Cauterizing Flame", "Cayde's Retribution", "Celerity", "Cerebral Uplink", "Chain Reaction", "Chambered Compensator", "Chaos Reshaped", "Chaotic Exchanger", "Charge Shot", "Charged with Blight", "Chill Clip", "Circle of Life", "Classy Contender", "Cleanshot IS", "Clenched Fist", "Close Enough", "Close the Gap", "Close to Melee", "Closing Time", "Clown Cartridge", "Cluster Bomb", "Cobra Totemic", "Cold Fusion", "Cold Steel", "Collective Action", "Collective Pugilism", "Collective Purpose", "Combat Grip", "Combat Sights", "Command Frame", "Command Frame II", "Command Frame III", "Command Frame IV", "Composite Propellant", "Composite Stock", "Compounding Force", "Compressed Wave Frame", "Compression Chamber", "Compulsive Reloader", "Concussion Grenades", "Condor SS2", "Conduction Tines", "Confined Launch", "Conserve Momentum", "Consul's Pitch", "Contending Cascade", "Control SAS", "Controlled Burst", "Cooling Baubles", "Corkscrew Rifling", "Cormorant Combo", "Cormorant Reversal", "Cornered", "Coronal Culmination", "Corrupted Nucleosynthesis", "Corvo SS2", "Cosmology", "Counterattack", "Countermass", "Cranial Spike", "Cranial Spike II", "Cranial Spike III", "Cranial Spike IV", "Creeping Attrition", "Cross Counter", "Crossfire HCS", "Crossing Over", "Crow's Wings", "Cruel Remedy", "Cryocannon", "Crystalline Corpsebloom", "Crystalline Transistor", "Cursebringer", "Cursed Thrall", "Danger Close", "Danger Zone", "Dark Deliverance", "Dark Descent", "Dark Ether Reaper", "Dawning Surprise", "Dealer's Choice", "Dearly Departed", "Death at First Glance", "Deconstruct", "Deep Stone Crypt Armor", "Delayed Gratification", "Delicate Tuning", "Demolitionist", "Demoralize", "Depths of Duskfield", "Desperado", "Desperate Measures", "Desperation", "Destabilizing Rounds", "Detonator Beam", "Devil Scope D2", "Devouring Rift", "Dichotomous Thinking", "Disaster Plan", "Discord", "Disorienting Grenades", "Disruption Break", "Disruption Weapon", "Dornröschen", "Double Dodge", "Double Down", "Double Fire", "Dragon's Vengeance", "Dragonfly", "Dreaded Visage", "Dream Work", "Drop Mag", "Dual Loader", "Dual Speed Receiver", "Duelist's Trance", "Dusk Dot D1", "Dusk Scope D2", "Dusk Sight D1", "Dynamic Charge", "Dynamic Duo", "Dynamic Heat Weapon", "Dynamic Sway Reduction", "EM Anomaly", "Eager Edge", "EagleEye SLR20", "Eddy Current", "Edge of Action", "Edge of Concurrence", "Edge of Intent", "Elemental Capacitor", "Elemental Honing", "Embers of Light", "En Garde", "Encore", "Enduring Guard", "Energy Transfer", "Enhanced Battery", "Enhanced Heatsink", "Enlightened Action", "Ensemble", "Envious Arsenal", "Envious Assassin", "Evolution", "Excavation", "Exhaustive Research", "Exhumation", "Explosive Head", "Explosive Light", "Explosive Pact", "Explosive Payload", "Explosive Shadow", "Extended Barrel", "Extended Mag", "Extrovert", "Eye of the Storm", "Eyes Up, Guardian", "Eyes on All", "Fan Fire", "Fanatical Lance", "FarPoint SAS", "Fastdraw HCS", "Fastdraw IS", "Feast of Light", "Feeding Frenzy", "Fervid Coldsnap", "Field Prep", "Field-Tested", "Firefly", "Firewalker", "Firing Line", "Firmly Planted", "Fistfuls of Bullets", "Fitted Stock", "Flame Refraction", "Flared Magwell", "Flash Counter", "Flash HS5", "Fleet Footed", "Flourish", "Fluted Barrel", "Focus Lens FLS2", "Focused Fury", "For the Empire", "Force Multiplier", "Forge Master", "Four-Headed Dog", "Fourth Time's the Charm", "Fragile Focus", "Frame of Reference", "Frenzy", "Friendly Competition", "Full Auto Trigger System", "Full Bore", "Full Choke", "Full Court", "Full Stop", "Fury Conductors", "Fusion Harness", "GA Post", "GB Iron", "Garden of Salvation Armor", "Gathering Light", "Genesis", "Genesis Rounds", "Gift of the Traveler", "Glacial Fortification", "Glacial Guard", "Glorious Charge", "Golden Munitions", "Golden Tricorn", "Golden Tricorn Enhanced", "Grasp of the Devourer", "Grave Robber", "Gravity Well", "Grenade Chaser", "Grenades and Horseshoes", "Guidance Ring", "Gun and Run", "Gutshot Straight", "Hail Barrage", "Hail Storm", "Hailstorm", "Halberdier's Reach", "Hammer of the Gods", "Hammer-Forged Rifling", "Hand-Laid Stock", "Harbinger's Pulse", "Hard Launch", "Harmonic Laser", "Harmonic Resonance", "Harmony", "Harsh Truths", "Harvester Spike", "Hatchling", "Hawkeye Hack", "Head Rush", "Headseeker", "Headstone", "Heal Clip", "Heart Piercer", "Hearts of Ice", "Heat Sink", "Heating Up", "Heavy Burst", "Heavy Grip", "Heavy Guard", "Heavy Hunter Armor", "Heavy Metal", "Heavy Slug Thrower", "Heavy Titan Armor", "Heavy Warlock Armor", "Helium Spirals", "High Ground", "High Noon", "High Octane", "High Priority", "High-Caliber Rounds", "High-Explosive Ordnance", "High-Impact Frame", "High-Impact Longbow", "High-Impact Reserves", "High-Velocity Rounds", "Hip-Fire Grip", "HitMark HCS", "Hitmark IS", "Honed Edge", "Horizon Ironsights", "Horns of Doom", "Hot Swap", "Hungering Quarrel", "Hunter's Trace", "Hunter's Trace II", "Hunter's Trace III", "Hunter's Trace IV", "Hydraulic Boosters", "Häkke Breach Armaments", "Häkke Heavy Burst Fire", "Häkke Light Burst Fire", "IS 2 Classic", "IS 5 Circle", "Ice Breaker", "Ignition Trigger", "Illegally Modded Holster", "Immovable Object", "Impact Casing", "Imperial Allegiance", "Impetus", "Implosion Rounds", "Impromptu Ammunition", "Impulse Amplifier", "Impulse MS3", "Incandescent", "Indomitability", "Infinite Guard", "Insatiable", "Insectoid Robot Grenades", "Inverse Relationship", "Invisible Hand", "Ionic Conductor", "Ionic Return", "Ionized Battery", "Ionized Heatsink", "Iron Gaze", "Iron Grip", "Iron Lord's Pride", "Iron Reach", "Jolt PS", "Jolting Feedback", "Judgment", "Jump Driver", "Jump Jets", "Keep Away", "Kickstart", "Kill Clip", "Killing Tally", "Killing Wind", "Kinetic Tremors", "King Dot K2", "King Sight K1", "Kintsugi", "Kismet", "LB Assault", "LC Ranged", "LD Watchdog", "LN2 Burst", "Lagrangian Sight", "Lancer's Vigil", "Land Tank", "Last Stand", "Last Wish Armor", "Lasting Impression", "Lead From Light", "Lead from Gold", "Legacy PR-55 Frame", "Leviathan's Sigh", "Light Battery", "Light Heatsink", "Light Mag", "Light Shift", "Lightning Rod", "Lightning Rounds", "Lightning Seeker", "Lightweight Frame", "Linear Actuators", "Linear Compensator", "Liquid Coils", "Liquid Cooling", "Lone Wolf", "Long March", "Longest Winter", "Longview SLR10", "Longview SLR20", "Looks Can Kill", "Loose Change", "Loyal Moths", "M1R Distribution Matrix", "M1R Distribution Matrix II", "M1R Distribution Matrix III", "M1R Distribution Matrix IV", "MIDA Multi-Tool", "MIDA Software Package", "MIDA Synergy", "Mad Scientist", "Magnificent Howl", "Malevolent Grasp", "Mark 10 Glass", "Mark 15 Lens", "Mark of the Devourer", "Markov Chain", "Marksman Sights", "Master of Arms", "Meaning-Making", "Mecha Holster", "Mechanized Autoloader", "Meganeura", "Melee Momentum", "Memento Mori", "Meyrin RDL", "Meyrin RDS", "Micro-Missile", "Micro-Missile Frame", "Mini Frags", "Misdirection", "Missile Tracers", "Mobile Hunter Armor", "Mobile Titan Armor", "Mobile Warlock Armor", "Mobius Conduit", "Model 6 Loop", "Model 8 Red", "Modified Heatsink", "Monte Carlo Method", "Mortal Polarity", "Move to Survive", "Moving Target", "Mulligan", "Multikill Clip", "Myrmidon's Reach", "Nadir Focus", "Nail, Meet Hammer", "Nano-Assault", "Nano-Munitions", "Nanotech Tracer Missiles", "New Tricks", "Nightmare Fuel", "Nightsworn Sight", "Nightsworn Sight II", "Nightsworn Sight III", "Nightsworn Sight IV", "No Backpack", "No Distractions", "Noble Deeds", "Noble Rounds", "Nucleosynthetic Magazine", "Null Factor", "Offhand Strike", "Omega Strike", "Omolon Fluid Dynamics", "On Black Wings", "Once More", "One Quiet Moment", "One for All", "One with the Void", "One-Two Punch", "Onslaught", "Opening Shot", "Osmosis", "Outlaw", "Overcharge Capacitor", "Overclocked Heatsink", "Overflow", "Overflowing Light", "Pace Yourself", "Pack Hunter", "Panic Response", "Panic Response II", "Panic Response III", "Panic Response IV", "Panic Response V", "Paracausal Affinity", "Paracausal Beam", "Paracausal Fluid", "Paracausal Imbuement", "Paracausal Pellets", "Paracausal Shot", "Parasitism", "Particle Repeater", "Parting Gift", "Payday", "Penance", "Percussive Flames", "Peregrine Strike", "Perfect Float", "Permeability", "Perpetual Loophole", "Perpetual Motion", "Personal Assistant", "Phase Converter", "Phase Magazine", "Photoinhibition", "Physic", "Pick Your Poison", "Pinpoint Slug Frame", "Planetary Torrent", "Play with Your Prey", "Poison Arrows", "Polygonal Rifling", "Polymer Grip", "Praxic Lift", "Precision Frame", "Precision Instrument", "Precision Slug", "Primeval's Torment", "Prismatic Inferno", "Probability Matrix", "Problem Solver", "Projection Fuse", "Property: Irreducible", "Property: Undecidable", "Protective Weave", "Prototype Trueseeker", "Proximity Grenades", "Proximity Power", "Psychohack", "Pugilist", "Pulse Monitor", "Puppeteer's Control", "Pyrogenesis", "Pyrotoxin Rounds", "Queen's Wrath", "Quick Launch", "QuickDot SAS", "Quickdraw", "Quickdraw IS", "Radiolaria Transposer", "Rampage", "Rampager's Handle", "Ranged Lens RLR5", "Ranged Lens RLS3", "Ranged Weapon", "Rangefinder", "Rapid Cooldown", "Rapid Fire Slug", "Rapid Hit", "Rapid-Fire Frame", "Rapid-Fire Glaive", "Rasmussen ISA", "Rasputin's Arsenal", "Rat Pack", "Ravenous Beast", "Reaper's Tithe", "Reciprocity", "Recombination", "Reconstruction", "Recycled Energy", "Red Dot 2 MOA", "Red Dot Micro", "Red Dot-OAS", "Red Dot-ORS", "Red Dot-ORS1", "Redemption", "Redirection", "Refined Soul", "Reflective Vents", "Rega's Refrain", "Regenerative Motion", "Reign Havoc", "Release the Wolves", "Relentless Strikes", "Relentless Tracker", "Remote Detonation", "Remote Shield", "Replenishing Aegis", "Repulsor Brace", "Repulsor Force", "Reservoir Burst", "Resolute", "Restoration Ritual", "Restorative Titan Armor", "Restorative Turret", "Restorative Warlock Armor", "Resurgence Directive", "Resurgence Directive II", "Resurgence Directive III", "Resurgence Directive IV", "Reverberation", "Reversal of Fortune", "Revolution", "Revved Consumption", "Rewind Again", "Rewind Rounds", "Ricochet Rounds", "Ride the Bull", "Rifle Scope SSF", "Rifle Scope ST", "Rifled Barrel", "Right Hook", "Rimestealer", "Rites of Ember", "Roar of Battle", "Roast 'Em", "Rolling Storm", "Roving Assassin", "Ruinscribe's Beacon", "Ruinscribe's Forge", "Runneth Over", "SC Holo", "SD Thermal", "SLO-10 Post", "SLO-12 Post", "SLO-21 Post", "SPO-26 Front", "SPO-28 Front", "SPO-57 Front", "SRO-37 Ocular", "SRO-41 Ocular", "SRO-52 Ocular", "SSO-05 Sniper", "SSO-07 Sniper", "SSO-08 Sniper", "SUROS Legacy", "Sacred Flame", "Saint's Fists", "Satou Focus Lens", "Satou Precision Lens", "Scatter Charge", "Scissor Fingers", "Screaming Swarm", "Search Party", "See Me, Feel Me", "Seraph Rounds", "Seriously, Watch Out", "Serve the Colony", "Sharp Edges", "Sharp Harvest", "Sharpshooter", "Shatter Shot", "Shattering Blade", "Shield Disorient", "Shiver Quiver", "Shock Blast", "Shoot to Loot", "Short-Action Stock", "Shortened Barrel", "Shortgaze SLH10", "Shortspec SAS", "Shot Package", "Shot Swap", "Shot in the Dark", "Shrapnel Launcher", "Signal MS5", "Silkbound Slayer", "Skittering Stinger", "Skulking Wolf", "Sleight of Hand", "Slice", "Slickdraw", "Slideshot", "Slideways", "Slug Rifle", "Smallbore", "Smart Drift Control", "Smooth Grip", "Smoothbore", "Snapshot Sights", "Snareweaver", "Snareweaver II", "Snareweaver III", "Snareweaver IV", "Sneak Bow", "Soaring Fang", "Soaring Fusilier", "Solar Rampart", "Soul Devourer", "Souldrinker", "Spark PS", "Spheromatik Trigger", "Spike Grenades", "Spindle", "Spinning Up", "Spirit of Alpha Lupi", "Spirit of Apotheosis", "Spirit of Caliban", "Spirit of Competition", "Spirit of Contact", "Spirit of Galanor", "Spirit of Harmony", "Spirit of Hoarfrost", "Spirit of Inmost Light", "Spirit of Osmiomancy", "Spirit of Renewal", "Spirit of Scars", "Spirit of Severance", "Spirit of Starfire", "Spirit of Synthoceps", "Spirit of Verity", "Spirit of Vesper", "Spirit of the Abeyant", "Spirit of the Armamentarium", "Spirit of the Assassin", "Spirit of the Bear", "Spirit of the Claw", "Spirit of the Coyote", "Spirit of the Cyrtarachne", "Spirit of the Dragon", "Spirit of the Eternal Warrior", "Spirit of the Filaments", "Spirit of the Foetracer", "Spirit of the Gyrfalcon", "Spirit of the Horn", "Spirit of the Liar", "Spirit of the Necrotic", "Spirit of the Ophidian", "Spirit of the Stag", "Spirit of the Star-Eater", "Spirit of the Swarm", "Spirit of the Wormhusk", "Splicer Surge", "Split Decision", "Split Electron", "Spread Shot", "Spread Shot Package", "Spring-Loaded Mounting", "Starless Night", "Starlight Beam", "Stats for All", "Steady Hands", "Steady Rounds", "SteadyHand HCS", "Sticky Grenades", "Stopping Power", "Storm and Stress", "Stormbringer", "Strange Protractor", "Strategist", "String of Curses", "Stunning Recovery", "Stylostixis", "Subjugation", "Subsistence", "Successful Warm-Up", "Sun Blast", "Sunburn", "Sundering", "Sunfire Furnace", "Supercharged Battery", "Supercharged Magazine", "Superconductor", "Support Frame", "Sureshot HCS", "Suros Synergy", "Surplus", "Surrounded", "Survival Well", "Survivalist Hunter Armor", "Suspending Blast", "Swap Mag", "Swarmers", "Swashbuckler", "Swooping Talons", "Sword Logic", "Swordmaster's Guard", "Sympathetic Arsenal", "Synapse Junctions", "Tactic SAS", "Tactical Battery", "Tactical Heatsink", "Tactical Mag", "Taken Ambition", "Taken Divergence", "Taken Predator", "Taken Resolution", "Tap the Trigger", "Target Acquired", "Target Lock", "Target SAS", "Tear", "Tempest Cascade", "Temporal Anomaly", "Temporal Manipulation", "Temporal Manipulation II", "Temporal Manipulation III", "Temporal Manipulation IV", "Temporal Unlimiter", "Tenacious Handle", "Tenacity", "Tesseract", "Tex Balanced Stock", "Textured Grip", "The Corruption Spreads", "The Corruption Spreads II", "The Corruption Spreads III", "The Corruption Spreads IV", "The Dance", "The Fate of All Fools", "The Fourth Magic", "The Fundamentals", "The Gift of Certainty", "The Lost Voice", "The Master", "The Perfect Fifth", "The Right Choice", "The Roadborn", "The Scientific Method", "The Whispers", "Thermal Atomization", "Thermoplastic Grenades", "Thin the Herd", "Threat Detector", "Threat Remover", "Thresh", "Thunderer", "Tight Fit", "Tilting at Windmills", "Time-Slip", "Timed Payload", "Timeless Mythoclast", "Timelost Magazine", "Tireless Blade", "Tithing Harvest", "To Excess", "To the Pain", "Together Forever", "Tome of Dawn", "Torch HS3", "Touch of Malice", "Touch of Venom", "Toxic Overload", "Tracking Module", "Trait Locked", "Traitor's Vessel", "Transcendent Duelist", "Transcendent Moment", "Transfusion Matrix", "Transmission MS7", "Transmutation", "Trench Barrel", "Tri-Planar Mass Driver", "Trickle Charge", "Trinary Vision", "Triple Tap", "TrueSight HCS", "TrueSight IS", "Tunnel Vision", "Turnabout", "Twintails", "Umbral Sustenance", "Uncanny Arrows", "Under Pressure", "Under-Over", "Unforeseen Repercussions", "Unplanned Reprieve", "Unrelenting", "Unrepentant", "Unsated Hunger", "Unstoppable Force", "Unwound", "Unyielding Handle", "Upgraded Sensor Pack", "Ursine Guard", "Valiant Charge", "Vampire's Caress", "Vanguard Determination", "Vanguard's Vindication", "Vanishing Execution", "Vanishing Shadow", "Veist Stinger", "Vengeance", "Vermin", "Vestigial Alchemy", "Vexadecimal", "Violent Reanimation", "Void Leech", "Volatile Battery", "Volatile Launch", "Volatile Light", "Voltaic Mirror", "Voltshot", "Vorpal Weapon", "Vortex Frame", "Warlord's End", "Warlord's Sigil", "Wave Frame", "Wave Sword Frame", "Webcatcher", "Weft Cutter", "Weighted Edge", "Well-Rounded", "Wellspring", "Whirlwind Blade", "White Nail", "White Nail II", "White Nail III", "White Nail IV", "Wild Card", "Willing Vessel", "Winged Eclipse", "Wire Rifle", "Wish-Dragon Teeth", "Wish-Dragon's Talons", "Withering Gaze", "Wolf Dot W2", "Wolf Sight W1", "Wolfpack Rounds", "Worm Byproduct", "Worm's Hunger", "Wraithmetal Mail", "Wrath of the Colossus", "Yepaki SS2", "Zen Moment", "Zoom 10 Point", "Zoom 30 Focus" ] ================================================ FILE: src/data/d2/watermark-to-event.json ================================================ { "/common/destiny2_content/icons/83fbcacd223402c09af4b7ab067f8cce.png": 1, "/common/destiny2_content/icons/be60f0242a039e62f1c5d4fc97801c75.png": 1, "/common/destiny2_content/icons/fe8bcc20fbfaf4cac69dfb640bb0b84e.png": 2, "/common/destiny2_content/icons/19e8ff2fc05c8bc381a672c2b44d4bdf.png": 2, "/common/destiny2_content/icons/50c3ebe414c6946429934d79504922fa.png": 3, "/common/destiny2_content/icons/518a725160c326a532b33c4af7a5e027.png": 3, "/common/destiny2_content/icons/53dc0b02306726ff1517af33ac908cef.png": 4, "/common/destiny2_content/icons/6529f7497921fd62d40c7fa7a1e00dd3.png": 4, "/common/destiny2_content/icons/c1e11e70eba15abcd4e0414fa29ef714.png": 5, "/common/destiny2_content/icons/00d64d97639991cfdb1afaefecf418b6.png": 5, "/common/destiny2_content/icons/9c091ec0e22c01dacc25efb63b46eb9b.png": 6, "/common/destiny2_content/icons/5cb49360f28fa037106307dda7cc7495.png": 6 } ================================================ FILE: src/data/d2/watermark-to-season.json ================================================ { "/common/destiny2_content/icons/4f28dc0f39238fe25d298a894ea71389.png": 1, "/common/destiny2_content/icons/b7c66ccc849867c9be6f24a183fa70e3.png": 1, "/common/destiny2_content/icons/7ba9d804508dd083ec20fcdb8ba0869d.png": 2, "/common/destiny2_content/icons/95b4ece09fb7ba577ed5a0c440042d0e.png": 2, "/common/destiny2_content/icons/da5f961ef97b78293cc498978c10e178.png": 3, "/common/destiny2_content/icons/cbf309e32b600da3f2077b5ee9f884fa.png": 3, "/common/destiny2_content/icons/aeb95eb1abe8e45e1fe2573d6b3ab3c5.png": 4, "/common/destiny2_content/icons/66b9c58a2fe597a173d9794b6f26ca82.png": 4, "/common/destiny2_content/icons/e0c16042274fd7d9cbffc4489e340c5d.png": 5, "/common/destiny2_content/icons/3baa918ed7dd95f509ee3eaecdc1e5b3.png": 5, "/common/destiny2_content/icons/2c022e452f395db7b1daec1cb44631fc.png": 6, "/common/destiny2_content/icons/a501d8889d7d89d44e87727218260754.png": 6, "/common/destiny2_content/icons/58d3ec8338cc9746a2e0cf901fbcec0e.png": 7, "/common/destiny2_content/icons/851d625c6dddb90b6be61a68b4645deb.png": 7, "/common/destiny2_content/icons/0b212b58a961f150708bca95095e0ecb.png": 8, "/common/destiny2_content/icons/8ab1d58ebd3945a5686eec7575eb2f0a.png": 8, "/common/destiny2_content/icons/a15754752f40aaf7b1b00aadb70a8f35.png": 8, "/common/destiny2_content/icons/59172b06d7abeca120ecf35f79e33a67.png": 8, "/common/destiny2_content/icons/ede19a0e1a54564243b0e5e8a18bde84.png": 9, "/common/destiny2_content/icons/5fed5e618423591a8cf4bd66a4f69c97.png": 9, "/common/destiny2_content/icons/247715dd42abef457b52ef37280c0e42.png": 10, "/common/destiny2_content/icons/4487c1c73c54e95ea54142058c0e1cae.png": 10, "/common/destiny2_content/icons/d105aa342f2d0c53a90a28477552f61f.png": 11, "/common/destiny2_content/icons/4520f392d955026aac4c4740345d2361.png": 11, "/common/destiny2_content/icons/a5e27dc822aa72787f388bd1fc115803.png": 12, "/common/destiny2_content/icons/b4f8743a293c3ee5f0dcdca0212180aa.png": 12, "/common/destiny2_content/icons/bce51cf90464e28026140df77c4eb6ce.png": 12, "/common/destiny2_content/icons/c4204008ac8496a9dafcf696145f37b9.png": 12, "/common/destiny2_content/icons/7b48b09fbb50634680168d5880b16bc9.png": 13, "/common/destiny2_content/icons/a4465539d5e5c65c12c9862cd653d082.png": 13, "/common/destiny2_content/icons/36418dde751148bd3b95a023d491ea73.png": 14, "/common/destiny2_content/icons/3d602b7985a350dd5e85130b148484f1.png": 14, "/common/destiny2_content/icons/914322d11262322c839a5388db2a4943.png": 15, "/common/destiny2_content/icons/6f97ea5b246a70fec98acf31b5b47eef.png": 15, "/common/destiny2_content/icons/bcc26708e314306fb2fc8cb98fcbf47e.png": 15, "/common/destiny2_content/icons/7f608702455231339fe7eb7ee6179527.png": 15, "/common/destiny2_content/icons/0b441021fbc328e6d0e2abc895f5c96e.png": 16, "/common/destiny2_content/icons/b1769a37f7cb2ae740d10d5e86d22015.png": 16, "/common/destiny2_content/icons/7b41678824a620d4f295984862702179.png": 16, "/common/destiny2_content/icons/3ac40b44ff238beec1f0c5715fab5f2d.png": 16, "/common/destiny2_content/icons/75adde12e4e9c9fb237e492d8258eb73.png": 17, "/common/destiny2_content/icons/b53275ee009af7fa70aa728dae78a0c0.png": 17, "/common/destiny2_content/icons/7d815c943977fe71bbf00caf1bd9c514.png": 18, "/common/destiny2_content/icons/eff64e4ea0eedf135899d98ce8f33f36.png": 18, "/common/destiny2_content/icons/41d05b7cb5cc0a384af07ee9b7d36dd2.png": 19, "/common/destiny2_content/icons/37a47df16553e25597c3717d427d4321.png": 19, "/common/destiny2_content/icons/a0556509f8825756b6b89f59f90528ec.png": 20, "/common/destiny2_content/icons/b17f9db83470e98839ae21433122e976.png": 20, "/common/destiny2_content/icons/fc02418ad2002351a3f88faa5b14eb88.png": 20, "/common/destiny2_content/icons/df9d5f7f1d41a9f5feae229442acaf3e.png": 20, "/common/destiny2_content/icons/ae5c7f708a36f754c2f68c65c88ab9aa.png": 21, "/common/destiny2_content/icons/d31fd6397a41caa861a1861f150805aa.png": 21, "/common/destiny2_content/icons/2dc17f123b7449b14144e76cfbeb2309.png": 22, "/common/destiny2_content/icons/669e649732ef72f873de7bbb696cfd50.png": 22, "/common/destiny2_content/icons/60d34bc853c51063b79592233c3661d4.png": 23, "/common/destiny2_content/icons/9087446b3fb6c1aff878d39cd9966b60.png": 23, "/common/destiny2_content/icons/6f17d323d81dd683086d88a9268f8106.png": 23, "/common/destiny2_content/icons/27df0ae2b804bdb49b75456e2ad759ef.png": 23, "/common/destiny2_content/icons/661c84a377389a3b8a1fc38b44189b41.png": 24, "/common/destiny2_content/icons/cc9167a8e8955b9f327d57d037327d02.png": 24, "/common/destiny2_content/icons/9bfaa5536772e2f3ef1252813a21c4d1.png": 24, "/common/destiny2_content/icons/a4b625257c5339538a82a1b17eec99aa.png": 24, "/common/destiny2_content/icons/24174c8ceefc3aea6bfe6b9d45de9d07.png": 24, "/common/destiny2_content/icons/24e8cdecc286ddbb6185195f075ac1af.png": 24, "/common/destiny2_content/icons/5232219633cc4d90570bffda36caccf4.png": 25, "/common/destiny2_content/icons/2401d52b8266057a1359e74c762a86ef.png": 25, "/common/destiny2_content/icons/0ac354c1c326441716ddb15d2c158c59.png": 26, "/common/destiny2_content/icons/1e56af6ab4bbbe7cafba8b3bab690d7c.png": 26, "/common/destiny2_content/icons/0d6c3365022ed3b059eac467b076978f.png": 26, "/common/destiny2_content/icons/17efe5b87a689739a5008426773f9cd5.png": 26, "/common/destiny2_content/icons/249813e647271a8227bae0d8a39ed505.png": 27, "/common/destiny2_content/icons/0e7739e80c1a520923e42610c7c52467.png": 27, "/common/destiny2_content/icons/6eeb62a30439cecc7699c22f3e1fb3cf.png": 27, "/common/destiny2_content/icons/ce7c82eaf8bb7bbe2debaa1bd7ad88b4.png": 27, "/common/destiny2_content/icons/6129365b4fad6754f2b8c4478fc3c4ac.png": 27, "/common/destiny2_content/icons/d133cfc43687423246b5afb81a3ae489.png": 27, "/common/destiny2_content/icons/4376a7d734583ae347acf9732aa3bb43.png": 28, "/common/destiny2_content/icons/c94c3d69d636310a10bf1d39fc4a3e29.png": 28, "/common/destiny2_content/icons/95f7754d52d6016fdc445fb62aa7a31e.png": 28, "/common/destiny2_content/icons/6e627f8283368c3c3441b95c38119a45.png": 28 } ================================================ FILE: src/data/d2/weapon-from-quest.json ================================================ { "20935540": 3399113824, "42351395": 3601169173, "204878059": 3656769954, "208088207": 1524355568, "449318888": 3107649396, "491078457": 1348325864, "616582330": 3688853371, "882778888": 3934247791, "1016668089": 4208016531, "1034055198": 4284902391, "1123421440": 1429553682, "1197486957": 4224767087, "1289796511": 484414765, "1331824604": 1000118612, "1345867570": 1789244740, "1363886209": 2881153063, "1395261499": 3715926175, "1526296434": 3010588096, "1621657423": 3897611837, "1645386487": 3999201698, "1681583613": 2145594812, "1766088024": 3519390051, "1983149589": 615393850, "1994645182": 1083548813, "2034215657": 3907528162, "2084878005": 2723555753, "2130065553": 2397886961, "2179048386": 2163065963, "2232171099": 613601972, "2595497736": 1271443422, "2723909519": 3536491683, "2812324400": 472120382, "2821677368": 3465651225, "2870169846": 4084168684, "2905188646": 1768751086, "2931957300": 1335967964, "3089417789": 2238822094, "3118061004": 2565205711, "3121540812": 3610649340, "3232203524": 3838101920, "3325778512": 1647306237, "3413860062": 692805471, "3512349612": 4204937010, "3605603507": 2617996611, "3690523502": 3424456965, "3824673936": 1051536457, "3870811754": 2032932188, "3924212056": 1330233961, "4037745684": 4120120192, "4068264807": 2991727273, "4207066264": 1831806500, "4277547616": 327090183 } ================================================ FILE: src/data/font/d2-font-glyphs.ts ================================================ export const enum FontGlyphs { CR = 13, space = 32, superscript_r = 876, saison = 7497, s_mark = 57424, capital_eszet = 57425, light = 57426, small_blocker = 57427, medium_blocker = 57428, large_blocker = 57429, healing_sword = 57430, void_blades = 57431, meteor_strike = 57432, settings = 57433, thermal_maul = 57440, void_shield = 57441, thermal_knives = 57442, arc_staff = 57443, nova_pulse = 57444, arc_beam = 57445, valkyrie = 57446, hammer_throw_melee = 57447, xl_blocker = 57448, invasion = 57449, combat_role_pierce = 57456, combat_role_overload = 57457, combat_role_stagger = 57458, quest = 57461, stasis_key_0 = 57462, stasis_key_1 = 57463, stasis_key_03 = 57464, stasis_key_3 = 57465, stasis_grenade_bolt = 57472, stasis_grenade_flare = 57473, stasis_grenade_wave = 57474, stasis_titan_super = 57475, stasis_hunter_super = 57476, stasis_warlock_super = 57477, stasis_titan_melee = 57478, stasis_hunter_melee = 57479, stasis_warlock_melee = 57480, stasis_encasement_shatter = 57481, stasis_crystal_shatter = 57488, stasis_turret = 57489, stasis_titan_spear = 57490, void_soul = 57491, balloom = 57492, void_shield_throw = 57493, void_ball = 57494, void_quickfall = 57495, bow = 57497, auto_rifle = 57600, pulse_rifle = 57601, scout_rifle = 57602, hand_cannon = 57603, shotgun = 57604, sniper_rifle = 57605, fusion_rifle = 57606, smg = 57607, rocket_launcher = 57608, sidearm = 57609, melee = 57616, grenade = 57617, hunter_smoke = 57618, grenade_launcher = 57619, solar_hammer_slam = 57620, solar_dynamite = 57621, solar_blast = 57622, arc_drone = 57624, warlock_blade = 57625, nova_bomb = 57632, hunter_staff = 57633, fist_of_havoc = 57634, golden_gun = 57635, throwing_knife = 57636, arc_warlock_super = 57637, solar_tian_super = 57638, void_hunter_super = 57639, void_titan_super = 57640, turret = 57648, pike = 57649, interceptor = 57650, sparrow = 57651, shield_artifact = 57652, loader_tank = 57653, spear_launcher = 57654, guardian_tank = 57655, beam_weapon = 57656, stasis = 57657, thermal = 57664, environment_hazard = 57665, headshot = 57666, arc = 57667, void = 57668, lostsector = 57669, revive_token = 57670, gilded_title = 57671, UniFFFD_001 = 57672, cabal_rifle = 57680, shrapnel_launcher = 57681, wire_rifle = 57682, sword_heavy = 57683, machinegun = 57684, grenade_launcher_field_forged = 57685, glaive = 57686, tofu = 61178, arc_titan_melee2 = 61179, arc_warlock_aspect = 61180, arc_hunter_super = 61181, arc_jolt = 61182, strand_threadling = 61183, strand_infest = 61184, strand_tangle = 61185, strand_suspend_grenade = 61186, strand_threadling_grenade = 61187, strand_grappling_hook = 61188, strand_grappling_hook_melee = 61189, strand_rope_dart_melee = 61190, strand_severing_leap_melee = 61191, strand_seize_melee = 61192, strand_hunter_quickfall = 61193, strand_titan_suspend_brace = 61194, strand_rope_dart_super = 61195, strand_titan_berserker_super = 61196, strand_warlock_minion_super = 61197, strand_kill = 61198, emoji_standard_background = 61199, emoji_dotted_eyes = 61200, emoji_curled_eyes = 61201, emoji_linear_eyes = 61202, emoji_shocked_inner_eyes = 61203, emoji_shocked_outer_eyes = 61204, emoji_smile_mouth = 61205, emoji_excited_mouth = 61206, emoji_upperteeth_mouth = 61207, emoji_lowerrteeth_mouth = 61208, emoji_tongue_mouth = 61209, emoji_grimacing_mouth = 61210, emoji_grimacingteeth_mouth = 61211, emoji_shocked_mouth = 61212, emoji_traits_freezing = 61213, emoji_traits_loving = 61214, emoji_traits_crying = 61215, emoji_traits_blushing = 61216, emoji_symbols_heart = 61217, emoji_symbols_flowerpetals = 61218, emoji_symbols_flowercenter = 61219, emoji_symbols_flowerleaves = 61220, emoji_hands_waving = 61221, emoji_hands_waving_detail = 61222, emoji_hands_clapping = 61223, emoji_hands_clapping_otherhand = 61224, emoji_hands_clapping_details = 61225, emoji_hands_flexing = 61226, emoji_hands_flexing_details = 61227, strand_warlock_suspend_tangle = 61228, strand_hunter_clone = 61229, strand_titan_slide_melee = 61230, region_chest = 61231, strand_hunter_buzzsaw = 61232, void_titan_exotic_chest = 61233, arc_hunter_air_move = 61234, arc_hunter_blink_dagger = 61235, solar_drone = 61236, solar_warlock_phoenix_form = 61237, void_titan_axe_throw = 61238, void_titan_axe_throw_relic = 61239, void_titan_grenade_absorb_blast = 61240, prism_titan_grenade = 61241, prism_hunter_grenade = 61242, prism_warlock_grenade = 61243, light_ability = 61244, darkness_ability = 61245, UniFFFD = 65533, } ================================================ FILE: src/data/font/dim-custom-symbols.ts ================================================ export const enum DimCustomSymbols { _ = 95, a = 97, b = 98, c = 99, d = 100, e = 101, f = 102, g = 103, h = 104, i = 105, j = 106, k = 107, l = 108, m = 109, n = 110, o = 111, p = 112, q = 113, r = 114, s = 115, t = 116, u = 117, v = 118, w = 119, x = 120, y = 121, z = 122, daybreak = 983040, golden_gun_marksman = 983041, nova_bomb_cataclysm = 983042, nova_bomb_vortex = 983043, shadowshot_moebius_quiver = 983044, ward_of_dawn = 983045, amplified = 983046, arc_soul = 983047, ball_lightning = 983048, ballistic_slam = 983049, barricade_rally = 983050, barricade_towering = 983051, bastion = 983052, blind = 983053, blink = 983054, celestial_fire = 983055, chain_lightning = 983056, chaos_accelerant = 983057, combination_blow = 983058, cryoclasm = 983059, cure = 983060, devour = 983061, diamond_lance = 983062, diamond_lance_throw = 983063, disorienting_blow = 983064, dodge_acrobats = 983065, dodge_gamblers = 983066, dodge_marksmans = 983067, electrostatic_mind = 983068, feed_the_void = 983069, flow_state = 983070, frostpulse = 983071, glacial_harvest = 983072, glide_balanced = 983073, glide_burst = 983074, glide_strafe = 983075, grenade_arcbolt = 983076, grenade_axion_bolt = 983077, grenade_firebolt = 983078, grenade_flashbang = 983079, grenade_flux = 983080, grenade_fusion = 983081, grenade_healing = 983082, grenade_incendiary = 983083, grenade_lightning = 983084, grenade_magnetic = 983085, grenade_pulse = 983086, grenade_scatter = 983087, grenade_skip = 983088, grenade_solar = 983089, grenade_storm = 983090, grenade_suppressor = 983091, grenade_swarm = 983092, grenade_thermite = 983093, grenade_tripmine = 983094, grenade_void_spike = 983095, grenade_void_wall = 983096, grenade_vortex = 983097, grim_harvest = 983098, gunpowder_gamble = 983099, hammer_strike = 983100, heat_rises = 983101, howl_of_the_storm = 983102, icarus_dash = 983103, iceflare_bolts = 983104, ignition = 983105, invisibility = 983106, ionic_trace = 983107, juggernaut = 983108, jump_high = 983109, jump_strafe = 983110, jump_triple = 983111, knife_lightweight = 983112, knife_proximity_explosive = 983113, knife_trick = 983114, knife_weighted_throwing = 983115, knock_em_down = 983116, knockout = 983117, lethal_current = 983118, lift_catapult = 983119, lift_high = 983120, lift_strafe = 983121, offensive_bulwark = 983122, on_your_mark = 983123, overshield = 983124, phoenix_dive = 983125, radiant = 983126, restoration = 983127, rift_empowering = 983128, rift_healing = 983129, roaring_flames = 983130, scorch = 983131, seismic_strike = 983132, shatter = 983133, shatterdive = 983134, shield_bash = 983135, slow = 983136, snare_bomb = 983137, sol_invictus = 983138, stasis_crystal = 983139, stasis_shard = 983140, stasis_titan_spear_slam = 983141, stylish_executioner = 983142, suppression = 983143, tectonic_harvest = 983144, tempest_strike = 983145, thruster = 983146, touch_of_flame = 983147, touch_of_thunder = 983148, touch_of_winter = 983149, trappers_ambush = 983150, vanishing_step = 983151, weaken = 983152, winters_shroud = 983153, adventure = 983154, altars_of_sorrow = 983155, black_armory_forge = 983156, curse_cycle = 983157, dungeon = 983158, dungeon_duality = 983159, dungeon_spire_of_the_watcher = 983160, edz = 983161, escalation_protocol = 983162, faction_crucible = 983163, faction_ironbanner = 983164, faction_xur = 983165, festival_of_the_lost = 983166, gambit_small = 983167, guardian_games = 983168, raid = 983169, raid_garden_of_salvation = 983170, strike = 983171, the_dawning = 983172, trials_of_osiris = 983173, wartable = 983174, battlegrounds = 983175, fragments = 983176, dreaming_city = 983177, luna = 983178, nessus = 983179, raid_deep_stone_crypt = 983180, raid_last_wish = 983181, raid_leviathan = 983182, raid_vault_of_glass = 983183, raid_vow_of_the_disciple = 983184, solstice = 983185, wellspring = 983186, ace_of_spades = 983187, arrivals = 983188, beyond_light = 983189, black_armory = 983190, bray_simulation = 983191, cabal_red_legion = 983192, cabal_unknown = 983193, chalice_of_opulence = 983194, clovis_bray = 983195, clovis_bray_device = 983196, crow = 983197, dawn = 983198, destiny = 983199, dim = 983200, eververse = 983201, faction_cryptarch = 983202, faction_dead_orbit = 983203, faction_fwc = 983204, faction_new_monarchy = 983205, faction_queens_wrath = 983206, faction_rally = 983207, faction_thenine = 983208, faction_vanguard = 983209, fallen_devils = 983210, fallen_dusk = 983211, fallen_judgement = 983212, fallen_kings = 983213, fallen_light = 983214, fallen_spider = 983215, fallen_unknown = 983216, fallen_winter = 983217, fallen_wolves = 983218, forsaken = 983219, gunsmith = 983220, hive = 983221, hive_crota = 983222, ishtar = 983223, lightfall = 983224, loom = 983225, plundered = 983226, risen = 983227, seasonal_arena = 983228, seraph = 983229, seventh_column = 983230, severed = 983231, shadowkeep = 983232, srl = 983233, taken = 983234, the_chosen = 983235, the_hunt = 983236, the_worthy = 983237, traveler = 983238, vex = 983239, warmind = 983240, witch_queen = 983241, fynch = 983242, gaul = 983243, starhorse = 983244, suraya = 983245, ahamkara_bones = 983246, cat_statues = 983247, eliksni_skull = 983248, executioner = 983249, exo_frame = 983250, gambit_mote = 983251, jade_rabbit = 983252, lectern = 983253, legendary = 983254, loot_chest = 983255, match_game = 983256, region_chest = 983257, relic = 983258, seasonal = 983259, skull = 983260, sleeper_node = 983261, standing_victory_gambit = 983262, team_alpha = 983263, gambit_bravo = 983264, team_bravo = 983265, vault = 983266, vendor = 983267, ravenous_beast = 983268, respawn_restricted = 983269, gambit_alpha = 983270, twin_tailed_fox = 983271, vorpal = 983272, worm = 983273, leviathan_axes = 983274, leviathan_cup = 983275, leviathan_dogs = 983276, leviathan_sun = 983277, operator = 983278, scanner = 983279, suppressor = 983280, bird_branch = 983281, bird_down = 983282, bird_fly = 983283, bird_stand = 983284, fire_left = 983285, fire_right = 983286, fish_curl = 983287, fish_left = 983288, fish_up = 983289, infinity = 983290, snake_eight = 983291, snake_split = 983292, snake_u = 983293, spear_left = 983294, spear_right = 983295, two_fish = 983296, abilities = 983297, ammo_heavy = 983298, ammo_primary = 983299, ammo_special = 983300, aspects = 983301, boots = 983302, chest = 983303, clan = 983304, class = 983305, class_abilities = 983306, class_hunter = 983307, class_titan = 983308, class_warlock = 983309, consumables = 983310, damage_kinetic = 983311, discipline = 983312, emblem = 983313, energy_weapon = 983314, engram = 983315, finisher = 983316, ghost = 983317, glimmer = 983318, gloves = 983319, gunsmith_materials = 983320, helmet = 983321, infuse = 983322, intellect = 983323, light = 983324, mobility = 983325, modifications = 983326, ornament = 983327, past = 983328, postmaster = 983329, power_level = 983330, power_weapon = 983331, recovery = 983332, resilience = 983333, resonance = 983334, roster = 983335, shaders = 983336, shaders_alt = 983337, ship = 983338, sparrow = 983339, strength = 983340, vault_armour = 983341, vault_weapons = 983342, accuracy = 983343, blast_radius = 983344, charge_time = 983345, draw_time = 983346, emote = 983347, handling = 983348, impact = 983349, loot = 983350, lore = 983351, masterwork = 983352, range = 983353, reload_speed = 983354, shield_duration = 983355, stability = 983356, transmat = 983357, velocity = 983358, hive_relic = 983359, trace_rifle = 983360, glaive_melee = 983361, sword_melee = 983362, the_rock = 983363, club = 983364, diamond = 983365, heart = 983366, spade = 983367, into_the_fray = 983368, mindspun_invocation = 983369, suspend = 983370, unravel = 983371, weavers_call = 983372, widows_silk = 983373, firesprite = 983374, void_breach = 983375, harmonic = 983376, orb = 983377, recharge = 983378, supers = 983379, movement = 983380, shaxx = 983381, prismatic = 983382, } ================================================ FILE: src/data/font/symbol-name-sources.ts ================================================ export const symbolData: { codepoint: number; glyph: string; source?: { tableName: | 'Trait' | 'InventoryItem' | 'SandboxPerk' | 'ActivityMode' | 'Objective' | 'ItemCategory' | 'InventoryBucket' | 'Faction' | 'Stat' | 'DamageType'; hash: number; fromRichText: boolean; }; }[] = [ { codepoint: 983379, glyph: '󰅓', source: { tableName: 'InventoryItem', hash: 171056525, fromRichText: false, }, }, { codepoint: 57633, glyph: '', source: { tableName: 'InventoryItem', hash: 3769507633, fromRichText: false, }, }, { codepoint: 57443, glyph: '', source: { tableName: 'SandboxPerk', hash: 2236497009, fromRichText: false, }, }, { codepoint: 61181, glyph: '', source: { tableName: 'InventoryItem', hash: 3769507632, fromRichText: false, }, }, { codepoint: 57634, glyph: '', source: { tableName: 'InventoryItem', hash: 119041299, fromRichText: false, }, }, { codepoint: 57432, glyph: '', source: { tableName: 'InventoryItem', hash: 119041298, fromRichText: false, }, }, { codepoint: 57637, glyph: '', source: { tableName: 'InventoryItem', hash: 1081893460, fromRichText: false, }, }, { codepoint: 57445, glyph: '', source: { tableName: 'InventoryItem', hash: 1081893461, fromRichText: false, }, }, { codepoint: 61235, glyph: '', source: { tableName: 'InventoryItem', hash: 2370269388, fromRichText: false, }, }, { codepoint: 57635, glyph: '', source: { tableName: 'InventoryItem', hash: 375052469, fromRichText: false, }, }, { codepoint: 983041, glyph: '󰀁', source: { tableName: 'InventoryItem', hash: 375052468, fromRichText: false, }, }, { codepoint: 57442, glyph: '', source: { tableName: 'InventoryItem', hash: 375052471, fromRichText: false, }, }, { codepoint: 57638, glyph: '', source: { tableName: 'InventoryItem', hash: 2529942647, fromRichText: false, }, }, { codepoint: 57440, glyph: '', source: { tableName: 'InventoryItem', hash: 2747500760, fromRichText: false, }, }, { codepoint: 57625, glyph: '', source: { tableName: 'InventoryItem', hash: 2274196886, fromRichText: false, }, }, { codepoint: 983040, glyph: '󰀀', source: { tableName: 'InventoryItem', hash: 2274196886, fromRichText: false, }, }, { codepoint: 57430, glyph: '', source: { tableName: 'InventoryItem', hash: 2274196887, fromRichText: false, }, }, { codepoint: 61237, glyph: '', source: { tableName: 'InventoryItem', hash: 1869939005, fromRichText: false, }, }, { codepoint: 57639, glyph: '', source: { tableName: 'InventoryItem', hash: 2370269389, fromRichText: false, }, }, { codepoint: 983044, glyph: '󰀄', source: { tableName: 'InventoryItem', hash: 2722573681, fromRichText: false, }, }, { codepoint: 57431, glyph: '', source: { tableName: 'InventoryItem', hash: 2722573682, fromRichText: false, }, }, { codepoint: 57640, glyph: '', source: { tableName: 'InventoryItem', hash: 4260353952, fromRichText: false, }, }, { codepoint: 57441, glyph: '', source: { tableName: 'SandboxPerk', hash: 3112248479, fromRichText: false, }, }, { codepoint: 983045, glyph: '󰀅', source: { tableName: 'InventoryItem', hash: 4260353953, fromRichText: false, }, }, { codepoint: 57632, glyph: '', source: { tableName: 'SandboxPerk', hash: 3484134371, fromRichText: false, }, }, { codepoint: 983042, glyph: '󰀂', source: { tableName: 'InventoryItem', hash: 1656118682, fromRichText: false, }, }, { codepoint: 983043, glyph: '󰀃', source: { tableName: 'InventoryItem', hash: 1656118681, fromRichText: false, }, }, { codepoint: 57444, glyph: '', source: { tableName: 'InventoryItem', hash: 1656118680, fromRichText: false, }, }, { codepoint: 61238, glyph: '', source: { tableName: 'InventoryItem', hash: 2529942646, fromRichText: false, }, }, { codepoint: 57476, glyph: '', source: { tableName: 'InventoryItem', hash: 2370269391, fromRichText: false, }, }, { codepoint: 57475, glyph: '', source: { tableName: 'InventoryItem', hash: 2021620139, fromRichText: false, }, }, { codepoint: 57477, glyph: '', source: { tableName: 'InventoryItem', hash: 1869939006, fromRichText: false, }, }, { codepoint: 61195, glyph: '', source: { tableName: 'InventoryItem', hash: 2370269384, fromRichText: false, }, }, { codepoint: 61196, glyph: '', source: { tableName: 'InventoryItem', hash: 2529942642, fromRichText: false, }, }, { codepoint: 61197, glyph: '', source: { tableName: 'InventoryItem', hash: 1869939001, fromRichText: false, }, }, { codepoint: 57616, glyph: '', source: { tableName: 'SandboxPerk', hash: 729990577, fromRichText: true, }, }, { codepoint: 983058, glyph: '󰀒', source: { tableName: 'InventoryItem', hash: 2657901005, fromRichText: false, }, }, { codepoint: 983064, glyph: '󰀘', source: { tableName: 'InventoryItem', hash: 2716335210, fromRichText: false, }, }, { codepoint: 983132, glyph: '󰁜', source: { tableName: 'InventoryItem', hash: 2708585277, fromRichText: false, }, }, { codepoint: 983049, glyph: '󰀉', source: { tableName: 'InventoryItem', hash: 2708585276, fromRichText: false, }, }, { codepoint: 61179, glyph: '', source: { tableName: 'InventoryItem', hash: 1980796563, fromRichText: false, }, }, { codepoint: 983056, glyph: '󰀐', source: { tableName: 'InventoryItem', hash: 1232050831, fromRichText: false, }, }, { codepoint: 983048, glyph: '󰀈', source: { tableName: 'InventoryItem', hash: 1232050830, fromRichText: false, }, }, { codepoint: 57636, glyph: '', source: { tableName: 'InventoryItem', hash: 3710811138, fromRichText: false, }, }, { codepoint: 983112, glyph: '󰁈', source: { tableName: 'InventoryItem', hash: 4016776974, fromRichText: false, }, }, { codepoint: 983113, glyph: '󰁉', source: { tableName: 'InventoryItem', hash: 4016776973, fromRichText: false, }, }, { codepoint: 983114, glyph: '󰁊', source: { tableName: 'InventoryItem', hash: 2657901004, fromRichText: false, }, }, { codepoint: 983115, glyph: '󰁋', source: { tableName: 'InventoryItem', hash: 4016776975, fromRichText: false, }, }, { codepoint: 983100, glyph: '󰀼', source: { tableName: 'InventoryItem', hash: 852252788, fromRichText: false, }, }, { codepoint: 57447, glyph: '', source: { tableName: 'InventoryItem', hash: 852252789, fromRichText: false, }, }, { codepoint: 983055, glyph: '󰀏', source: { tableName: 'InventoryItem', hash: 1470370539, fromRichText: false, }, }, { codepoint: 57622, glyph: '', source: { tableName: 'InventoryItem', hash: 1470370538, fromRichText: false, }, }, { codepoint: 61236, glyph: '', source: { tableName: 'InventoryItem', hash: 83039192, fromRichText: false, }, }, { codepoint: 983137, glyph: '󰁡', source: { tableName: 'InventoryItem', hash: 1139822081, fromRichText: false, }, }, { codepoint: 57618, glyph: '', source: { tableName: 'Objective', hash: 1157531927, fromRichText: false, }, }, { codepoint: 983135, glyph: '󰁟', source: { tableName: 'InventoryItem', hash: 4220332375, fromRichText: false, }, }, { codepoint: 57493, glyph: '', source: { tableName: 'InventoryItem', hash: 1980796560, fromRichText: false, }, }, { codepoint: 57494, glyph: '', source: { tableName: 'InventoryItem', hash: 2299867342, fromRichText: false, }, }, { codepoint: 57479, glyph: '', source: { tableName: 'InventoryItem', hash: 1341767667, fromRichText: false, }, }, { codepoint: 57478, glyph: '', source: { tableName: 'InventoryItem', hash: 1980796561, fromRichText: false, }, }, { codepoint: 57480, glyph: '', source: { tableName: 'InventoryItem', hash: 2543177538, fromRichText: false, }, }, { codepoint: 61190, glyph: '', source: { tableName: 'InventoryItem', hash: 1680616210, fromRichText: false, }, }, { codepoint: 61191, glyph: '', source: { tableName: 'InventoryItem', hash: 1980796564, fromRichText: false, }, }, { codepoint: 61192, glyph: '', source: { tableName: 'InventoryItem', hash: 2307689415, fromRichText: false, }, }, { codepoint: 57617, glyph: '', source: { tableName: 'SandboxPerk', hash: 528482921, fromRichText: true, }, }, { codepoint: 983076, glyph: '󰀤', source: { tableName: 'InventoryItem', hash: 1582574009, fromRichText: false, }, }, { codepoint: 983079, glyph: '󰀧', source: { tableName: 'InventoryItem', hash: 2909720723, fromRichText: false, }, }, { codepoint: 983080, glyph: '󰀨', source: { tableName: 'InventoryItem', hash: 4198689901, fromRichText: false, }, }, { codepoint: 983084, glyph: '󰀬', source: { tableName: 'InventoryItem', hash: 2994412667, fromRichText: false, }, }, { codepoint: 983086, glyph: '󰀮', source: { tableName: 'InventoryItem', hash: 1323461861, fromRichText: false, }, }, { codepoint: 983088, glyph: '󰀰', source: { tableName: 'InventoryItem', hash: 146194908, fromRichText: false, }, }, { codepoint: 983090, glyph: '󰀲', source: { tableName: 'InventoryItem', hash: 2481624867, fromRichText: false, }, }, { codepoint: 983078, glyph: '󰀦', source: { tableName: 'InventoryItem', hash: 2202441959, fromRichText: false, }, }, { codepoint: 983081, glyph: '󰀩', source: { tableName: 'InventoryItem', hash: 1013086087, fromRichText: false, }, }, { codepoint: 983082, glyph: '󰀪', source: { tableName: 'InventoryItem', hash: 1841016428, fromRichText: false, }, }, { codepoint: 983083, glyph: '󰀫', source: { tableName: 'InventoryItem', hash: 2581086849, fromRichText: false, }, }, { codepoint: 983089, glyph: '󰀱', source: { tableName: 'InventoryItem', hash: 2216698406, fromRichText: false, }, }, { codepoint: 983092, glyph: '󰀴', source: { tableName: 'InventoryItem', hash: 2842514288, fromRichText: false, }, }, { codepoint: 983093, glyph: '󰀵', source: { tableName: 'InventoryItem', hash: 2400634603, fromRichText: false, }, }, { codepoint: 983094, glyph: '󰀶', source: { tableName: 'InventoryItem', hash: 2946990961, fromRichText: false, }, }, { codepoint: 983077, glyph: '󰀥', source: { tableName: 'InventoryItem', hash: 3232422679, fromRichText: false, }, }, { codepoint: 983085, glyph: '󰀭', source: { tableName: 'InventoryItem', hash: 886607940, fromRichText: false, }, }, { codepoint: 983087, glyph: '󰀯', source: { tableName: 'InventoryItem', hash: 1514173218, fromRichText: false, }, }, { codepoint: 983091, glyph: '󰀳', source: { tableName: 'InventoryItem', hash: 2265076177, fromRichText: false, }, }, { codepoint: 983095, glyph: '󰀷', source: { tableName: 'InventoryItem', hash: 1255073825, fromRichText: false, }, }, { codepoint: 983096, glyph: '󰀸', source: { tableName: 'InventoryItem', hash: 2809141585, fromRichText: false, }, }, { codepoint: 983097, glyph: '󰀹', source: { tableName: 'InventoryItem', hash: 1016030582, fromRichText: false, }, }, { codepoint: 57472, glyph: '', source: { tableName: 'InventoryItem', hash: 1399219, fromRichText: false, }, }, { codepoint: 57473, glyph: '', source: { tableName: 'InventoryItem', hash: 1399216, fromRichText: false, }, }, { codepoint: 57474, glyph: '', source: { tableName: 'InventoryItem', hash: 1399217, fromRichText: false, }, }, { codepoint: 61186, glyph: '', source: { tableName: 'InventoryItem', hash: 1517917190, fromRichText: false, }, }, { codepoint: 61187, glyph: '', source: { tableName: 'InventoryItem', hash: 3994381207, fromRichText: false, }, }, { codepoint: 61188, glyph: '', source: { tableName: 'InventoryItem', hash: 1225978592, fromRichText: false, }, }, { codepoint: 61242, glyph: '', source: { tableName: 'InventoryItem', hash: 1005476557, fromRichText: false, }, }, { codepoint: 61241, glyph: '', source: { tableName: 'InventoryItem', hash: 275534325, fromRichText: false, }, }, { codepoint: 61243, glyph: '', source: { tableName: 'InventoryItem', hash: 1324853482, fromRichText: false, }, }, { codepoint: 983306, glyph: '󰄊', source: { tableName: 'InventoryItem', hash: 405131479, fromRichText: false, }, }, { codepoint: 983066, glyph: '󰀚', source: { tableName: 'InventoryItem', hash: 426473317, fromRichText: false, }, }, { codepoint: 983067, glyph: '󰀛', source: { tableName: 'InventoryItem', hash: 426473316, fromRichText: false, }, }, { codepoint: 983065, glyph: '󰀙', source: { tableName: 'InventoryItem', hash: 2711519343, fromRichText: false, }, }, { codepoint: 61234, glyph: '', source: { tableName: 'InventoryItem', hash: 2835214901, fromRichText: false, }, }, { codepoint: 983050, glyph: '󰀊', source: { tableName: 'InventoryItem', hash: 489583097, fromRichText: false, }, }, { codepoint: 983051, glyph: '󰀋', source: { tableName: 'InventoryItem', hash: 489583096, fromRichText: false, }, }, { codepoint: 983146, glyph: '󰁪', source: { tableName: 'InventoryItem', hash: 489583098, fromRichText: false, }, }, { codepoint: 983128, glyph: '󰁘', source: { tableName: 'InventoryItem', hash: 25156514, fromRichText: false, }, }, { codepoint: 983129, glyph: '󰁙', source: { tableName: 'InventoryItem', hash: 25156515, fromRichText: false, }, }, { codepoint: 983125, glyph: '󰁕', source: { tableName: 'InventoryItem', hash: 1444664836, fromRichText: false, }, }, { codepoint: 983380, glyph: '󰅔', source: { tableName: 'InventoryItem', hash: 230819033, fromRichText: false, }, }, { codepoint: 983109, glyph: '󰁅', source: { tableName: 'InventoryItem', hash: 20616658, fromRichText: false, }, }, { codepoint: 983110, glyph: '󰁆', source: { tableName: 'InventoryItem', hash: 20616659, fromRichText: false, }, }, { codepoint: 983111, glyph: '󰁇', source: { tableName: 'InventoryItem', hash: 20616656, fromRichText: false, }, }, { codepoint: 983119, glyph: '󰁏', source: { tableName: 'InventoryItem', hash: 266130437, fromRichText: false, }, }, { codepoint: 983120, glyph: '󰁐', source: { tableName: 'InventoryItem', hash: 266130439, fromRichText: false, }, }, { codepoint: 983121, glyph: '󰁑', source: { tableName: 'InventoryItem', hash: 266130438, fromRichText: false, }, }, { codepoint: 983073, glyph: '󰀡', source: { tableName: 'InventoryItem', hash: 5333294, fromRichText: false, }, }, { codepoint: 983074, glyph: '󰀢', source: { tableName: 'InventoryItem', hash: 5333293, fromRichText: false, }, }, { codepoint: 983075, glyph: '󰀣', source: { tableName: 'InventoryItem', hash: 5333292, fromRichText: false, }, }, { codepoint: 983054, glyph: '󰀎', source: { tableName: 'InventoryItem', hash: 5333295, fromRichText: false, }, }, { codepoint: 983301, glyph: '󰄅', source: { tableName: 'InventoryItem', hash: 483775550, fromRichText: false, }, }, { codepoint: 983176, glyph: '󰂈', source: { tableName: 'InventoryItem', hash: 190429600, fromRichText: false, }, }, { codepoint: 983070, glyph: '󰀞', source: { tableName: 'InventoryItem', hash: 4194622036, fromRichText: false, }, }, { codepoint: 983118, glyph: '󰁎', source: { tableName: 'InventoryItem', hash: 4194622038, fromRichText: false, }, }, { codepoint: 983145, glyph: '󰁩', source: { tableName: 'InventoryItem', hash: 4194622037, fromRichText: false, }, }, { codepoint: 983108, glyph: '󰁄', source: { tableName: 'InventoryItem', hash: 1656549673, fromRichText: false, }, }, { codepoint: 983117, glyph: '󰁍', source: { tableName: 'InventoryItem', hash: 1262901523, fromRichText: false, }, }, { codepoint: 983148, glyph: '󰁬', source: { tableName: 'InventoryItem', hash: 1656549672, fromRichText: false, }, }, { codepoint: 983047, glyph: '󰀇', source: { tableName: 'InventoryItem', hash: 1293395731, fromRichText: false, }, }, { codepoint: 57624, glyph: '', source: { tableName: 'InventoryItem', hash: 1293395731, fromRichText: false, }, }, { codepoint: 983068, glyph: '󰀜', source: { tableName: 'InventoryItem', hash: 1293395729, fromRichText: false, }, }, { codepoint: 61180, glyph: '', source: { tableName: 'InventoryItem', hash: 790664812, fromRichText: false, }, }, { codepoint: 57621, glyph: '', source: { tableName: 'InventoryItem', hash: 2835214903, fromRichText: false, }, }, { codepoint: 983099, glyph: '󰀻', source: { tableName: 'InventoryItem', hash: 2835214903, fromRichText: false, }, }, { codepoint: 983116, glyph: '󰁌', source: { tableName: 'InventoryItem', hash: 3066103998, fromRichText: false, }, }, { codepoint: 983123, glyph: '󰁓', source: { tableName: 'InventoryItem', hash: 3066103999, fromRichText: false, }, }, { codepoint: 57620, glyph: '', source: { tableName: 'InventoryItem', hash: 1262901520, fromRichText: false, }, }, { codepoint: 983130, glyph: '󰁚', source: { tableName: 'InventoryItem', hash: 2984351204, fromRichText: false, }, }, { codepoint: 983138, glyph: '󰁢', source: { tableName: 'InventoryItem', hash: 2984351205, fromRichText: false, }, }, { codepoint: 983101, glyph: '󰀽', source: { tableName: 'InventoryItem', hash: 83039194, fromRichText: false, }, }, { codepoint: 983103, glyph: '󰀿', source: { tableName: 'InventoryItem', hash: 83039195, fromRichText: false, }, }, { codepoint: 983147, glyph: '󰁫', source: { tableName: 'InventoryItem', hash: 83039193, fromRichText: false, }, }, { codepoint: 983142, glyph: '󰁦', source: { tableName: 'InventoryItem', hash: 187655374, fromRichText: false, }, }, { codepoint: 983150, glyph: '󰁮', source: { tableName: 'InventoryItem', hash: 187655372, fromRichText: false, }, }, { codepoint: 57495, glyph: '', }, { codepoint: 983151, glyph: '󰁯', source: { tableName: 'InventoryItem', hash: 187655373, fromRichText: false, }, }, { codepoint: 983052, glyph: '󰀌', source: { tableName: 'InventoryItem', hash: 1602994569, fromRichText: false, }, }, { codepoint: 983122, glyph: '󰁒', source: { tableName: 'InventoryItem', hash: 1602994570, fromRichText: false, }, }, { codepoint: 983057, glyph: '󰀑', source: { tableName: 'InventoryItem', hash: 2321824285, fromRichText: false, }, }, { codepoint: 57491, glyph: '', source: { tableName: 'InventoryItem', hash: 2321824287, fromRichText: false, }, }, { codepoint: 983069, glyph: '󰀝', source: { tableName: 'InventoryItem', hash: 790664815, fromRichText: false, }, }, { codepoint: 983098, glyph: '󰀺', source: { tableName: 'InventoryItem', hash: 1920417385, fromRichText: false, }, }, { codepoint: 983134, glyph: '󰁞', source: { tableName: 'InventoryItem', hash: 2934767476, fromRichText: false, }, }, { codepoint: 983149, glyph: '󰁭', source: { tableName: 'InventoryItem', hash: 4184589900, fromRichText: false, }, }, { codepoint: 983153, glyph: '󰁱', source: { tableName: 'InventoryItem', hash: 2835214902, fromRichText: false, }, }, { codepoint: 983059, glyph: '󰀓', source: { tableName: 'InventoryItem', hash: 2031919265, fromRichText: false, }, }, { codepoint: 983062, glyph: '󰀖', source: { tableName: 'InventoryItem', hash: 1262901522, fromRichText: false, }, }, { codepoint: 57490, glyph: '', source: { tableName: 'InventoryItem', hash: 1262901522, fromRichText: false, }, }, { codepoint: 983063, glyph: '󰀗', source: { tableName: 'InventoryItem', hash: 1262901522, fromRichText: false, }, }, { codepoint: 983141, glyph: '󰁥', source: { tableName: 'InventoryItem', hash: 1262901522, fromRichText: false, }, }, { codepoint: 983102, glyph: '󰀾', source: { tableName: 'InventoryItem', hash: 1563930741, fromRichText: false, }, }, { codepoint: 983144, glyph: '󰁨', source: { tableName: 'InventoryItem', hash: 2031919264, fromRichText: false, }, }, { codepoint: 57489, glyph: '', source: { tableName: 'InventoryItem', hash: 790664813, fromRichText: false, }, }, { codepoint: 983071, glyph: '󰀟', source: { tableName: 'InventoryItem', hash: 668903197, fromRichText: false, }, }, { codepoint: 983072, glyph: '󰀠', source: { tableName: 'InventoryItem', hash: 2651551055, fromRichText: false, }, }, { codepoint: 983104, glyph: '󰁀', source: { tableName: 'InventoryItem', hash: 668903196, fromRichText: false, }, }, { codepoint: 61193, glyph: '', source: { tableName: 'InventoryItem', hash: 4249729126, fromRichText: false, }, }, { codepoint: 983373, glyph: '󰅍', source: { tableName: 'InventoryItem', hash: 4249729127, fromRichText: false, }, }, { codepoint: 61194, glyph: '', source: { tableName: 'InventoryItem', hash: 988980152, fromRichText: false, }, }, { codepoint: 983368, glyph: '󰅈', source: { tableName: 'InventoryItem', hash: 988980153, fromRichText: false, }, }, { codepoint: 983369, glyph: '󰅉', source: { tableName: 'InventoryItem', hash: 262821318, fromRichText: false, }, }, { codepoint: 983372, glyph: '󰅌', source: { tableName: 'InventoryItem', hash: 262821317, fromRichText: false, }, }, { codepoint: 983297, glyph: '󰄁', source: { tableName: 'InventoryItem', hash: 256339607, fromRichText: false, }, }, { codepoint: 983046, glyph: '󰀆', source: { tableName: 'Trait', hash: 3291013836, fromRichText: false, }, }, { codepoint: 983053, glyph: '󰀍', source: { tableName: 'Trait', hash: 500183315, fromRichText: false, }, }, { codepoint: 983107, glyph: '󰁃', source: { tableName: 'Trait', hash: 3824458961, fromRichText: false, }, }, { codepoint: 61182, glyph: '', source: { tableName: 'Trait', hash: 3221118171, fromRichText: false, }, }, { codepoint: 983060, glyph: '󰀔', source: { tableName: 'Trait', hash: 3263723277, fromRichText: false, }, }, { codepoint: 983374, glyph: '󰅎', source: { tableName: 'Trait', hash: 37177486, fromRichText: false, }, }, { codepoint: 983105, glyph: '󰁁', source: { tableName: 'Trait', hash: 3268862716, fromRichText: false, }, }, { codepoint: 983126, glyph: '󰁖', source: { tableName: 'Trait', hash: 157469667, fromRichText: false, }, }, { codepoint: 983127, glyph: '󰁗', source: { tableName: 'Trait', hash: 3488482714, fromRichText: false, }, }, { codepoint: 983131, glyph: '󰁛', source: { tableName: 'Trait', hash: 1096356879, fromRichText: false, }, }, { codepoint: 983061, glyph: '󰀕', source: { tableName: 'Trait', hash: 3078132110, fromRichText: false, }, }, { codepoint: 983106, glyph: '󰁂', source: { tableName: 'Trait', hash: 655301426, fromRichText: false, }, }, { codepoint: 983124, glyph: '󰁔', source: { tableName: 'Trait', hash: 2485406866, fromRichText: false, }, }, { codepoint: 983143, glyph: '󰁧', source: { tableName: 'Trait', hash: 2578642829, fromRichText: false, }, }, { codepoint: 983375, glyph: '󰅏', source: { tableName: 'Trait', hash: 3328352616, fromRichText: false, }, }, { codepoint: 57492, glyph: '', source: { tableName: 'Trait', hash: 4105407564, fromRichText: false, }, }, { codepoint: 983152, glyph: '󰁰', source: { tableName: 'Trait', hash: 3336638905, fromRichText: false, }, }, { codepoint: 57481, glyph: '', source: { tableName: 'Trait', hash: 37938188, fromRichText: false, }, }, { codepoint: 983133, glyph: '󰁝', source: { tableName: 'Trait', hash: 37938188, fromRichText: false, }, }, { codepoint: 983136, glyph: '󰁠', source: { tableName: 'Trait', hash: 4239423954, fromRichText: false, }, }, { codepoint: 983139, glyph: '󰁣', source: { tableName: 'Trait', hash: 3385340084, fromRichText: false, }, }, { codepoint: 57488, glyph: '', source: { tableName: 'Trait', hash: 37938188, fromRichText: false, }, }, { codepoint: 983140, glyph: '󰁤', source: { tableName: 'Trait', hash: 4043161234, fromRichText: false, }, }, { codepoint: 61184, glyph: '', source: { tableName: 'Trait', hash: 2519102437, fromRichText: false, }, }, { codepoint: 983370, glyph: '󰅊', source: { tableName: 'Trait', hash: 2679722414, fromRichText: false, }, }, { codepoint: 61185, glyph: '', source: { tableName: 'Trait', hash: 1577394840, fromRichText: false, }, }, { codepoint: 61230, glyph: '', source: { tableName: 'InventoryItem', hash: 988980155, fromRichText: false, }, }, { codepoint: 61229, glyph: '', source: { tableName: 'InventoryItem', hash: 2835214897, fromRichText: false, }, }, { codepoint: 61232, glyph: '', source: { tableName: 'InventoryItem', hash: 4249729124, fromRichText: false, }, }, { codepoint: 61228, glyph: '', source: { tableName: 'InventoryItem', hash: 262821319, fromRichText: false, }, }, { codepoint: 983371, glyph: '󰅋', source: { tableName: 'Trait', hash: 945613349, fromRichText: false, }, }, { codepoint: 57497, glyph: '', source: { tableName: 'Objective', hash: 1368601876, fromRichText: true, }, }, { codepoint: 57600, glyph: '', source: { tableName: 'Objective', hash: 49530695, fromRichText: true, }, }, { codepoint: 57601, glyph: '', source: { tableName: 'Objective', hash: 189060104, fromRichText: true, }, }, { codepoint: 57602, glyph: '', source: { tableName: 'Objective', hash: 75057024, fromRichText: true, }, }, { codepoint: 57603, glyph: '', source: { tableName: 'Objective', hash: 563593850, fromRichText: true, }, }, { codepoint: 57609, glyph: '', source: { tableName: 'Objective', hash: 141911950, fromRichText: true, }, }, { codepoint: 57607, glyph: '', source: { tableName: 'Objective', hash: 102976778, fromRichText: true, }, }, { codepoint: 57604, glyph: '', source: { tableName: 'Objective', hash: 212380697, fromRichText: true, }, }, { codepoint: 57605, glyph: '', source: { tableName: 'Objective', hash: 273389628, fromRichText: true, }, }, { codepoint: 57606, glyph: '', source: { tableName: 'Objective', hash: 215999859, fromRichText: true, }, }, { codepoint: 57685, glyph: '', source: { tableName: 'Objective', hash: 1217177904, fromRichText: true, }, }, { codepoint: 57686, glyph: '', source: { tableName: 'Objective', hash: 1351954994, fromRichText: true, }, }, { codepoint: 57656, glyph: '', source: { tableName: 'Objective', hash: 554293431, fromRichText: true, }, }, { codepoint: 983360, glyph: '󰅀', source: { tableName: 'Objective', hash: 554293431, fromRichText: true, }, }, { codepoint: 57608, glyph: '', source: { tableName: 'Objective', hash: 13215836, fromRichText: true, }, }, { codepoint: 57619, glyph: '', source: { tableName: 'Objective', hash: 43313268, fromRichText: true, }, }, { codepoint: 57682, glyph: '', source: { tableName: 'Objective', hash: 1476676901, fromRichText: true, }, }, { codepoint: 57683, glyph: '', source: { tableName: 'Objective', hash: 1260068656, fromRichText: true, }, }, { codepoint: 57684, glyph: '', source: { tableName: 'Objective', hash: 172143731, fromRichText: true, }, }, { codepoint: 57666, glyph: '', source: { tableName: 'Objective', hash: 30510483, fromRichText: true, }, }, { codepoint: 57654, glyph: '', }, { codepoint: 983359, glyph: '󰄿', }, { codepoint: 61239, glyph: '', }, { codepoint: 61233, glyph: '', }, { codepoint: 57667, glyph: '', source: { tableName: 'SandboxPerk', hash: 679036014, fromRichText: true, }, }, { codepoint: 57668, glyph: '', source: { tableName: 'SandboxPerk', hash: 1554078996, fromRichText: true, }, }, { codepoint: 57664, glyph: '', source: { tableName: 'SandboxPerk', hash: 1821367741, fromRichText: true, }, }, { codepoint: 57657, glyph: '', source: { tableName: 'SandboxPerk', hash: 35992462, fromRichText: true, }, }, { codepoint: 61198, glyph: '', source: { tableName: 'SandboxPerk', hash: 381243875, fromRichText: true, }, }, { codepoint: 57665, glyph: '', }, { codepoint: 983311, glyph: '󰄏', source: { tableName: 'DamageType', hash: 3373582085, fromRichText: false, }, }, { codepoint: 983382, glyph: '󰅖', }, { codepoint: 61244, glyph: '', }, { codepoint: 61245, glyph: '', }, { codepoint: 983343, glyph: '󰄯', source: { tableName: 'Stat', hash: 1591432999, fromRichText: false, }, }, { codepoint: 983344, glyph: '󰄰', source: { tableName: 'SandboxPerk', hash: 627527846, fromRichText: false, }, }, { codepoint: 983345, glyph: '󰄱', source: { tableName: 'Stat', hash: 2961396640, fromRichText: false, }, }, { codepoint: 983346, glyph: '󰄲', source: { tableName: 'Stat', hash: 447667954, fromRichText: false, }, }, { codepoint: 983348, glyph: '󰄴', source: { tableName: 'Stat', hash: 943549884, fromRichText: false, }, }, { codepoint: 983349, glyph: '󰄵', source: { tableName: 'Stat', hash: 4043523819, fromRichText: false, }, }, { codepoint: 983353, glyph: '󰄹', source: { tableName: 'Stat', hash: 1240592695, fromRichText: false, }, }, { codepoint: 983354, glyph: '󰄺', source: { tableName: 'Stat', hash: 4188031367, fromRichText: false, }, }, { codepoint: 983355, glyph: '󰄻', source: { tableName: 'Stat', hash: 1842278586, fromRichText: false, }, }, { codepoint: 983356, glyph: '󰄼', source: { tableName: 'Stat', hash: 155624089, fromRichText: false, }, }, { codepoint: 983358, glyph: '󰄾', source: { tableName: 'Stat', hash: 2523465841, fromRichText: false, }, }, { codepoint: 983325, glyph: '󰄝', source: { tableName: 'Stat', hash: 2996146975, fromRichText: false, }, }, { codepoint: 983333, glyph: '󰄥', source: { tableName: 'Stat', hash: 392767087, fromRichText: false, }, }, { codepoint: 983332, glyph: '󰄤', source: { tableName: 'Stat', hash: 1943323491, fromRichText: false, }, }, { codepoint: 983312, glyph: '󰄐', source: { tableName: 'Stat', hash: 1735777505, fromRichText: false, }, }, { codepoint: 983323, glyph: '󰄛', source: { tableName: 'Objective', hash: 119163662, fromRichText: false, }, }, { codepoint: 983340, glyph: '󰄬', source: { tableName: 'Objective', hash: 980790784, fromRichText: false, }, }, { codepoint: 983321, glyph: '󰄙', source: { tableName: 'InventoryItem', hash: 20603181, fromRichText: false, }, }, { codepoint: 983319, glyph: '󰄗', source: { tableName: 'InventoryItem', hash: 673268892, fromRichText: false, }, }, { codepoint: 983303, glyph: '󰄇', source: { tableName: 'InventoryItem', hash: 648507367, fromRichText: false, }, }, { codepoint: 983302, glyph: '󰄆', source: { tableName: 'InventoryItem', hash: 1436723983, fromRichText: false, }, }, { codepoint: 983305, glyph: '󰄉', source: { tableName: 'ItemCategory', hash: 49, fromRichText: false, }, }, { codepoint: 57456, glyph: '', source: { tableName: 'SandboxPerk', hash: 200616812, fromRichText: true, }, }, { codepoint: 57458, glyph: '', source: { tableName: 'SandboxPerk', hash: 72139184, fromRichText: true, }, }, { codepoint: 57457, glyph: '', source: { tableName: 'SandboxPerk', hash: 136649446, fromRichText: true, }, }, { codepoint: 983307, glyph: '󰄋', source: { tableName: 'Objective', hash: 5020780, fromRichText: false, }, }, { codepoint: 983308, glyph: '󰄌', source: { tableName: 'Objective', hash: 72589355, fromRichText: false, }, }, { codepoint: 983309, glyph: '󰄍', source: { tableName: 'Objective', hash: 121105404, fromRichText: false, }, }, { codepoint: 983193, glyph: '󰂙', source: { tableName: 'Objective', hash: 106925256, fromRichText: false, }, }, { codepoint: 983221, glyph: '󰂵', source: { tableName: 'Objective', hash: 738831275, fromRichText: false, }, }, { codepoint: 983211, glyph: '󰂫', source: { tableName: 'Objective', hash: 41039169, fromRichText: false, }, }, { codepoint: 983239, glyph: '󰃇', source: { tableName: 'Objective', hash: 550810786, fromRichText: false, }, }, { codepoint: 983219, glyph: '󰂳', source: { tableName: 'Objective', hash: 1328104731, fromRichText: false, }, }, { codepoint: 983234, glyph: '󰃂', source: { tableName: 'Objective', hash: 849228675, fromRichText: false, }, }, { codepoint: 983220, glyph: '󰂴', source: { tableName: 'Faction', hash: 1021210278, fromRichText: false, }, }, { codepoint: 983209, glyph: '󰂩', source: { tableName: 'Faction', hash: 611314723, fromRichText: false, }, }, { codepoint: 983171, glyph: '󰂃', source: { tableName: 'ActivityMode', hash: 4110605575, fromRichText: false, }, }, { codepoint: 983167, glyph: '󰁿', source: { tableName: 'InventoryItem', hash: 2388937381, fromRichText: false, }, }, { codepoint: 983381, glyph: '󰅕', source: { tableName: 'ActivityMode', hash: 1164760504, fromRichText: false, }, }, { codepoint: 983164, glyph: '󰁼', source: { tableName: 'InventoryItem', hash: 3388529809, fromRichText: false, }, }, { codepoint: 983173, glyph: '󰂅', source: { tableName: 'SandboxPerk', hash: 2133645639, fromRichText: false, }, }, { codepoint: 983169, glyph: '󰂁', source: { tableName: 'ActivityMode', hash: 2043403989, fromRichText: false, }, }, { codepoint: 983158, glyph: '󰁶', source: { tableName: 'ActivityMode', hash: 608898761, fromRichText: false, }, }, { codepoint: 57669, glyph: '', source: { tableName: 'ActivityMode', hash: 103143560, fromRichText: false, }, }, { codepoint: 57461, glyph: '', source: { tableName: 'Objective', hash: 119206183, fromRichText: true, }, }, { codepoint: 57671, glyph: '', }, { codepoint: 57426, glyph: '', }, { codepoint: 983326, glyph: '󰄞', source: { tableName: 'InventoryBucket', hash: 3313201758, fromRichText: false, }, }, { codepoint: 983334, glyph: '󰄦', source: { tableName: 'InventoryItem', hash: 213377779, fromRichText: false, }, }, { codepoint: 983327, glyph: '󰄟', source: { tableName: 'InventoryItem', hash: 432217080, fromRichText: false, }, }, { codepoint: 983376, glyph: '󰅐', }, { codepoint: 983269, glyph: '󰃥', }, { codepoint: 983377, glyph: '󰅑', source: { tableName: 'Objective', hash: 62478650, fromRichText: false, }, }, ]; export type TranslateManually = | 57495 | 57654 | 983359 | 61239 | 57665 | 983382 | 61244 | 61245 | 57671 | 57426 | 983376 | 983269; /* * Could not find a source for (did the definitions disappear?): * Rocket Tracers * Unused rich text replacements (these should only be input actions replaced with the mapped buttons by the game): * [Aim Down Sights] * [Alternate Weapon Action] * [Light Attack] * [Heavy Attack] * [Super] * [###DestinyNamedSubstitutions.ui_player_action_jump_button###] * [Shoot] * [Air Dodge] * [Sprint] * [afflicted|burdened|cursed] * [Block] * [Reload] * [Boost] * [Air Move] * [Brake] * [Stasis: Glyph 0] * [Stasis: Glyph 3 Locked] * [Stasis: Glyph 1 Locked] * [Stasis: Glyph 2 Locked] * [Insert Medal Here] */ ================================================ FILE: src/earlyErrorReport.js ================================================ window.onerror = (message, source, line, col, error) => { const params = { message, source, line, col }; // eslint-disable-next-line no-console console.log(params, error); const errorBox = document.querySelector('#errorreport'); if (errorBox) { errorBox.textContent = Object.entries(params) // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string .map(([k, v]) => `${k}:\n ${v}`) .join('\n'); } }; ================================================ FILE: src/fa-subset.js ================================================ import { fontawesomeSubset } from 'fontawesome-subset'; import fs from 'node:fs/promises'; import * as icons from './app/shell/icons/Library.js'; const subset = {}; let fontCss = ` /* stylelint-disable */ @use 'sass:string'; @use './font-awesome-icon-variables.scss' as *; // Convenience function used to set content property @function fa-content($fa-var) { @return string.unquote('"#{ $fa-var }"'); } `; for (const icon of Object.values(icons)) { if (typeof icon === 'string') { const [lib, symbol] = icon.split(' '); const iconName = symbol.replace('fa-', ''); const libName = lib === 'fas' ? 'solid' : lib === 'far' ? 'regular' : lib === 'fab' ? 'brands' : undefined; if (!libName) { throw new Error('Unknown library'); } subset[libName] ||= []; subset[libName].push(iconName); fontCss = fontCss.concat( `.fa-${iconName}:before { content: fa-content($fa-var-${iconName}); }\n`, ); } } await fontawesomeSubset(subset, 'src/data/webfonts'); await fs.writeFile('src/app/shell/icons/font-awesome.scss', fontCss); ================================================ FILE: src/global.d.ts ================================================ /* eslint-disable @typescript-eslint/method-signature-style */ declare const $DIM_VERSION: string; declare const $DIM_FLAVOR: 'release' | 'beta' | 'dev' | 'test' | 'pr'; declare const $DIM_BUILD_DATE: string; declare const $DIM_WEB_API_KEY: string; declare const $DIM_WEB_CLIENT_ID: string; declare const $DIM_WEB_CLIENT_SECRET: string; declare const $DIM_API_KEY: string; declare const $BROWSERS: string[]; declare const $ANALYTICS_PROPERTY: string; declare const $PUBLIC_PATH: string; declare const $featureFlags: ReturnType; interface Window { OC?: unknown; MSStream?: unknown; // Service worker stuff __precacheManifest: string[] | undefined; __WB_MANIFEST: string[]; skipWaiting(): void; /** * You can set this in console to enable the ability to use a saved JSON * profile for debugging. */ enableMockProfile?: boolean; } interface Navigator { /** iOS-only: True if the app is running in installed mode */ standalone?: boolean; setAppBadge(num?: number); clearAppBadge(); } interface Performance { measureUserAgentSpecificMemory(): Promise; } interface MeasureMemoryResult { bytes: number; breakdown: { bytes: number; attribution: [ { url: string; scope: string; }, ]; types: string[]; }[]; } interface ObjectConstructor { groupBy( items: Iterable, keySelector: (item: Item, index: number) => string | number, ): Record; } interface MapConstructor { groupBy( items: Iterable, keySelector: (item: Item, index: number) => Key, ): Map; } declare module '*/CHANGELOG.md' { const value: string; export default value; } declare module '*.jpg' { const value: string; export default value; } declare module '*.svg' { const value: string; export default value; } declare module '*.svg?react' { import React from 'react'; const SVG: React.FC>; export default SVG; } declare module '*.png' { const value: string; export default value; } declare module '*.apng' { const value: string; export default value; } declare module '*.gif' { const value: string; export default value; } declare module '*.html' { const value: string; export default value; } declare module '*.m.scss' { const value: { [className: string]: string }; export default value; } declare module '*.scss' { const value: string; export default value; } declare module 'file-loader?*' { const value: string; export default value; } declare module 'locale/*.json' { const value: string; export default value; } declare module '@beyond-js/md5' { export default function md5(str: string): string; } ================================================ FILE: src/htaccess ================================================ # Apache Server Configs v2.14.0 | MIT License # https://github.com/h5bp/server-configs-apache # (!) Using `.htaccess` files slows down Apache, therefore, if you have # access to the main server configuration file (which is usually called # `httpd.conf`), you should add this logic there. # # https://httpd.apache.org/docs/current/howto/htaccess.html. # ###################################################################### # # CROSS-ORIGIN # # ###################################################################### # ---------------------------------------------------------------------- # | Cross-origin requests | # ---------------------------------------------------------------------- # Allow cross-origin requests. # # https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS # http://enable-cors.org/ # http://www.w3.org/TR/cors/ Header set Access-Control-Allow-Origin "*" # ---------------------------------------------------------------------- # | Cross-origin images | # ---------------------------------------------------------------------- # Send the CORS header for images when browsers request it. # # https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image # https://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html SetEnvIf Origin ":" IS_CORS Header set Access-Control-Allow-Origin "*" env=IS_CORS # ---------------------------------------------------------------------- # | Cross-origin web fonts | # ---------------------------------------------------------------------- # Allow cross-origin access to web fonts. Header set Access-Control-Allow-Origin "*" # ---------------------------------------------------------------------- # | Cross-origin resource timing | # ---------------------------------------------------------------------- # Allow cross-origin access to the timing information for all resources. # # If a resource isn't served with a `Timing-Allow-Origin` header that # would allow its timing information to be shared with the document, # some of the attributes of the `PerformanceResourceTiming` object will # be set to zero. # # http://www.w3.org/TR/resource-timing/ # http://www.stevesouders.com/blog/2014/08/21/resource-timing-practical-tips/ Header set Timing-Allow-Origin: "*" # ###################################################################### # # ERRORS # # ###################################################################### # ---------------------------------------------------------------------- # | Custom error messages/pages | # ---------------------------------------------------------------------- # Customize what Apache returns to the client in case of an error. # https://httpd.apache.org/docs/current/mod/core.html#errordocument ErrorDocument 404 /404.html # ---------------------------------------------------------------------- # | Error prevention | # ---------------------------------------------------------------------- # Disable the pattern matching based on filenames. # # This setting prevents Apache from returning a 404 error as the result # of a rewrite when the directory with the same name does not exist. # # https://httpd.apache.org/docs/current/content-negotiation.html#multiviews Options -MultiViews # ###################################################################### # # MEDIA TYPES AND CHARACTER ENCODINGS # # ###################################################################### # ---------------------------------------------------------------------- # | Media types | # ---------------------------------------------------------------------- # Serve resources with the proper media types (f.k.a. MIME types). # # https://www.iana.org/assignments/media-types/media-types.xhtml # https://httpd.apache.org/docs/current/mod/mod_mime.html#addtype # Data interchange AddType application/atom+xml atom AddType application/json json topojson map AddType application/ld+json jsonld AddType application/rss+xml rss AddType application/vnd.geo+json geojson AddType application/xml rdf xml # JavaScript # Normalize to standard type. # https://tools.ietf.org/html/rfc4329#section-7.2 AddType application/javascript js # Manifest files AddType application/manifest+json webmanifest AddType application/x-web-app-manifest+json webapp AddType text/cache-manifest appcache # Media files AddType audio/mp4 f4a f4b m4a AddType audio/ogg oga ogg opus AddType image/bmp bmp AddType image/svg+xml svg svgz AddType image/webp webp AddType video/mp4 f4v f4p m4v mp4 AddType video/ogg ogv AddType video/webm webm AddType video/x-flv flv # Serving `.ico` image files with a different media type # prevents Internet Explorer from displaying them as images: # https://github.com/h5bp/html5-boilerplate/commit/37b5fec090d00f38de64b591bcddcb205aadf8ee AddType image/x-icon cur ico # Web fonts AddType application/font-woff woff AddType application/font-woff2 woff2 AddType application/vnd.ms-fontobject eot # Browsers usually ignore the font media types and simply sniff # the bytes to figure out the font type. # https://mimesniff.spec.whatwg.org/#matching-a-font-type-pattern # # However, Blink and WebKit based browsers will show a warning # in the console if the following font types are served with any # other media types. AddType application/x-font-ttf ttc ttf AddType font/opentype otf # Other AddType application/octet-stream safariextz AddType application/x-bb-appworld bbaw AddType application/x-chrome-extension crx AddType application/x-opera-extension oex AddType application/x-xpinstall xpi AddType text/vcard vcard vcf AddType text/vnd.rim.location.xloc xloc AddType text/vtt vtt AddType text/x-component htc # ---------------------------------------------------------------------- # | Character encodings | # ---------------------------------------------------------------------- # Serve all resources labeled as `text/html` or `text/plain` # with the media type `charset` parameter set to `UTF-8`. # # https://httpd.apache.org/docs/current/mod/core.html#adddefaultcharset AddDefaultCharset utf-8 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Serve the following file types with the media type `charset` # parameter set to `UTF-8`. # # https://httpd.apache.org/docs/current/mod/mod_mime.html#addcharset AddCharset utf-8 .atom \ .bbaw \ .css \ .geojson \ .js \ .json \ .jsonld \ .manifest \ .rdf \ .rss \ .topojson \ .vtt \ .webapp \ .webmanifest \ .xloc \ .xml # ###################################################################### # # SECURITY # # ###################################################################### # ---------------------------------------------------------------------- # | Clickjacking | # ---------------------------------------------------------------------- # Protect website against clickjacking. # # The example below sends the `X-Frame-Options` response header with # the value `DENY`, informing browsers not to display the content of # the web page in any frame. # # This might not be the best setting for everyone. You should read # about the other two possible values the `X-Frame-Options` header # field can have: `SAMEORIGIN` and `ALLOW-FROM`. # https://tools.ietf.org/html/rfc7034#section-2.1. # # Keep in mind that while you could send the `X-Frame-Options` header # for all of your website’s pages, this has the potential downside that # it forbids even non-malicious framing of your content (e.g.: when # users visit your website using a Google Image Search results page). # # Nonetheless, you should ensure that you send the `X-Frame-Options` # header for all pages that allow a user to make a state changing # operation (e.g: pages that contain one-click purchase links, checkout # or bank-transfer confirmation pages, pages that make permanent # configuration changes, etc.). # # Sending the `X-Frame-Options` header can also protect your website # against more than just clickjacking attacks: # https://cure53.de/xfo-clickjacking.pdf. # # https://tools.ietf.org/html/rfc7034 # http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx # https://www.owasp.org/index.php/Clickjacking Header set X-Frame-Options "DENY" # `mod_headers` cannot match based on the content-type, however, # the `X-Frame-Options` response header should be send only for # HTML documents and not for the other resources. Header unset X-Frame-Options # ---------------------------------------------------------------------- # | Content Security Policy (CSP) | # ---------------------------------------------------------------------- # Mitigate the risk of cross-site scripting and other content-injection # attacks. # # This can be done by setting a `Content Security Policy` which # lists trusted sources of content for your website. # # The example header below allows ONLY scripts that are loaded from # the current website's origin (no inline scripts, no CDN, etc). # That almost certainly won't work as-is for your website! # # To make things easier, you can use an online CSP header generator # such as: http://cspisawesome.com/. # # http://content-security-policy.com/ # http://www.html5rocks.com/en/tutorials/security/content-security-policy/ # https://w3c.github.io/webappsec-csp/ Header set Content-Security-Policy "<%= csp %>" # credentialless is only supported by chrome but require-corp blocks Bungie.net messages # Disabled for now as it blocks our Google Fonts #Header set Cross-Origin-Embedder-Policy "credentialless" #Header set Cross-Origin-Opener-Policy "same-origin" # `mod_headers` cannot match based on the content-type, however, # the `Content-Security-Policy` response header should be send # only for HTML documents and not for the other resources. Header unset Content-Security-Policy #Header unset Cross-Origin-Embedder-Policy #Header unset Cross-Origin-Opener-Policy Header unset Content-Security-Policy #Header unset Cross-Origin-Embedder-Policy #Header unset Cross-Origin-Opener-Policy # JavaScript files must set this cross-origin-embedder policy or we can't load web workers #Header set Cross-Origin-Embedder-Policy "require-corp" # ---------------------------------------------------------------------- # | File access | # ---------------------------------------------------------------------- # Block access to directories without a default document. # # You should leave the following uncommented, as you shouldn't allow # anyone to surf through every directory on your server (which may # includes rather private places such as the CMS's directories). Options -Indexes # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Block access to all hidden files and directories with the exception of # the visible content from within the `/.well-known/` hidden directory. # # These types of files usually contain user preferences or the preserved # state of an utility, and can include rather private places like, for # example, the `.git` or `.svn` directories. # # The `/.well-known/` directory represents the standard (RFC 5785) path # prefix for "well-known locations" (e.g.: `/.well-known/manifest.json`, # `/.well-known/keybase.txt`), and therefore, access to its visible # content should not be blocked. # # https://www.mnot.net/blog/2010/04/07/well-known # https://tools.ietf.org/html/rfc5785 RewriteEngine On RewriteCond %{REQUEST_URI} "!(^|/)\.well-known/([^./]+./?)+$" [NC] RewriteCond %{SCRIPT_FILENAME} -d [OR] RewriteCond %{SCRIPT_FILENAME} -f RewriteRule "(^|/)\." - [F] # ---------------------------------------------------------------------- # | HTTP Strict Transport Security (HSTS) | # ---------------------------------------------------------------------- # Force client-side SSL redirection. # # If a user types `example.com` in their browser, even if the server # redirects them to the secure version of the website, that still leaves # a window of opportunity (the initial HTTP connection) for an attacker # to downgrade or redirect the request. # # The following header ensures that browser will ONLY connect to your # server via HTTPS, regardless of what the users type in the browser's # address bar. # # (!) Remove the `includeSubDomains` optional directive if the website's # subdomains are not using HTTPS. # # http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ # https://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14#section-6.1 # http://blogs.msdn.com/b/ieinternals/archive/2014/08/18/hsts-strict-transport-security-attacks-mitigations-deployment-https.aspx Header always set Strict-Transport-Security "max-age=16070400; includeSubDomains" # ---------------------------------------------------------------------- # | Reducing MIME type security risks | # ---------------------------------------------------------------------- # Prevent some browsers from MIME-sniffing the response. # # This reduces exposure to drive-by download attacks and cross-origin # data leaks, and should be left uncommented, especially if the server # is serving user-uploaded content or content that could potentially be # treated as executable by the browser. # # http://www.slideshare.net/hasegawayosuke/owasp-hasegawa # http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-v-comprehensive-protection.aspx # https://msdn.microsoft.com/en-us/library/ie/gg622941.aspx # https://mimesniff.spec.whatwg.org/ Header set X-Content-Type-Options "nosniff" # ---------------------------------------------------------------------- # | Server-side technology information | # ---------------------------------------------------------------------- # Remove the `X-Powered-By` response header that: # # * is set by some frameworks and server-side languages # (e.g.: ASP.NET, PHP), and its value contains information # about them (e.g.: their name, version number) # # * doesn't provide any value to users, contributes to header # bloat, and in some cases, the information it provides can # expose vulnerabilities # # (!) If you can, you should disable the `X-Powered-By` header from the # language / framework level (e.g.: for PHP, you can do that by setting # `expose_php = off` in `php.ini`) # # https://php.net/manual/en/ini.core.php#ini.expose-php Header unset X-Powered-By # ---------------------------------------------------------------------- # | Server software information | # ---------------------------------------------------------------------- # Prevent Apache from adding a trailing footer line containing # information about the server to the server-generated documents # (e.g.: error messages, directory listings, etc.) # # https://httpd.apache.org/docs/current/mod/core.html#serversignature ServerSignature Off # ###################################################################### # # WEB PERFORMANCE # # ###################################################################### SetEnv no-gzip AddEncoding gzip .svgz AddEncoding gzip .gz AddEncoding br .br RewriteEngine On RewriteBase / # Serve pre-gzipped stuff if we got it # for any request, search for pre-compressed versions (gzip or brotli) and serve that directly if it's supported # Serve pre-brotli stuff if we got it # RewriteCond %{HTTP:Accept-encoding} br # RewriteCond %{REQUEST_FILENAME}\.br -f # RewriteRule ^(.*)$ <%= publicPath %>$1.br [QSA,L] # Serve pre-gzipped stuff if we got it # RewriteCond %{HTTP:Accept-encoding} gzip # RewriteCond %{REQUEST_FILENAME}\.gz -f # RewriteRule ^(.*)$ <%= publicPath %>$1.gz [QSA,L] # Redirect unknown paths to index.html RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_URI} !\.(json|wasm|js|css|a?png|jpg|map)(\.(gz|br))?$ RewriteRule (.*) <%= publicPath %>index.html [QSA,L] # Our host seems to want to force the .gz files to be application/x-gzip ForceType text/html Header set Content-Encoding gzip ForceType application/javascript Header set Content-Encoding gzip ForceType text/css Header set Content-Encoding gzip ForceType application/json Header set Content-Encoding gzip ForceType image/svg+xml Header set Content-Encoding gzip ForceType application/font-sfnt Header set Content-Encoding gzip ForceType application/vnd.ms-fontobject Header set Content-Encoding gzip ForceType application/wasm Header set Content-Encoding gzip ForceType application/json Header set Content-Encoding gzip ForceType application/json # ---------------------------------------------------------------------- # | Content transformation | # ---------------------------------------------------------------------- # Prevent intermediate caches or proxies (e.g.: such as the ones # used by mobile network providers) from modifying the website's # content. # # https://tools.ietf.org/html/rfc2616#section-14.9.5 # # (!) If you are using `mod_pagespeed`, please note that setting # the `Cache-Control: no-transform` response header will prevent # `PageSpeed` from rewriting `HTML` files, and, if the # `ModPagespeedDisableRewriteOnNoTransform` directive isn't set # to `off`, also from rewriting other resources. # # https://developers.google.com/speed/pagespeed/module/configuration#notransform Header merge Cache-Control "no-transform" # ---------------------------------------------------------------------- # | Expires headers | # ---------------------------------------------------------------------- # Serve resources with far-future expires headers. # # (!) If you don't control versioning with filename-based # cache busting, you should consider lowering the cache times # to something like one week. # # https://httpd.apache.org/docs/current/mod/mod_expires.html ExpiresActive on ExpiresDefault "access plus 1 month" # CSS ExpiresByType text/css "access plus 1 year" # Data interchange ExpiresByType application/atom+xml "access plus 1 hour" ExpiresByType application/rdf+xml "access plus 1 hour" ExpiresByType application/rss+xml "access plus 1 hour" ExpiresByType application/json "access plus 1 year" ExpiresByType application/ld+json "access plus 0 seconds" ExpiresByType application/schema+json "access plus 0 seconds" ExpiresByType application/vnd.geo+json "access plus 0 seconds" ExpiresByType application/xml "access plus 0 seconds" ExpiresByType text/xml "access plus 0 seconds" # Favicon (cannot be renamed!) and cursor images ExpiresByType image/vnd.microsoft.icon "access plus 1 week" ExpiresByType image/x-icon "access plus 1 week" # HTML ExpiresByType text/html "access plus 0 seconds" # JavaScript ExpiresByType application/javascript "access plus 1 year" ExpiresByType application/x-javascript "access plus 1 year" ExpiresByType text/javascript "access plus 1 year" ExpiresByType application/wasm "access plus 1 year" # Manifest files ExpiresByType application/manifest+json "access plus 1 week" ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" ExpiresByType text/cache-manifest "access plus 0 seconds" # Media files ExpiresByType audio/ogg "access plus 1 year" ExpiresByType image/bmp "access plus 1 year" ExpiresByType image/gif "access plus 1 year" ExpiresByType image/jpeg "access plus 1 year" ExpiresByType image/png "access plus 1 year" ExpiresByType image/apng "access plus 1 year" ExpiresByType image/svg+xml "access plus 1 year" ExpiresByType image/webp "access plus 1 year" ExpiresByType video/mp4 "access plus 1 year" ExpiresByType video/ogg "access plus 1 year" ExpiresByType video/webm "access plus 1 year" # Web fonts # Embedded OpenType (EOT) ExpiresByType application/vnd.ms-fontobject "access plus 1 year" ExpiresByType font/eot "access plus 1 year" # OpenType ExpiresByType font/opentype "access plus 1 year" # TrueType ExpiresByType application/x-font-ttf "access plus 1 year" # Web Open Font Format (WOFF) 1.0 ExpiresByType application/font-woff "access plus 1 year" ExpiresByType application/x-font-woff "access plus 1 year" ExpiresByType font/woff "access plus 1 year" # Web Open Font Format (WOFF) 2.0 ExpiresByType application/font-woff2 "access plus 1 year" # Other ExpiresByType text/x-cross-domain-policy "access plus 1 week" # ---------------------------------------------------------------------- # | Cache-control: immutable | # ---------------------------------------------------------------------- # A new directive that prevents browsers from revalidating cached data on reload. Header merge Cache-Control "immutable" # Do not cache the service worker! # no-cache = revalidate with the server before using Header set Cache-Control "max-age=0, s-maxage=86400, must-revalidate" Header unset Expires # Do not cache html # s-maxage tells CloudFlare how long it can cache (max-age is for browsers) # We set our html to a low cache lifetime (5 min) because *many* paths map to this - our other files like service-worker.js get # manually invalidated as part of the deploy, but we can't invalidate every path e.g. https://beta.destinyitemmanager.com/4611686018433092312/d2/inventory Header set Cache-Control "max-age=0, s-maxage=300, must-revalidate" Header unset Expires # Cache version.json for only 15 minutes Header set Cache-Control "max-age=900, s-maxage=86400, must-revalidate" Header unset Expires # Cache manifest Header set Cache-Control "max-age=900, s-maxage=86400" Header unset Expires Header set Cache-Control "max-age=900, s-maxage=86400" Header unset Expires Header set Cache-Control "max-age=900, s-maxage=86400" Header unset Expires ================================================ FILE: src/index.html ================================================ DIM <% splash.forEach(function([dw, dh, dpr, or, w, h]) { %> <% }) %> <%= htmlWebpackPlugin.files.js .sort((a,b)=>a.includes("earlyErrorReport")?-1:b.includes("earlyErrorReport")?1:0) .map((p)=>``).join("\n ") %> <%= htmlWebpackPlugin.files.css .map((p)=>``).join("\n ") %>

DIM failed to load. Please follow our Troubleshooting Guide to find a solution, and contact us if those steps don't work.


      
================================================ FILE: src/locale/de.json ================================================ { "AWA": { "ConfirmDescription": "Bitte benutze die Destiny 2-Gefährten-App, um DIM das Modifizieren deiner Gegenstände zu erlauben.", "ConfirmTitle": "Aktion bestätigen", "Error": "Fehler beim Ändern von Mods oder Perks", "ErrorMessage": "Wir konnten {{plug}} nicht auf {{item}} ausrüsten.\n\n{{error}}", "FailedToken": "Konnte keine Berechtigung zum Ändern des Gegenstands erhalten", "IrreversiblePlugging": "Du besitzt {{plug}} nicht, also werden wir es nicht ersetzen." }, "Accounts": { "Choose": "Profile für {{bungieName}}", "ErrorLoadInventory": "Fehler beim Laden deiner Destiny {{version}} Charaktere und des Inventars", "ErrorLoadManifest": "Destiny Info-Datenbank kann nicht von Bungie geladen werden", "ErrorLoading": "Destiny Accounts können nicht von Bungie.net geladen werden", "MissingAccountWarning": "Wenn du dein Konto hier nicht siehst, hast du dich möglicherweise nicht mit dem richtigen Bungie.net-Konto eingeloggt oder Bungie.net ist wegen Wartungsarbeiten möglicherweise nicht verfügbar.", "MissingDescription": "Das Konto, das du sehen möchtest, ist kein, mit deinem Bungie.net-Profil, verknüpftes Konto. Wähle eines deiner Konten unten.", "MissingTitle": "Account nicht gefunden", "NoCharacters": "Zu diesem Bungie.net-Account können keine Destiny-Charaktere gefunden werden. Versuche dich mit einem anderen Account einzuloggen.", "NoCharactersTitle": "Keine Charaktere gefunden", "SwitchAccounts": "Du kannst die Konten später aus dem Menü in der Kopfzeile wechseln.", "Title": "Konten" }, "Activities": { "Activities": "Aktivitäten", "Hard": "Schwer", "Nightfall": "Dämmerungs-Strike", "Normal": "Normal", "WeeklyHeroic": "Wöchentlicher Heroischer Strike" }, "Armory": { "AlternateItems": "Alternative Versionen", "Armory": "Waffenkammer", "DifferentSeason": "Neu von einer anderen Saison", "NoNotes": "Keine Notizen", "OpenInArmory": "im Arsenal ansehen", "Season": "Saison {{season}}, Jahr {{year}}", "TrashlistedRolls_one": "Mülllisten-Roll", "TrashlistedRolls_other": "{{count, number}} Mülllisten-Rolls", "Unknown": "Unbekannter Gegenstand", "UnknownPerkHash": "Der Perk Hash {{hash}} ({{perkName}}) erscheint nicht auf diesem Element, daher ist dieser Wunschlisten-Roll ungültig. Bitte kontaktiere den Autor der Wunschliste, um dies zu korrigieren. Bitte beachte, dass Wunschlisten immer die nicht verbesserte Version von Perks angeben sollten.", "WishlistedRolls_one": "Wunschlisten-Roll", "WishlistedRolls_other": "{{count, number}} Wunschlisten-Rolls", "YourItems": "Deine Gegenstände" }, "Browsercheck": { "Samsung": "Samsung Internet kann Websites zu dunkel erscheinen lassen, wenn der dunkle Modus eingeschaltet ist. Einstellungen > Labs > Webseite dunkles Theme verwenden oder wechsle zu einem anderen Browser.", "Steam": "Der Steam-Overlay-Browser ist sehr alt und einige oder alle DIM-Funktionen funktionieren möglicherweise nicht. Wir können dies nicht unterstützen.", "Unsupported": "Das DIM-Team unterstützt diesen Browser nicht. Einige oder alle DIM-Funktionen funktionieren möglicherweise nicht." }, "Bucket": { "Armor": "Rüstung", "Class": "Fokus", "General": "Allgemein", "Ghost": "Geist", "Inventory": "Inventar", "Postmaster": "Poststelle", "Progress": "Fortschritt", "Reputation": "Ruf", "Unknown": "Unbekannt", "Vault": "Tresor", "Weapons": "Waffen" }, "BulkNote": { "Append": "An Notizen anhängen / #Hashtags hinzufügen", "Confirm": "Notizen aktualisieren", "Remove": "Von Notizen entfernen / #Hashtags entfernen", "Replace": "Notizen ersetzen", "Title_one": "Notizen für 1 Gegenstand ändern", "Title_other": "Notizen für {{count}} Gegenstände ändern" }, "BungieAlert": { "Title": "Eine Nachricht von Bungie:" }, "BungieService": { "AppNotPermitted": "DIM hat keine Berechtigung diese Aktion durchzuführen.", "DestinyCannotPerformActionAtThisLocation": "Du kannst keine Gegenstände ausrüsten oder Mods wechseln, während du dich in einer Aktivität befindest. Geh dafür in den Orbit oder einen sozialen Bereich. Dies liegt an der Bungie.net-API, nicht an DIM.", "DestinyItemUnequippable": "Du kannst diesen Gegenstand nicht ausrüsten. Falls die letzte Aktivität dieses Charakters das Loadout gesperrt hatte, logge dich bitte neu in den Charakter ein.", "DestinyLegacyPlatform": "Bungies Dienste haben derzeit einen Bug, der DIM daran hindert Informationen für deinen Destiny 2 Account zu laden, wenn du Destiny 1 auf einer älteren Konsolengeneration gespielt hast. Bungie wird den Fehler bald beheben, aber bis dahin musst du Destiny 1 auf einer neuen Konsole spielen, um die Informationen laden zu können.", "DevVersion": "Du hast eine Entwicklungsversion von DIM? Du musst deine Chrome-Erweiterung auf Bungie.net registrieren.", "Difficulties": "Bungie.net hat derzeit Schwierigkeiten.", "ErrorTitle": "Bungie.net-Fehler", "ItemUniquenessExplanation": "Ein Charakter kann nur ein '{{name}}' haben.", "Maintenance": "Bungie.net-Server werden zur Zeit gewartet.", "MissingInventory": "Bungie.net hat dein Inventar nicht zurückgegeben, möglicherweise weil deine Privatsphäre-Einstellungen es verhindern. Versuche dich auszuloggen und dich wieder einzuloggen.", "NetworkError": "Netzwerkfehler - {{status}} {{statusText}}", "NoAccount": "Kein Destiny Konto gefunden. Hast du die richtige Plattform ausgewählt?", "NoAccountForPlatform": "Konnte auf {{platform}} keinen Destiny-Account für dich finden.", "NotConnected": "Du bist eventuell nicht mit dem Internet verbunden.", "NotConnectedOrBlocked": "Du bist eventuell nicht mit dem Internet verbunden, oder ein Werbeblocker oder eine Privatsphäre-Erweiterung blockieren möglicherweise Bungie.net.", "NotLoggedIn": "Bitte autorisiere DIM, um diese App zu nutzen.", "Slow": "Bungie.net ist gerade langsam", "SlowDetails": "Bungie.net braucht eine lange Zeit, um Deine Informationen zurückzugeben. Dies kann passieren, wenn viele Spieler gleichzeitig im Spiel sind oder wenn Bungie.net Probleme hat. Möglicherweise hast Du auch ein Problem mit der Internetverbindung. Wir werden weiter auf eine Antwort warten.", "SlowResponse": "Bungie.net war zu langsam, um zu reagieren.", "Throttled": "Bungie.net beschränkt, wie viele Anfragen DIM machen kann.", "Twitter": "Hol dir Status-Updates auf:", "UnknownError": "Bungie.net Nachricht: {{message}}", "VendorNotFound": "Händlerdaten nicht verfügbar." }, "Compare": { "Archetype": "Archetyp", "AssumeMasterworked": "Als Meisterwerk ansehen", "AssumeMasterworkedDescription": "Stats wenn vollständig gemeisterwerkt, ohne aktuelle Mods", "BaseStatsDescription": "Basis-Stats, ohne Meisterwerk oder Mods", "Button": "Vergleiche", "ButtonHelp": "Items vergleichen", "CompareBaseStats": "Basis-Stats anzeigen", "CurrentStats": "Aktuelle Stats", "CurrentStatsDescription": "Aktuelle Stats, einschließlich Mods und Meisterwerk-Level", "Error": { "Invalid": "Es gibt keine gültigen Gegenstände zum Vergleich.", "Unmatched": "Dieser Gegenstand stimmt nicht mit der Art der zu vergleichenden Gegenstände überein." }, "InitialItem": "Das ist der Gegenstand, von dem aus der Vergleich gestartet wurde", "IsVendorItem": "Dieser Gegenstand ist nicht in deinem Inventar, aber {{vendorName}} verkauft ihn.", "NoModArmor": "Pre-Mods" }, "Cooldown": { "Grenade": "Granaten-Abklingzeit: {{cooldown}}", "Melee": "Nahkampf-Abklingzeit: {{cooldown}}", "Super": "Super-Abklingzeit: {{cooldown}}" }, "Countdown": { "Days_compact_one": "{{count}}T", "Days_compact_other": "{{count}}T", "Days_one": "1 Tag", "Days_other": "{{count}} Tage" }, "Csv": { "EmptyFile": "Es waren keine Zeilen in der Datei.", "ImportConfirm": "Bist du sicher, dass du Markierungen/Notizen von CSV importieren möchtest? Dies überschreibt Markierungen/Notizen für alle in deiner Tabelle enthaltenen Elemente.", "ImportFailed": "Fehler beim Importieren von Tags/Notizen von CSV: {{error}}", "ImportSuccess_one": "Markierung/Notiz für ein Element geladen.", "ImportSuccess_other": "Markierungen/Notizen für {{count}} Einträge geladen.", "ImportWrongFileType": "Die Datei ist keine CSV-Datei.", "WrongFields": "CSV muss die Spalten 'Id', 'Notes', 'Tag' und 'Hash' haben." }, "Dialog": { "Cancel": "Abbrechen", "OK": "OK" }, "EnergyMeter": { "Energy": "Energie", "Unused": "Unbenutzt", "UpgradeNeeded": "Die aktuelle Energiekapazität dieses Gegenstands ist {{energyCapacity}}. Um die ausgewählten Mods auszurüsten, muss seine Energiekapazität {{energyUsed}} sein.", "Used": "Benutzt" }, "ErrorBoundary": { "Title": "Ein Fehler ist aufgetreten" }, "ErrorPanel": { "BrowserTooOld": "Dein Browser ist zu alt für DIM. Bitte aktualisiere deinen Browser auf die neueste Version.", "BrowserTooOldTitle": "Inkompatibler Browser", "Description": "Versuche dein Inventar in der Destiny 2 Companion App zu laden, um zu sehen, ob Bungie.net nicht verfügbar ist.", "ReadTheGuide": "Lies unser Benutzerhandbuch (im Menü verlinkt) für Schritte zur Fehlerbehebung.", "SystemDown": "Dies betrifft alle Destiny-Apps, und das DIM-Team kann es nicht reparieren oder umgehen.", "Troubleshooting": "Fehlerbehebung" }, "FarmingMode": { "D2Desc_female_one": "DIM verhindert, dass Gegenstände an die Poststelle gehen, indem sichergestellt wird, dass auf {{store}} immer ein freier Platz pro Gegenstandstyp vorhanden ist.", "D2Desc_female_other": "DIM verhindert, dass Gegenstände an die Poststelle gehen, indem sichergestellt wird, dass auf {{store}} immer {{count}} freie Plätze pro Gegenstandstyp vorhanden sind.", "D2Desc_male_one": "DIM verhindert, dass Gegenstände an die Poststelle gehen, indem sichergestellt wird, dass auf {{store}} immer ein freier Platz pro Gegenstandstyp vorhanden ist.", "D2Desc_male_other": "DIM verhindert, dass Gegenstände an die Poststelle gehen, indem sichergestellt wird, dass auf {{store}} immer {{count}} freie Plätze pro Gegenstandstyp vorhanden sind.", "D2Desc_one": "DIM verhindert, dass Gegenstände an die Poststelle gehen, indem sichergestellt wird, dass auf {{store}} immer ein freier Platz pro Gegenstandstyp vorhanden ist.", "D2Desc_other": "DIM verhindert, dass Gegenstände an die Poststelle gehen, indem sichergestellt wird, dass auf {{store}} immer {{count}} freie Plätze pro Gegenstandstyp vorhanden sind.", "Desc_female_one": "DIM verschiebt Engramme und Glimmer von der {{store}} in den Tresor und lässt einen Platz pro Gegenstandstyp frei, um zu verhindern, dass etwas zur Poststelle geschickt wird.", "Desc_female_other": "DIM verschiebt Engramme und Glimmer vom {{store}} in den Tresor und lässt {{count}} Plätze pro Gegenstandstyp frei, um zu verhindern, dass etwas zur Poststelle geschickt wird.", "Desc_male_one": "DIM verschiebt Engramme und Glimmer von der {{store}} in den Tresor und lässt einen Platz pro Gegenstandstyp frei, um zu verhindern, dass etwas zur Poststelle geschickt wird.", "Desc_male_other": "DIM verschiebt Engramme und Glimmer vom {{store}} in den Tresor und lässt {{count}} Plätze pro Gegenstandstyp frei, um zu verhindern, dass etwas zur Poststelle geschickt wird.", "Desc_one": "DIM verschiebt Engramme und Glimmer von der {{store}} in den Tresor und lässt einen Platz pro Gegenstandstyp frei, um zu verhindern, dass etwas zur Poststelle geschickt wird.", "Desc_other": "DIM verschiebt Engramme und Glimmer vom {{store}} in den Tresor und lässt {{count}} Plätze pro Gegenstandstyp frei, um zu verhindern, dass etwas zur Poststelle geschickt wird.", "FarmingMode": "Farm-Modus", "FarmingModeNote": "(Platz für Loot freimachen)", "MakeRoom": { "Desc": "DIM verschiebt Engramme und Glimmer-Gegenstände von der {{store}} zum Tresor oder zu anderen Charakteren, um zu verhindern, dass etwas in der Poststelle landet.", "Desc_female": "DIM verschiebt Engramme und Glimmer-Gegenstände von der {{store}} zum Tresor oder zu anderen Charakteren, um zu verhindern, dass etwas in der Poststelle landet.", "Desc_male": "DIM verschiebt Engramme und Glimmer-Gegenstände von der {{store}} zum Tresor oder zu anderen Charakteren, um zu verhindern, dass etwas in der Poststelle landet.", "MakeRoom": "Schaffe Platz durch Verschieben von Gegenständen, um neue Gegenstände aufnehmen zu können", "Tooltip": "Wenn ausgewählt, wird DIM Waffen und Rüstungen verschieben, um im Tresor Platz für Engramme zu schaffen." }, "OutOfRoom": "Du hast keinen Platz mehr, um Gegenstände von {{character}} zu verschieben. Es ist Zeit den Müll wegzuschmeißen!", "OutOfRoomTitle": "Kein Platz mehr", "Stop": "Stopp", "Vault": "Es verschiebt Gegenstände in den Tresor, um Platz zu schaffen." }, "FashionDrawer": { "Accept": "Kosmetik speichern", "CannotFitOrnament": "Dieser Gegenstand hat keinen Ornamenten-Sockel oder du hast keine Ornamente dafür.", "CannotFitShader": "Diesem Gegenstand kann kein Shader zugewiesen werden", "ClearOrnaments": "Ornament-Auswahl aufheben", "ClearOrnamentsTitle": "Alle Ornamente mit \"Keine Präferenz\" markieren", "ClearShaders": "Shader-Auswahl aufheben", "ClearShadersTitle": "Alle Shader mit \"Keine Präferenz\" markieren", "NoPreference": "Keine Präferenz - diese Fassung bleibt unverändert", "Reset": "Kosmetik löschen", "Sync": "Synchr.", "SyncOrnaments": "Ornamente synchronisieren", "SyncOrnamentsTitle": "Ornamente aus demselben Set für alle Gegenstände verwenden, falls freigeschaltet", "SyncShaders": "Shader synchr.", "SyncShadersTitle": "Denselben Shader für alle Gegenstände verwenden", "Title": "Shader und Ornamente auswählen", "UseEquipped": "Derzeitige Kossmetik verwenden" }, "FileUpload": { "Instructions": "Klicke oder ziehe Dateien" }, "Filter": { "Adept": "\\(Meister\\)", "AmmoType": "Zeigt Gegenstände basierend auf deren Munitionstyp.", "Armor": "Zeigt Gegenstände, die Rüstung sind.", "Armor3": "Zeigt Gegenstände an, die das 'Rüstung 3.0'-System verwenden, das mit \"Am Rande des Schicksals\" eingeführt wurde.", "ArmorCategory": "Zeigt Rüstungen basierend auf deren Kategorie.", "ArmorIntrinsic": "Zeigt legendäre Rüstung, die einen intrinsischen Perk hat, wie zum Beispiel Kunstvolle Rüstung.", "Artifice": "Zeigt kunstvolle Rüstung.", "Ascended": "Zeigt Gegenstände, die einen aufsteigenden Knoten haben, die aufgestiegen sind.", "Breaker": "Filter nach Brecher-Typ oder dem entsprechenden Champion-Typ. Breaker:instrinsic zeigt Gegenstände mit intrinsischer Brecherfähigkeit.", "BulkClear_one": "Markierung von 1 Gegenstand entfernt.", "BulkClear_other": "Markierungen von {{count}} Gegenständen entfernt.", "BulkRevert_one": "Markierung auf 1 Gegenstand zurückgesetzt.", "BulkRevert_other": "Markierungen auf {{count}} Gegenständen zurückgesetzt.", "BulkTag_one": "Ausgewählte Gegenstände als {{tag}} markiert.", "BulkTag_other": "{{count}} ausgewählte Gegenstände als {{tag}} markiert.", "Catalyst": "Zeigt Katalysatoren nach Status. catalyst:complete zeigt vollständige und angewendete Katalysatoren; catalyst:incomplete zeigt freigeschaltete, aber unvollständige bzw. nicht angewendete Katalysatoren; und catalyst:missing zeigt Gegenstände, die Platz für Katalysatoren haben, die du noch nicht gefunden hast.", "Class": "Zeigt Gegenstände basierend auf ihrer Klassenaffinität an.", "Combine": "Filter können kombiniert oder mit Klammern gruppiert werden. Nutze \"or\" und \"and\", um deine Suche einzugrenzen, zum Beispiel \"{{example}}\".", "ContributePower": "Zeige Gegenstände, die Power haben und zu deinem Power-Level beitragen können.", "Cosmetic": "Zeigt Gegenstände, die Flair oder Kosmetika sind.", "Craftable": "Zeigt formbare Gegenstände an.", "CraftedDupe": "Zeigt doppelte Waffen, bei denen mindestens eines der Duplikate hergestellt wurde.", "Curated": "Zeigt Gegenstände die ein kuratierter Roll sind.", "CurrentClass": "Zeigt Gegenstände an, die auf dem derzeit eingeloggten Hüter ausrüstbar sind.", "CustomStatLower": "Zeigt Rüstungen an, deren Stats strikt niedriger sind als die anderer Rüstungen desselben Typs, wobei nur die Stats aus der Liste \"individuelle Summe\" dieser Klasse berücksichtigt werden.", "DamageType": "Zeigt Gegenstände basierend auf deren Schadenstyp.", "Deepsight": "Zeigt Waffen mit Tiefenblick-Aktivierung, deren Bauplan extrahiert oder die Tiefenblick-Aktivierung mit einem Tiefenblick-Harmonisierer verliehen werden kann.", "Deprecated": "Dieser Filter wird nicht mehr unterstützt.", "Description": "Beschreibung", "DescriptionFilter": "Zeigt Gegenstände, deren Beschreibung teilweise mit dem Filtertext übereinstimmt. Suche ganze Phrasen mit Anführungszeichen.", "DisabledModSlot": "Zeigt Items mit deaktiviertem Mod.", "Dupe": "Zeigt doppelte Gegenstände, einschließlich Neuauflagen", "DupeArchetype": "Gruppiert Rüstung mit dem gleichen Stats-Archetyp.", "DupeCount": "Gegenstände, die die angegebene Anzahl an Duplikaten haben.", "DupeLower": "Doppelte Gegenstände, einschließlich Neuauflagen, die nicht das höchste Powerlevel haben. Nur ein Duplikat wird als das höchste ausgewählt, und der Rest als niedriger betrachtet.", "DupePerks": "Zeigt Gegenstände, deren Perks entweder ein Duplikat oder eine Teilmenge eines Gegenstands desselben Typs sind.", "DupeSetBonus": "Gruppiert Rüstung mit dem gleichen Stats-Archetyp.", "DupeStats": "Zeigt Rüstungen mit identischen Basis-Stats, einschließlich Stat-Verstellern wie Kunstvoll und Tuning-Mods.", "DupeTertiary": "Gruppiert Rüstungen mit dem gleichen tertiären Stat.", "DupeTraits": "Waffen, deren Eigenschaften entweder ein Duplikat oder eine Teilmenge einer Waffe desselben Typs sind.", "DupeTunedStat": "Gruppiert Rüstung mit dem gleichen Tuning-Stat.", "DupeUntunedStats": "Gruppiert Rüstungen mit identischen Basis-Stats, ignoriert Tuning-Mods.", "DupeZeroStats": "Gruppiert Rüstung mit den gleichen 3 Nicht-Null-Basis-Stats.", "Energy": "Zeigt Gegenstände an, die das 'Rüstung 2.0'-System verwenden, das mit \"Festung der Schatten\" eingeführt wurde.", "EnergyCapacity": "Zeigt Gegenstände basierend auf ihrer aktuellen Energiekapazität.", "Engrams": "Zeige Engramme.", "Enhanceable": "Zeigt Waffen, die verbessert werden können.", "Enhanced": "Zeigt Waffen basierend auf ihrer Verbesserungsstufe.", "EnhancedPerk": "Zeigt Waffen, die die angegebene Anzahl an verbesserten Perks haben.", "EnhancementReady": "Zeigt Waffen, die Schwellenwerte für die Perk Verbesserung erreicht haben.", "Equipment": "Gegenstände, die ausgerüstet werden können.", "Equipped": "Gegenstände, die aktuell von einem Charakter ausgerüstet sind.", "Event": "Zeigt Gegenstände nach dem Event in dem sie in Destiny 2 erschienen sind an.", "ExtraPerk": "Zeigt Legendäre Waffen mit zufälligen Rolls und einem zusätzlich auswählbaren Perk.", "Featured": "Gegenstände, die in der aktuellen Saison als \"Neue Ausrüstung\" oder \"Im Brennpunkt stehende Gegenstände\" gelten.", "Filter": "Filter", "FilterWith": "Filtern mit:", "Focusable": "Zeigt Gegenstände an, die bei einem Händler fokussiert werden können", "Foundry": "Zeigt an, von welcher Schmiede die Gegenstände stammen.", "Glimmer": "Zeigt Verbrauchsgegenstände, die mit dem Erhalt von Glimmer verbunden sind.", "Harrowed": "\\(Gequält\\)", "HasNotes": "Zeige Gegenstände mit Notizen an.", "HasOrnament": "Zeigt Gegenstände an, die ein Ornament haben.", "HasShader": "Zeigt Gegenstände, die einen Shader angewendet haben.", "Holofoil": "Zeigt Holofoil Waffen.", "InDimLoadout": "is:iningameloadout zeigt Gegenstände an, die in einem DIM-Loadout enthalten sind.", "InInGameLoadout": "is:iningameloadout zeigt Gegenstände an, die in einem In-Game-Loadout enthalten sind.", "InInventory": "Zeigt Items an, von denen du mindestens eine Kopie in deinem Inventar hast. Nur wirklich nützlich in den Bildschirmen Händler und Sammlungen.", "InLoadout": "is:inloadout zeigt Gegenstände, die in einem beliebigen Loadout enthalten sind. Eine Suche mit inloadout: zeigt Gegenstände, die in Loadouts mit passenden Titeln enthalten sind. Bei Verwendung eines Hashtags zeigt inloadout: Gegenstände an, deren Loadouts den Hashtag im Titel oder Notizen haben. Bei Verwendung mit einem Bereich werden Gegenstände angezeigt, die in entsprechend vielen Loadouts enthalten sind.", "Infusable": "Zeigt Gegenstände, die infundiert werden können.", "InfusionFodder": "Zeigt Gegenstände, die für Glimmer, in einen gleichen Gegenstand, mit nicht so viel Power, infundiert werden können.", "IsAdept": "Zeigt Waffen, die mit Meister-Mods kompatibel sind.", "IsCrafted": "Zeigt Waffen, die hergestellt wurden.", "ItemHash": "Zeigt die Gegenstände mit dem angegebenen Inventar-Item Hash an. Für fortgeschrittene Benutzer.", "ItemId": "Zeigt das Item mit der angegebenen Inventar-Item-ID. Für fortgeschrittene Benutzer.", "Leveling": { "Complete": "{{term}} - zeigt Gegenstände, die fertig sind - jedes Upgrade freigeschaltet.", "Incomplete": "{{term}} - zeigt Gegenstände, die nicht fertig sind - es gibt noch mindestens ein freischaltbares Upgrade.", "NeedsXP": "{{term}} - zeigt Gegenstände, in die noch XP gesteckt werden kann.", "Upgraded": "{{term}} - zeigt Gegenstände an, die genug XP haben um alle Knoten freizuschalten, aber es sind noch nicht alle freigeschaltet worden.", "XPComplete": "{{term}} - zeigt Gegenstände, in die keine XP gesteckt werden können (egal ob deren Upgrades freigeschaltet wurden oder nicht)." }, "Location": "Zeigt Gegenstände basierend auf ihrer Position in der App. Links/Mitte/Rechts sind die visuelle Position des Charakters, und während 'inleftchar' immer funktionieren wird, basieren die anderen beiden darauf wie viele Charaktere du hast. 'current' ist dein letzter/aktuell eingeloggter Charakter (markiert mit einem gelben Dreieck).", "LockAllFailed": "Fehler beim Sperren von Gegenständen", "LockAllSuccess": "{{num}} Gegenstände gesperrt", "Locked": "Zeigt Gegenstände basierend auf deren Sperr-Status.", "Masterwork": "Zeigt Gegenstände basierend auf deren Meisterwerk-Stats oder Meisterwerk-Level.", "MasterworkKills": "Zeigt Gegenstände basierend auf dem Wert des Meisterwerk-Kill-Trackers an.", "MaxPower": "Zeigt Gegenstände mit der höchsten Power pro Slot.", "MaxPowerLoadout": "Zeigt die Gegenstände im Loadout, die deinen Powerlevel für jede Charakterklasse maximieren würden.", "Memento": "Zeigt Waffen, die einen Memento-Slot haben.", "ModSlot": "Zeigt Rüstungen mit einem bestimmten Mod-Typ-Slot.", "Mods": { "Y3": "Zeigt Gegenstände mit beliebigen, ausgestatteten Mods an." }, "Name": "Zeigt Gegenstände, deren Name genau (exaktname:), oder teilweise (name:), mit dem Filtertext übereinstimmt. Suche nach ganzen Phrasen mit Anführungszeichen.", "NamedStat": "Zeigt Rüstung, die Punkte in der jeweiligen Kategorie hat.", "Negate": "Um eine Suche zu negieren, nutze ein Minuszeichen als Präfix für diesen Suchbegriff oder dem Wort \"not\", zum Beispiel \"{{notexample}}\" oder \"{{notexample2}}\".", "NewItems": "Zeigt neue Gegenstände.", "Notes": "Zeigt Gegenstände, denen du eine eigene Notiz hinzugefügt hast.", "OriginTrait": "Zeigt Waffen, die einen Ursprungs-Perk haben.", "Ornament": "Zeigt Items mit Ornamenten und filtert nach ihrem Status.", "PartialMatch": "Zeigt Gegenstände, deren Name, Beschreibung, jeglicher Perk, oder irgendein Mod eine teilweise Übereinstimmung zum Suchtext haben. Suche nach ganzen Phrasen mittels Anführungszeichen.", "PatternUnlocked": "Zeigt Gegenstände, dessen Bauplan freigeschaltet ist, auch wenn der Gegenstand an sich nicht geformt wurde.", "Perk": "Zeigt Gegenstände, bei denen einer der Perks oder Mods den Suchtext im Namen oder in der Beschreibung enthält. Suche nach ganzen Phrasen mit Anführungszeichen.", "PerkName": "Zeigt Gegenstände mit einem Perk oder Mod an, deren Name exakt (exactperk:) oder partiell (perkname:) mit dem Filtertext übereinstimmt. Suche nach ganzen Phrasen mit Hilfe von Anführungszeichen.", "PinnacleReward": "Zeigt Aktivitäten, welche eine Spitzenbelohnung hervorbringen.", "Postmaster": "Gegenstände, die derzeit bei der Poststelle sind.", "PowerKeywords": "Verwende statt einer Zahl die Schlüsselwörter \"pinnaclecap\" oder \"softcap\", um auf das Powerlevel der aktuellen Saison zu verweisen.", "PowerLevel": "Zeigt Gegenstände basierend auf ihrem Powerlevel an. $t(Filter.PowerKeywords)", "PowerfulReward": "Zeigt Aktivitäten, welche eine mächtige Belohnung hervorbringen.", "PrismaticDamageType": "Zeigt Gegenstände basierend darauf, ob sie ein Licht- oder Dunkelheitsschaden sind. Lichtarten sind Arkus, Solar und Leere. Dunkelheitstypen sind Stasis und Strang.", "Quality": "Zeigt Gegenstände basierend auf ihrer prozentualen Gesamtqualität an. '{{percentage}}' ist ein Synonym für '{{quality}}'.", "RandomRoll": "Zeigt Gegenstände an, die mit Zufalls-Rolls droppen.", "RarityTier": "Zeigt Gegenstände basierend auf deren Seltenheits-Rang an.", "Reforgeable": "Zeigt Gegenstände an, die beim Waffenmeister umgeschmiedet werden können.", "Release": "Zeigt Gegenstände, erhältlich von einem bestimmten Release oder Event.", "RequiredLevel": "Zeigt Gegenstände basierend auf deren Mindestlevel an.", "RetiredPerk": "Zeigt Waffen mit nicht mehr erhältlichen Perks.", "SearchPrompt": "Verfügbare Filterbefehle durchsuchen", "Season": "Zeigt Gegenstände aus der Saison, in der sie in Destiny 2 erschienen sind, an.", "StackFull": "Zeigt Gegenstände, deren Stapel voll ist (Verbesserungskerne, Seltsame Münzen, Waffenmeister-Materialien, usw.)", "StackLevel": "Zeigt Gegenstände basierend auf der Menge der Gegenstände im Stapel an.", "Stackable": "Zeigt Gegenstände an, die gestapelt werden können (Munition-Synthesen, Seltsame Münzen, etc.)", "StatLower": "Zeigt Rüstungen an, deren Stats strikt niedriger sind als die anderer Rüstungen des selben Typs.", "Stats": "Zeigt Gegenstände basierend auf einem bestimmten Stat-Wert an. $t(Filter.StatsExtras)", "StatsBase": "Filtert Rüstungen basierend auf ihrem Basis-Stat-Wert, Mod- oder Meisterwerk-Verbesserungen werden ignoriert. $t(Filter.StatsExtras)", "StatsExtras": "Unterstützt das Addieren von Stats durch das Verbinden mehrerer Stat-Namen mit dem + oder & Symbol. Es gibt auch spezielle Schlüsselwörter \"highest\", \"secondhighest\", \"thirdhighest\" usw. die Stats auf der Grundlage ihres Rangs in den Statistiken eines Gegenstands widerspiegeln. Jeder benutzerdefinierte Summe-Name hat auch einen eigenen Suchbegriff, der in den benutzerdefinierte Summe-Einstellungen angezeigt wird.", "StatsLoadout": "Findet ein Set von Gegenständen, die für den maximalen Gesamtwert eines bestimmten Stats geeignet sind.", "StatsMax": "Findet Rüstung mit dem höchstmöglichen Wert für einen bestimmten Stat. Enthält alle Gegenstände mit den höchsten Stats.", "StatsOrdinal": "Findet Rüstung 3.0 mit der angegebenen Stat-Fokussierung.", "Tags": { "Tag": "Zeigt Gegnstände mit einem bestimmten Tag an.", "Tagged": "Zeigt Gegenstände mit irgend einem Tag an." }, "Tier": "Zeigt Gegenstände basierend auf ihrem Tier von 0-5.", "Timelost": "\\(zeitverirrt\\)", "Tracked": "Zeigt Quests/Beutezüge basierend auf ihrem Verfolgungszustand an.", "Transferable": "Gegenstände, die zwischen den Charakteren verschoben werden können.", "Trashlist": "Zeigt Gegenstände, die der Müllliste deiner Wunschliste entsprechen.", "TunedStat": "Zeigt Items mit Tuning-Mods für die angegebenen Stat.", "Unascended": "Zeigt Gegenstände, die einen aufsteigenden Knoten haben, die nicht aufgestiegen sind.", "Undo": "Rückgängig", "UnlockAllFailed": "Konnte Gegenstände nicht entsperren", "UnlockAllSuccess": "{{num}} Gegenstände entsperrt", "Vendor": "Artikel ist bei einem bestimmten Händler erhältlich.", "VendorItem": "Gegenstände von einem Händler, die nicht in deinem Inventar sind. Nützlich, um Händlergegenstände vom Loadout-Optimierer auszuschließen.", "Weapon": "Zeigt Gegenstände, die Waffen sind.", "WeaponLevel": "Zeigt Waffen basierend auf ihrem Waffen-Level an.", "WeaponType": "Zeigt Waffen basierend auf ihrem Waffentyp an.", "Wishlist": "Zeigt Gegenstände, die deiner Wunschliste entsprechen.", "WishlistDupe": "Zeigt Duplikate an, von denen mindestens eines auf deiner Wunschliste steht.", "WishlistEnabled": "Zeigt Gegenstände, die berechtigt sind, Wunschlisten-Rolls zu haben.", "WishlistNotes": "Zeigt die Wunschlisten-Gegenstände an, deren Notizen der Suche entsprechen.", "WishlistUnknown": "Zeigt Gegenstände ohne Roll-Empfehlungen in den geladenen Wunschlisten an.", "Year": "Zeigt Gegenstände nach dem Jahr in dem sie in Destiny erschienen sind an." }, "General": { "ClickForDetails": "Für Details klicken", "Close": "Schließen", "Confirm": "Bestätigen?", "UserGuideLink": "Benutzerhandbuch" }, "Glyphs": { "Axe": "Axt", "DarkAbility": "Dunkelheitsfähigkeit", "Gilded": "Vergoldet", "Harmonic": "Harmonisch", "HiveSword": "Schar-Schwert", "LightAbility": "Lichtfähigkeit", "LightLevel": "Licht-Level", "Misadventure": "Missgeschick", "Missing": "Fehlend", "OpenSymbolsPicker": "Symbolauswahl öffnen", "Prismatic": "Prisma", "Quickfall": "Schnellfall", "RespawnRestricted": "Wiedereinstieg eingeschränkt", "ScorchCannon": "Scorch-Kanone", "SearchSymbols": "Symbole suchen...", "Smoke": "Rauch" }, "Header": { "About": "Über DIM", "AutoRefresh": "DIM wird automatisch aktualisieren, solange du noch spielst.", "BulkTag": "Massenbeschriftung Gegenstände", "BungieNetAlert": "Bungie-Warnung", "Clear": "Suchfilter löschen", "CompareMatching": "Items vergleichen", "DeleteSearch": "Suche löschen", "FilterHelp": "Suche Item/Perk, {{example}} und mehr", "FilterHelpBrief": "Suche Gegenstände", "FilterHelpLoadouts": "Loadout-Namen und -Notizen suchen", "FilterHelpMenuItem": "Filter Hilfe...", "FilterHelpOptimizer": "Filter Rüstung, die in Loadouts enthalten ist, z.B.: {{example}}", "FilterHelpProgress": "Suche Meilensteine und Herausforderungen", "FilterHelpRecords": "Triumphe und Sammlungen durchsuchen", "FilterMatchCount_one": "1 Gegenstand", "FilterMatchCount_other": "{{count}} Gegenstände", "Filters": "Filter", "InstallDIM": "Als App installieren", "InstallDIMBanner": "Installiere DIM als App auf dem Startbildschirm", "Inventory": "Inventar", "IosPwaPrompt": "Klicke im mobilen Safari auf das Teilen-Symbol (mittlere Taste unten) und wähle \"Zu Home Screen hinzufügen\".", "KeyboardShortcuts": "Tastaturkürzel", "LaunchDIMAlone": "Separates Fenster", "MaterialCounts": "Materialanzahl", "Menu": "Menü", "ProfileAge": "Die Destiny-Server haben ihre Daten zuletzt vor {{age}} aktualisiert.\nWenn du DIM aktualisierst, könntest du neuere Daten erhalten, aber Bungie.net könnte auch veraltete Informationen zurückgeben.", "Refresh": "Aktualisiere Destiny Daten [R]", "ReloadApp": "App neu laden", "ReportBug": "Fehler melden", "SaveSearch": "Suche speichern", "SearchActions": "Öffne Suchaktionen", "SearchResults": "Zeige Gegenstände", "Shop": "Shop", "TagAs": "Markiere als '{{tag}}'", "UpgradeDIM": "DIM aktualisieren", "WhatsNew": "Was ist neu" }, "Help": { "CannotMove": "Gegenstand kann nicht von diesem Charakter wegbewegt werden.", "NoStorage": "DIM kann die Daten nicht speichern", "NoStorageMessage": "DIM kann keine Daten in deinem Browser speichern. Das kann durch Surfen im Privatem- oder Inkognito-Modus, oder zu wenig freiem Speicher verursacht werden. Versuche deinen Computer neu zu starten! Du wirst dich weder anmelden, noch DIM nutzen können." }, "Hotkey": { "Armory": "Zeige Arsenal für einen Gegenstand", "CheatSheetTitle": "Tastaturkürzel:", "ClearDialog": "Dialog schließen", "ClearNewItems": "Markierung von neuen Gegenständen entfernen", "Enter": "ENTER", "ItemPopupTab": "Tab in Gegenstand-Detailansicht wechseln", "LockUnlock": "Sperre oder entsperre einen Gegenstand", "MarkItemAs": "Markiere Item als '{{tag}}'", "Menu": "Menü umschalten", "Note": "Notizen eingeben", "Pull": "Ziehe Gegenstand zum aktiven Charakter", "RefreshInventory": "Aktualisiere Inventar", "ShowHotkeys": "Tastaturkürzel anzeigen", "StartSearch": "Starte eine Suche", "StartSearchClear": "Starte eine neue Suche", "Tab": "TAB", "Vault": "Sende Gegenstand zum Tresor" }, "InGameLoadout": { "ClearSlot": "Slot {{index}} löschen", "Create": "Loadout erstellen", "CreateTitle": "Erstelle In-Game-Loadout von aktueller Ausrüstung", "CurrentlyEquipped": "Zurzeit ausgerüstet", "DeleteFailed": "Fehler beim Löschen des Loadouts", "Deleted": "Loadout gelöscht", "DeletedBody": "In-Game-Loadout in Slot {{index}} gelöscht", "EditFailed": "Fehler beim Aktualisieren des Loadouts", "EditIdentifiers": "Kennzeichnung bearbeiten", "EditTitle": "Bearbeite Loadout-Namen und -Symbol", "EquipNotReady": "In-Game nicht zum Ausrüsten bereit", "EquipReady": "In-Game zum Ausrüsten bereit", "LoadoutDetails": "Loadout-Details", "MatchingLoadouts": "Passende Loadouts:", "PrepareEquip": "Ausrüsten vorbereiten", "Replace": "Ersetze Loadout {{index}}", "Save": "Aktualisiere Loadout", "SaveIdentifiers": "Kennzeichnung aktualisieren", "SnapshotFailed": "Fehler beim Speichern des ausgerüsteten Loadouts" }, "Infusion": { "Filter": "Gegenstände filtern", "InfuseSource": "Gegenstand auswählen um {{name}} in ihn zu infundieren", "InfuseTarget": "Gegenstand auswählen um ihn in {{name}} zu infundieren", "InfusionMaterials": "Infusionsmaterialien", "NoItems": "Keine infundierbaren Gegenstände verfügbar.", "NoTransfer": "Transfer des Infundiermaterials\n{{target}} kann nicht verschoben werden.", "SwitchDirection": "Umschalten", "TransferItems": "Übertragen" }, "Inventory": { "ClickToExpand": "(Klicken zum Erweitern)", "MissingSilver": "Dein Silber-Guthaben ist nur verfügbar, wenn du das Spiel spielst." }, "Item": { "SetBonus": { "NPiece_one": "{{count}} Stück", "NPiece_other": "{{count}} Stück" }, "ThumbsDown": "Daumen runter", "ThumbsUp": "Daumen Hoch" }, "ItemFeed": { "ClearFeed": "Feed löschen", "Description": "Gegenstands-Feed", "HideTagged": "Markierte ausblenden", "NoNewItems": "Keine neuen Gegenstände", "ShowOlderItems": "Zeige ältere Gegenstände" }, "ItemMove": { "Consolidate": "{{name}} zusammengeführt", "Distributed": "{{name}} verteilt\n{{name}} ist gleichermaßen zwischen Charakteren verteilt.", "MovingItem": "In Tresor übertragen", "MovingItem_female": "Übertrage auf {{target}}", "MovingItem_male": "Übertrage auf {{target}}", "ToStore": "Alle {{name}} sind nun bei deinem {{store}}.", "ToVault": "Alle {{name}} sind nun in deinem Tresor." }, "ItemPicker": { "ChooseItem": "Gegenstand auswählen:", "SearchPlaceholder": "Suche Gegenstände" }, "ItemService": { "BucketFull": { "Guardian": "Es sind zu viele '{{itemtype}}'-Items im Inventar vom {{store}}.", "Guardian_female": "Es sind zu viele '{{itemtype}}'-Items im Inventar vom {{store}}.", "Guardian_male": "Es sind zu viele '{{itemtype}}'-Items im Inventar vom {{store}}.", "Vault": "Es sind zu viele '{{itemtype}}'-Items im {{store}}." }, "Classified": "Dieser Gegenstand ist geheim und kann zur Zeit nicht übertragen werden.", "Classified2": "Klassifizierter Gegenstand. Über diesen Gegenstand werden aktuell noch keine Informationen durch Bungie bereitgestellt. Füge dem Gegenstand Notizen hinzu und nutze den \"notes:\"-Suchfilter, um ihn zu finden.", "Deequip": "Kann keinen weiteren ausrüstbaren Gegenstand finden, um {{itemname}} abzulegen", "ExoticError": "'{{itemname}}' kann nicht ausgerüstet werden, da der exotische Gegenstand im {{slot}}-Slot nicht abgelegt werden kann. ({{error}})", "NotEnoughRoom": "Es gibt nichts, was wir vom {{store}} wegbewegen könnten, um Platz für {{itemname}} zu schaffen", "NotEnoughRoomGeneral": "Es ist nicht genug Platz vorhanden, um diesen Gegenstand zu verschieben.", "OnlyEquippedClassLevel": "Das kann nur von einem {{class}} ab einem Level von {{level}} oder drüber ausgerüstet werden.", "OnlyEquippedLevel": "Das kann nur von Charakteren ab einem Level von {{level}} oder drüber ausgerüstet werden.", "PostmasterAlmostFull": "Fast voll!", "PostmasterFull": "Voll!", "PreviewVendor": "{{type}}-Inhaltsvorschau", "StackFull": "Du hast bereits einen kompletten Stapel von {{name}}", "StoreName": "{{genderRace}} {{className}}" }, "KillType": { "ClassAbilities": "Klassenfähigkeit", "Finisher": "Finisher", "Grenade": "Granate", "Melee": "Nahkampf", "Precision": "Präzision", "Super": "Super" }, "LB": { "AddStack": "Eine weitere Kopie dieses Mods hinzufügen", "AdvancedOptions": "Erweiterte Optionen", "ChooseAMod": "Wähle deine Mods", "ChooseASetBonus": "Wähle deine Set-Boni", "ChooseAnExotic": "Wähle deine exotische Rüstung", "ClearLocked": "Gesperrte entsperren", "ContainsVendorItems": "Diese Ausrüstung enthält Gegenstände von Händlern", "Current": "Aktuelles", "Equip": "Anlegen an {{character}}", "Exclude": "Ignorierte Gegenstände", "ExcludeHelp": "Benutze Shift + Klick bei einem Gegenstand (oder ziehe ihn in dieses Feld), um Sets ohne diesen Gegenstand zu generieren.", "ExistingBuildStats": "Existierende Build-Stats", "ExistingBuildStatsNote": "Nur Builds mit höheren Stats werden anzeigt.", "FilterSets": "Sets filtern", "Help": { "And": "Rüstung mit all diesen Perks wird benutzt (\"und\")", "ChangeNodes": "Verändere Intellekt, Disziplin oder Stärke im Spiel auf die Werte, die beim Erstellen des Loadouts angezeigt werden.", "Discipline": "Disziplin beschleunigt die Granaten-Aufladezeit", "DragAndDrop": "Klicke und ziehe Gegenstände in die Felder, um Sets aus nur dieser Ausrüstung zu erstellen", "Help": "Brauchst du Hilfe?", "HigherTiers": "Höhere Tier-Stufen sind besser", "Intellect": "Intellekt beschleunigt die Super-Aufladezeit", "Lock": "Sperre eine bestimmte Art von Perk, indem du auf sein Feld klickst und den Perk auswählst", "MultiPerk": "Um Rüstung mit mehreren Perks zu nutzen, klicke mit Shift+Klick auf die gewünschten Perks", "NoPerk": "Wenn ein Perk nicht auftaucht, besitzt du keinen Gegenstand mit diesem Perk", "Or": "Rüstung mit irgendeinem dieser Perks wird benutzt (\"oder\")", "ShiftClick": "Shift+Klick auf einen Gegenstand, um Sets ohne diese Ausrüstung zu erstellen", "StatsIncrease": "Wenn sich der Verteidigungswert für ein Rüstungsteil erhöht, nehmen auch die Werte für dieses Rüstungsteil (Int/Dis/Stä) zu.", "Strength": "Stärke beschleunigt die Nahkampf-Aufladezeit", "Synergy": "Versuche Rüstung zu finden, welche dir mehr Munition für Waffenklassen geben, die du benutzt.", "Tier11Example": "4/5/2 (ein Tier-Stufe 11 Set) ist 4 Intellekt, 5 Disziplin, 2 Stärke (4+5+2 = Tier-Stufe 11)" }, "HideAllConfigs": "Verstecke alle Einstellungen", "HideConfigs": "Verstecke Einstellungen", "IncompatibleWithOptimizer": "Dieser Gegenstand ist nicht mit dem Optimierer kompatibel. Bitte erwirb eine neue Version aus der Sammlung.", "LB": "Loadout-Optimierer", "LightMode": { "HelpCurrent": "Berechnet Ausrüstung mit dem aktuellen Verteidigungswert.", "HelpScaled": "Berechnet Loadouts als ob alle Gegenstände 350 Verteidigung hätten.", "LightMode": "Licht Modus" }, "Loading": "Lade die besten Sets", "LockEquipped": "Ausgerüstetes sperren", "LockPerk": "Perk sperren", "Locked": "Gesperrte Items", "LockedHelp": "Ziehe einen beliebigen Gegenstand in sein Feld, um es in dem Loadout zu verwenden. Mit Shift + Klick kannst du Gegenstände ignorieren.", "Missing2": "Es fehlen seltene, legendäre oder exotische Items, um ein vollständiges Set zu generieren!", "ProcessingMode": { "Fast": "Schnell", "Full": "Vollständig", "HelpFast": "Nur die besten Gegenstände werden einbezogen.", "HelpFull": "Bezieht mehr Gegenstände ein, braucht dafür länger.", "ProcessingMode": "Berechnung" }, "RemoveStack": "Entferne eine Kopie dieses Mods", "Scaled": "Skaliert", "SearchAMod": "Suche Mod-Name oder -Beschreibung", "SearchASetBonus": "", "SearchAnExotic": "Suche Namen oder Beschreibung von Exotischen", "SelectExotic": "Wähle Exotik", "SelectMods": "Mods auswählen", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Aktivitäts-Mods", "SelectSetBonus": "Set-Bonus auswählen", "SelectSubclassOptions": "Fokus anpassen", "ShowAllConfigs": "Zeige alle Einstellungen", "ShowConfigs": "Zeige Einstellungen", "ShowGear": "{{class}}-Rüstung", "Vendor": "Gegenstände von Händlern einbeziehen" }, "Loading": { "Accounts": "Lade Destiny Konten...", "Code": "Lade DIM-Code...", "FilterHelp": "Lade Suchhilfe...", "Profile": "Lade Destiny Profil...", "Vendors": "Lade Destiny Händler..." }, "LoadoutAnalysis": { "Analyzed": "{{numLoadouts}} Loadouts analysiert", "Analyzing": "Analysiere {{numAnalyzed}}/{{numLoadouts}} Loadouts", "BetterStatsAvailable": { "Description": "Die Wahl anderer Rüstungen oder Mods für dieses Loadout erlaubt es höhere Stats zu erreichen. Wähle \"$t(Loadouts.OpenInOptimizer)\", um bessere Builds zu sehen.", "Name": "Bessere Stats verfügbar" }, "BetterStatsAvailableFontNote": "Hinweis: Dieses Loadout benutzt \"Quelle der ...\"-Mods, welche Werte über 200 erzeugen. DIM kann evtl. bessere Stats identifizieren, indem die zu hohen Stats verringert werden. Wenn dies unerwünscht ist, deaktiviere \"$t(Loadouts.IncludeRuntimeStatBenefits)\" im Loadout.", "DoesNotRespectExotic": { "Description": "Die Loadout-Optimierer-Einstellungen dieses Loadouts legen eine Exotic-Auswahl fest, aber das Loadout entspricht nicht diesem Exotic.", "Name": "Falsches Exotic" }, "DoesNotSatisfyStatConstraints": { "Description": "Loadout-Optimierer-Einstellungen für dieses Loadout geben Minimal-Stats vor, aber das Loadout erreicht sie nicht.", "Name": "Falsche Minimal-Stats" }, "EmptyFragmentSlots": { "Description": "Es gibt leere Fragment-Slots in diesem Fokus.", "Name": "Leere Fragment-Slots" }, "InvalidMods": { "Description": "Einige Mods in diesem Loadout sind veraltet oder passen aus anderen Gründen nicht auf deine Rüstungen.", "Name": "Veraltete Mods" }, "InvalidSearchQuery": { "Description": "Dieses Loadout wurde mit einer ungültigen Suchanfrage im Loadout-Optimierer erstellt.", "Name": "Ungültige Suchanfrage" }, "ItemsDoNotMatchSearchQuery": { "Description": "Dieses Loadout wurde mit einer Suchanfrage im Loadout-Optimierer erstellt und diese Suchanfrage schließt mindestens eines der Gegenstände im Loadout aus.", "Name": "Suche schließt Gegenstände aus" }, "MissingItems": { "Description": "Einige der Gegenstände in diesem Loadout befinden sich nicht mehr in deinem Inventar.", "Name": "Fehlende Gegenstände" }, "ModsDontFit": { "Description": "Rüstung in diesem Loadout kann nicht alle Loadout-Mods aufnehmen, auch wenn die Rüstung verbessert wurde.", "Name": "Nicht zugewiesene Mods" }, "NeedsArmorUpgrades": { "Description": "Rüstung in diesem Loadout muss verbessert werden, um alle Mods unterbringen zu können oder bestimmte Stats zu erreichen.", "Name": "Benötigt Rüstungsverbesserungen" }, "NotAFullArmorSet": { "Description": "Dieses Loadout konnte nicht weiter analysiert werden, da das Rüstungsset unvollständig ist.", "Name": "Unvollständiges Rüstungsset" }, "TooManyFragments": { "Description": "Es sind mehr Fragmente für diesen Fokus konfiguriert, als durch Aspekte ermöglicht werden.", "Name": "Zu viele Fragmente" }, "UsesSeasonalMods": { "Description": "Dieses Loadout ist auf Mods angewiesen, die nur in bestimmten Saisons verfügbar sind. Wenn die Saison endet, werden einige Mods nicht mehr sein oder die Kapazität der Rüstung übersteigen.", "Name": "Verwendet saisonale Mods" } }, "LoadoutBuilder": { "All": "Alle", "AlwaysAutoMods": "Kunstvoll- und Tuning-Mods werden immer automatisch gewählt.", "AnyExotic": "Irgendein Exotik", "AnyExoticDescription": "Sets müssen einen exotischen Gegenstand beinhalten, es ist aber egal welchen.", "Artifice": "Kunstvoll", "AssumeMasterwork": "Als Meisterwerk ansehen", "AssumeMasterworkOptions": { "All": "Alle Rüstungen: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "Alle Rüstungen: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nRüstung 2.0 Exotiks: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Verbessert um Kunstvoll-Mod zu akzeptieren.", "Current": "Aktuelle Stats gehen von einer Energiekapazität von mindestens {{minLoItemEnergy}} aus.", "Legendary": "Legendär: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExotisch: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Vollständige Meisterwerk-Stat-Boni, angenommenes Energie-Level mind. 10.", "None": "Alle Rüstungen: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "Stat-Mods automatisch hinzufügen", "AutomaticallyPicked": "Dieser Mod wurde automatisch hinzugefügt, um Build-Stats zu verbessern.", "CompareLoadout": "Vergleiche Loadout", "ConfirmOverwrite": "Bist du sicher, dass du die Gegenstände in deinem Loadout \"{{name}}\" durch dieses neue Rüstungsset ersetzen möchtest?", "DecreaseStatPriority": "Verringere Stat-Priorität", "DisabledByAutoStatMods": "Stat-Mods werden automatisch vom Loadout-Optimierer gewählt.", "DisabledDueToMaintenance": "Der Loadout-Optimierer ist derzeit aufgrund der Bungie API Wartung deaktiviert.", "EquipItems": "Ausrüsten", "ExcludeItem": "Gegenstand ausschließen", "ExcludeVendors": "Suche \"not:vendor\", um Gegenstände von Händlern vom Loadout-Optimierer auszuschließen.", "ExcludedItems": "Ignorierte Gegenstände", "ExistingLoadout": "Bestehendes Loadout", "Exotic": "Exotische Rüstung", "ExoticClassItemPerks": "Wenn du bestimmte Perks möchtest, verwende Suchanfragen wie exactperk:\"seele der wahrheit\". Klicke auf Perks in den Optimierer-Ergebnissen, um diese hinzuzufügen oder aus dem Filter zu entfernen.", "ExoticSpecialCategory": "Spezial", "FOTLWildcardWarning": "Dieses Set enthält ein Festival der verlorenen Maske. Benutze manuell den richtigen Mod um die gewünschten Set-Boni zu aktivieren.", "Filter": "Einstellungen", "IgnoreStat": "Wenn nicht ausgewählt, wird der Loadout-Optimierer diesen Stat beim Zusammenstellen von Sets nicht beachten", "IncreaseStatPriority": "Erhöhe Stat-Priorität", "Legendary": "Legendär", "LimitToNewFeaturedGear": "Auf neue/im Brennpunkt stehende Ausrüstung beschränken", "LockItem": "Gegenstand anpinnen", "MissingClass": "Build ist für: {{className}}", "MissingClassDescription": "Der Build, den du sehen möchtest, ist für eine Charakterklasse gedacht, die du nicht hast.", "MwExotic": "Exotik", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "Eine aktive Suchanfrage beschränkt die Elemente, die DIM in Loadouts einschließen kann", "AllowAutoStatMods": "Erlaube DIM, zusätzliche Statistik-Mods automatisch einzubinden", "AlwaysInvalidMods": "Diese Mods passen auf keine deiner Gegenstände:", "AssumeMasterworked": "Erlaube DIM, Rüstungen als Meisterwerk anzunehmen", "AssumptionsRestricted": "DIM kann keine Änderungen an der Rüstungs-Affinität empfehlen:", "BadSlot": "Im {{bucketName}}-Slot konnte keiner der erlaubten Gegenstände diese Mods aufnehmen:", "ExoticDoesNotExist": "Du hast keine der ausgewählten exotischen Rüstungen in deinem Inventar.", "Header": "Es wurden keine Builds gefunden. Hier sind mögliche Gründe, warum DIM keine Builds finden konnte:", "LowerBoundsFailed": "Viele Sets erfüllten nicht die Mindestanforderungen", "MaybeAllowMoreItems": "Erwäge, andere Gegenstände zu erlauben:", "MaybeDecreaseLowerBounds": "Ziehe in Betracht, die Mindestanforderungen zu reduzieren", "MaybeRemoveMods": "Erwäge, einige Mods zu entfernen:", "MaybeRemoveSearchQuery": "Ziehe in Betracht, Filter in der Suchleiste zu löschen oder zu ändern", "ModAssignmentFailed": "Viele Sets konnten nicht alle ausgewählten Mods unterbringen", "RemoveMods": "Diese Mods entfernen", "RemoveSetBonuses": "Erwäge, einige Set-Boni zu entfernen", "SetBonuses": "Du hast einige Set-Boni ausgewählt, vielleicht hast du nicht die richtigen Gegenstände, um sie zu verwenden." }, "NoExotic": "Kein Exotik", "NoExoticDescription": "Äquivalent zur Suche \"not:exotic\" in der Suchleiste - Sets werden keine exotischen Rüstungen verwenden.", "NoExoticPreference": "Kein Exotik gewählt", "NoExoticPreferenceDescription": "Exotische Rüstung wird verwendet, wenn sie die Stats maximiert.", "NoLoadoutsToCompare": "Keine Loadouts für Vergleich", "None": "Keine", "OptimizerExplanationGuide": "Lies das Benutzerhandbuch für weitere Informationen und ein Video-Tutorial.", "OptimizerExplanationMods": "Wähle ein Exotik, Mods und eine Unterklasse. Diese werden Statistiken zum Loadout beitragen, während alle Mods auf der Rüstung ignoriert werden.", "OptimizerExplanationSearch": "Verwenden Sie die Suchleiste, um die zu berücksichtigende Rüstung einzugrenzen, z.B. {{example}}. Wenn keine Rüstung in einem Slot mit der Suche übereinstimmt, werden alle Gegenstände für diesen Slot berücksichtigt.", "OptimizerExplanationStats": "Ziehe die wichtigsten Stats nach oben und deaktiviere Stats, die du nicht optimieren möchtest.", "OptimizerSet": "Optimierer-Set", "PinnedItems": "Angepinnte Gegenstände", "PinnedItemsFinePrint": "Suchfilter werden mit Loadout-Optimierer-Einstellungen gespeichert, aber angepinnte und ignorierte Gegenstände nicht. Angepinnte und ignorierte Gegenstände werden nicht berücksichtigt, wenn DIM bestehende Loadouts auf bessere Stat-Builds überprüft.", "ProcessingSets": "Suche nach den höchsten Sets...", "SaveAs": "Speichern unter", "SetBonus": "Set-Boni", "SpeedReport": "Evaluiert {{combos, number}} Kombinationen in {{time}} Sekunden mit {{cpus}} CPU Kernen.", "StatConstraints": "Stat-Prioritäten und Bereiche", "StatMax": "Max", "StatMin": "Min", "StatRangeTooltip": "Mit der aktuellen Min/Max-Einstellung sind Loadouts vorhanden, die {{min}} zu {{max}} Punkte in diesem Stat haben. Doppelklicken, um Min auf {{max}} zu setzen.", "StatTotal": "Gesamt: {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "Nicht in der Lage, alle Mods hinzuzufügen.", "UnableToAddAllModsBody": "Es waren nicht genügend Mod-Slots verfügbar für {{mods}}.", "UnlockItem": "Abpinnen" }, "LoadoutFilter": { "Contains": "Zeigt Loadouts an, die einen Gegenstand oder ein Mod haben, das dem Filtertext entspricht. Für eine Suche mit Leerzeichen in ihrem Namen, benutze Anführungszeichen.", "FashionOnly": "Zeigt Loadouts an, die nur Mode (Shader oder Ornamente) enthalten.", "LoadoutLight": "Zeigt Loadouts basierend auf ihrem berechneten Licht-Level. Verwende das Schlüsselwort Pinnaclecap oder Softcap statt einer Zahl, um auf die Power-Beschränkungen der aktuellen Saison zu verweisen.", "ModsOnly": "Zeigt Loadouts, die nur Rüstungsmods enthalten.", "Name": "Zeigt Gegenstände, deren Name genau (exaktname:), oder teilweise (name:), mit dem Filtertext übereinstimmen. Suche nach ganzen Phrasen mit Anführungszeichen.", "Notes": "Suche nach Loadouts nach ihrem Notizfeld.", "PartialMatch": "Zeigt Loadouts an, bei denen der Name oder die Notizen teilweise mit dem Filtertext übereinstimmen. Suche ganze Phrasen mit Anführungszeichen.", "Season": "Zeigt Loadouts in welcher Saison von Destiny 2 sie zuletzt geändert wurden.", "Subclass": "Zeigt Loadouts an, deren Unterklasse oder Schadensart teilweise mit dem Filtertext übereinstimmt." }, "Loadouts": { "Abilities": "Fähigkeiten", "Actions": "Aktionen für {{title}}", "AddEquippedItems": "Ausgerüstetes hinzufügen", "AddNotes": "Notizen hinzufügen", "AddUnequippedItems": "Unausgerüstet hinzufügen", "Any": "Jede Klasse", "Apply": "Anwenden", "ApplyInGameLoadoutInGame": "Dein Loadout ist bereit, aber da du dich in einer Aktivität befindest, musst du es im Spiel ausrüsten.", "ApplyMods": "Wende Mods an", "ApplySearch": "Übertrage Suche \"{{query}}\"", "ArmorStats": "Rüstungs-Stats", "ArtifactUnlocks": "Artefakt-Freischaltungen", "ArtifactUnlocksDesc": "Aufgrund von Bungie.net-Einschränkungen kann DIM dein Artefakt nicht automatisch konfigurieren. Du musst diese Freischaltungen im Spiel durchführen, bevor du das Loadout anwenden kannst.", "ArtifactUnlocksWithSeason": "Artefakt-Freischaltungen – S{{seasonNumber}}", "BadLoadoutShare": "Konnte geteiltes Loadout nicht laden", "BadLoadoutShareBody": "Das Loadout, das du zu laden versuchst, ist ungültig: {{error}}", "Before": "Bevor '{{name}}'", "CancelEditing": "Bearbeitung abbrechen", "CannotCustomizeSubclass": "Dieser Fokus kann nicht konfiguriert werden", "ChooseItem": "{{name}} hinzufügen", "ClassType": "Klassenunabhängiges Loadout", "ClassTypeMismatch": "Ein {{className}}-Gegenstand kann diesem Loadout nicht hinzugefügt werden", "ClassTypeMissing": "Du besitzt keinen {{className}}, für den du ein Loadout erstellen könntest", "ClassType_female": "{{className}} Loadout", "ClassType_male": "{{className}} Loadout", "Classified": "Einige deiner Gegenstände sind klassifiziert und können nicht in die Max-Power-Berechnung einbezogen werden.", "ClearLoadoutParameters": "Loadout-Optimierer-Einstellungen entfernen", "ClearSection": "Alle entfernen", "ClearSpace": "Anderes wegräumen", "ClearSpaceArmor": "Andere Rüstung wegräumen", "ClearSpaceWeapons": "Andere Waffen wegräumen", "ClearUnsetMods": "Andere Mods entfernen", "ClearingSpace": "Verschiebe andere Gegenstände", "CopyAndEdit": "Kopie bearbeiten", "Create": "Loadout erstellen", "CurrentlyEquipped": "Zurzeit ausgerüstet", "Deequip": "Lege Gegenstände von anderen Charakteren ab", "Delete": "Löschen", "DimLoadouts": "DIM-Loadouts", "Edit": "Ausrüstung bearbeiten", "EditBrief": "Ändern", "EquipInGameLoadout": "Rüste In-Game-Loadout aus", "EquipItems": "Rüste Gegenstände aus", "EquippableDifferent1": "Mehrere exotische Gegenstände wurden zur Berechnung deines maximalen Powerlevels verwendet, sodass die angezeigte Zahl beim Ausrüsten deiner Gegenstände im Spiel möglicherweise nicht erreichbar ist.", "EquippableDifferent2": "Das maximale Powerlevel ist nicht durch die Regel \"Ein exotischer Gegenstand\" begrenzt, wenn die Höhe deiner Drops, mächtigen und Spitzen-Belohnungen berechnet wird.", "Failed": "Loadout konnte nicht komplett angewendet werden", "Fashion": "Mode wählen", "FashionOnly": "Nur Mode", "FillFromEquipped": "Mit Ausgerüstetem auffüllen", "FillFromInventory": "Mit Nicht-Ausgerüstetem auffüllen", "FilteredItems": "Gefilterte Gegenstände", "FindAnother": "Finde andere(s) {{name}}", "FromEquipped": "Ausgerüstet", "Generated": "{{statTotal}} Stat-Level-Loadout", "HashtagTip": "Tipp: Du kannst #Hashtags in deinen Loadout-Namen oder -Notizen verwenden, um sie hier angezeigt zu bekommen.", "Import": { "BadURL": "Keine gültige Loadout-Teilen-URL.", "Error": "Fehler beim Laden des Loadouts:", "Error404": "Dieses Loadout existiert nicht.", "PasteHere": "Füge einen Loadout-Link ein, um die Ausrüstung zu öffnen." }, "ImportLoadout": "Importiere Loadout", "InGameActions": "Loadout-Aktionen im Spiel", "InGameLoadouts": "In-Game-Loadouts", "IncludeRuntimeStatBenefits": "\"Quelle der\"-Mod-Stats einbeziehen", "IncludeRuntimeStatBenefitsDesc": "\"Quelle der ...\"-Rüstungsmods erhöhen die Charakter-Stats während du Rüstungsladungen hast.\n\nMit dieser Einstellung bezieht DIM diese Mods aktiv ein und fügt deren Nutzen in Berechnungen und Optimierungen zu den Stats dieses Loadouts hinzu.", "ItemErrorSummary_one": "1 Gegenstands-Fehler:", "ItemErrorSummary_other": "{{count}} Gegenstands-Fehler:", "ItemLeveling": "Item Leveln", "LoadoutName": "Loadout-Name", "LoadoutParameters": "Loadout-Optimierer-Einstellungen", "LoadoutParametersExotic": "Loadout muss dieses Exotische beinhalten: {{exoticName}}", "LoadoutParametersQuery": "Gegenstände müssen mit diesem Suchfilter übereinstimmen", "LoadoutParametersStats": "Stat-Prioritäten und minimale/maximale Stat-Bereiche", "Loadouts": "Loadouts", "MakeRoom": "Schaffe Platz für Poststelle", "MakeRoomDone_female_one": "Platz für 1 Poststellen-Gegenstand durch Verschieben von 1 Gegenstand von {{store}} geschaffen.", "MakeRoomDone_female_other": "Platz für {{count}} Poststellen-Gegenstände durch Verschieben von {{movedNum}} Gegenständen vom {{store}} geschaffen.", "MakeRoomDone_male_one": "Platz für 1 Poststellen-Gegenstand durch Verschieben von 1 Gegenstand von {{store}} geschaffen.", "MakeRoomDone_male_other": "Platz für {{count}} Poststellen-Gegenstände durch Verschieben von {{movedNum}} Gegenständen vom {{store}} geschaffen.", "MakeRoomDone_one": "Platz für 1 Poststellen-Gegenstand durch Verschieben von 1 Gegenstand von {{store}} geschaffen.", "MakeRoomDone_other": "Platz für {{count}} Poststellen-Gegenstände durch Verschieben von {{movedNum}} Gegenständen vom {{store}} geschaffen.", "MakeRoomError": "Es kann kein Platz für alle Poststellen-Gegenstände geschaffen werden: {{error}}.", "ManageLoadouts": "Loadouts verwalten", "MaxSlots": "Du kannst nur {{slots}} {{bucketName}} in einem Loadout haben.", "MaximizeLight": "Max Licht", "MaximizePower": "Max Power", "MaximizeStat": "Maximiere Stat", "MissingItemsWarning": "Einige der Gegenstände in diesem Loadout befinden sich nicht mehr in deinem Inventar.", "ModErrorSummary_one": "1 Mod-Fehler:", "ModErrorSummary_other": "{{count}} Mod-Fehler:", "ModPlacement": { "InvalidMods": "Ungültige Mods", "InvalidModsDesc_one": "1 Mod passt in keines der Rüstungsteile.", "InvalidModsDesc_other": "{{count}} Mods passen in keines der Rüstungsteile.", "ModPlacement": "Mod-Platzierung", "StackableMod": "Stapelbar", "UnassignedMods": "Nicht zugewiesene Mods", "UnassignedModsDesc_one": "1 Mod passte nicht aufgrund unzureichender Energiekapazität oder Mod-Slots. Energieverbesserungen der ausgewählten Rüstung werden das Problem nicht beheben.", "UnassignedModsDesc_other": "{{count}} Mods passen nicht aufgrund unzureichender Energiekapazität oder Mod-Slots. Energieverbesserungen der ausgewählten Rüstung werden das Problem nicht beheben.", "UnstackableMod": "Nicht stapelbar", "UpgradeCosts": "Verbesserungskosten", "UpgradeCostsDesc": "Einige Rüstungen benötigen Energiekapazitätsverbesserungen, um die gewählten Mods verwenden zu können. Insgesamt kosten diese Verbesserungen:" }, "Mods": "Mods", "ModsOnly": "Nur Mods", "MoveItems": "Verschiebe Gegenstände", "NoSpace": "Du hast keinen Platz im Tresor und allen anderen Charakteren.", "NoneMatch": "Keiner deiner Ausrüstungen entsprach den Filtern.", "NotStarted": "Warte auf den Abschluss anderer Aktionen oder auf die Aktualisierung des Inventars", "NotesPlaceholder": "Schreibe ein paar Notizen über diese Ausrüstung – oder benutze #Hashtags, um sie in Kategorien einzuordnen", "NotificationTitle": "Ausrüstung: {{name}}", "OnWrongCharacterAdvice": "Klicke hier, um die Gegenstände dieses Charakters mit der höchsten Power zu finden.", "OnWrongCharacterWarning": "Die mächtigste Rüstung dieses Charakters ist auf einem anderen Charakter. Um zur Power für Drops und für mächtige und Spitzen-Belohnungen zu zählen, muss die Rüstung auf diesem Charakter oder im Tresor sein.", "OnlyItems": "Es können nur ausrüstbare Gegenstände, Materialien und Verbrauchs-Gegenstände zu einer Ausrüstung hinzugefügt werden.", "OpenInOptimizer": "Rüstung optimieren", "OpenOnStreamDeck": "Auf Stream-Deck öffnen", "PickArmor": "Wähle Rüstung", "PickMods": "Rüstungsmods hinzufügen", "Prismatic": { "Aspect": "Prisma Aspekt", "Grenade": "Prisma Granate", "Melee": "Prisma Nahkampf", "Super": "Super-Fähigkeit" }, "PullFromPostmaster": "Poststelle einsammeln", "PullFromPostmasterError": "Abrufen von Poststelle nicht möglich: {{error}}.", "PullFromPostmasterGeneralError": "Nicht in der Lage, alle Gegenstände aus der Poststelle zu holen.", "PullFromPostmasterNotification_female_one": "Übertrage 1 Poststellen-Gegenstand auf {{store}}.", "PullFromPostmasterNotification_female_other": "Übertrage {{count}} Poststellen-Gegenstände auf {{store}}.", "PullFromPostmasterNotification_male_one": "Übertrage 1 Poststellen-Gegenstand auf {{store}}.", "PullFromPostmasterNotification_male_other": "Übertrage {{count}} Poststellen-Gegenstände auf {{store}}.", "PullFromPostmasterNotification_one": "Übertrage 1 Poststellen-Gegenstand auf {{store}}.", "PullFromPostmasterNotification_other": "Übertrage {{count}} Poststellen-Gegenstände auf {{store}}.", "PullFromPostmasterPopupTitle": "Abrufen von Poststelle", "Random": "Zufällig", "Randomize": "Zufällige Ausrüstung", "RandomizeButton": "Zufällig generieren", "RandomizeNew": "Zufällig erstellen", "RandomizeQueryHint": "Tipp: Benutze zuerst die Suche, um einzuschränken, zwischen welchen Gegenständen zufällig ausgewählt wird.", "RandomizeSearch": "Zufällig von Suche", "RandomizeSearchPrompt": "Zufällige Gegenstände von der Suche \"{{query}}\" ausrüsten?", "Redo": "Wiederholen", "RestoreAllItems": "Alle Items", "SalvationsEdgeMods": "\"Rand der Erlösung\"-Mods", "Save": "Speichern", "SaveAsDIM": "Als DIM-Loadout speichern", "SaveAsNew": "Als Neu speichern", "SaveAsNewTooltip": "Die ursprüngliche Ausrüstung behalten und dies als neue Ausrüstung speichern", "SaveDisabled": { "AlreadyExists": "Wähle einen neuen Namen für die Ausrüstung.", "Empty": "Die Ausrüstung ist leer.", "NoName": "Das Loadout benötigt einen Namen." }, "SaveLoadout": "Loadout speichern", "Season": "Saison {{season}}", "SetBonusesDesc": "Benötigte Set-Boni", "Share": { "Copied": "Loadout-Link in die Zwischenablage kopiert", "CopyButton": "Link kopieren", "Error": "Fehler beim Abrufen des Freigabelinks", "Fashion": "Mode (Shader & Ornamente)", "LoadoutOptimizer": "Loadout-Optimierer-Einstellungen", "NativeShare": "Link teilen", "Notes": "Notizen", "NumItems_one": "{{count}} Gegenstand – Empfänger werden aufgefordert, einen vergleichbaren Gegenstand aus ihrem Inventar auszuwählen", "NumItems_other": "{{count}} Gegenstände – Empfänger werden aufgefordert, einen vergleichbaren Gegenstand aus ihrem Inventar auszuwählen", "NumMods_one": "{{count}} Mod", "NumMods_other": "{{count}} Mods", "Placeholder": "Teilen-Link wird geladen\n", "Subclass": "Fokus-Anpassung", "Summary": "Teile dieses Loadout mit:", "Title": "Teile \"{{name}}\"" }, "ShareLoadout": "Teilen", "ShowModPlacement": "Mod-Platzierung anzeigen", "Snapshot": "Als In-Game-Loadout speichern", "SocketOverrides": "Ändere Fokus-Optionen", "SortByEditTime": "Nach Änderungsdatum sortieren", "SortByName": "Nach Namen sortieren", "SubclassOptions": "{{subclass}}-Optionen", "SubclassOptionsSearch": "{{subclass}}-Optionen suchen", "Succeeded": "Loadout angewendet", "SyncFromEquipped": "Von Ausgerüstetem synchronisieren", "TooManyRequested": "Du hast {{total}} {{itemname}}, aber dein Loadout verlangt jedoch {{requested}}. Wir haben alles übertragen, was da war.", "TuningMods": "Tuning-Mods", "UnassignedModError": "Mod passt nicht auf deine aktuelle Rüstung", "Undo": "Rückgängig", "Update": "Änderungen speichern", "UpdateLoadout": "Aktualisiere Loadout", "VendorsCannotEquip": "Du hast diesen Gegenstand nicht. Tippe, um einen Ersatz zu wählen oder klicke auf X zum Entfernen:" }, "Manifest": { "Download": "Lade die neueste Destiny Info-Datenbank von Bungie herunter...", "Error": "Fehler beim Laden der Destiny Info-Datenbank:\n{{error}}\nZum erneuten Versuch erneut laden.", "Load": "Lade Destiny Info-Datenbank..." }, "Milestone": { "Daily": "Tägliche Herausforderung", "OneTime": "Einmalige Herausforderung", "SeasonalRank": "Saisonrang {{rank}}", "Special": "Besondere Event-Herausforderung", "Tutorial": "Tutorial-Herausforderung", "Unknown": "Herausforderung", "Weekly": "Wöchentliche Herausforderung" }, "Mods": { "HarmonicModDescription": "Der Effekt dieses Mods hat einen reduzierten Preis und ändert sein Element abhängig vom ausgerüsteten Fokus." }, "MoveAmount": { "Amount": "Menge:" }, "MovePopup": { "Acquired": "Dieser Gegenstand ist in der Sammlung freigeschaltet.", "AcquiredMod": "Dieser Mod ist in der Sammlung freigeschaltet.", "AddNote": "Notizen hinzufügen", "AddToLoadout": "Loadout", "AddToLoadoutTitle": "Dies zu einem Loadout hinzufügen", "All": "Alle", "ArtifactBreaker": "Diese Waffe hat {{breaker}} wegen eines freigeschalteten Artefaktperks.", "CannotCurrentlyRoll": "Dieser Perk kann nicht auf die aktuelle Version dieses Gegenstands gerollt werden.", "CantPullFromPostmaster": "Du musst die Poststelle im Spiel besuchen, um diesen Gegenstand abzurufen.", "CatalystProgress": "Katalysator-Fortschritt", "CommunityData": "Community-Einblick", "Consolidate": "Zusammenführen", "DistributeEvenly": "Gleichmäßig verteilen", "EnhancementTier": "Tier {{tier}}", "Equip": "Ausrüsten auf:", "EquipWithName": "Anlegen an {{character}}", "FavoriteUnFavorite": { "Favorite": "{{itemType}} zu Favoriten hinzufügen", "Favorited": "Favorit", "Unfavorite": "{{itemType}} aus Favoriten entfernen", "Unfavorited": "Kein Favorit" }, "Infuse": "Infundieren", "InfuseTitle": "Infundier-Suche öffnen", "IntrinsicBreaker": "Diese Waffe hat intrinsisch {{breaker}}.", "LoadingSockets": "Details zu Perk und Statistik wurden für dieses Element noch nicht geladen.", "LockUnlock": { "AutoLock": "Sperrstatus ist mit der Markierung dieses Gegenstands synchronisiert", "Lock": "{{itemType}} sperren", "Locked": "Gesperrt", "Unlock": "{{itemType}} entsperren", "Unlocked": "Entsperrt" }, "MissingSockets": "Perk- und Mod-Details sind nicht verfügbar, während Bungie ihre Dienste aktualisiert. Sie kommen zurück, sobald die Aktualisierung beendet wurde, in der Regel in ein paar Stunden.", "Notes": "Notizen:", "OpenOnStreamDeck": "Auf Stream-Deck öffnen", "OverviewTab": "Übersicht", "Owned": "Dieser Gegenstand ist in deinem Inventar.", "OwnedMod": "Diese Mod ist in deinem Modifikations-Inventar.", "PullItem": "Ziehe {{bucket}} auf {{store}}", "PullPostmaster": "Abrufen von Poststelle", "ReadLore": "Lese Überlieferungen und Hintergründe auf Ishtar Collective", "ReadLoreLink": "Lese Lore", "Rewards": "Belohnungen:", "SendToVault": "An Tresor senden", "Store": "Lagern auf:", "StoreWithName": "Auf {{character}} lagern", "Subtitle": { "QuestProgress": "Schritt {{questStepNum}} von {{questStepsTotal}}", "Type": "{{classType}} {{typeName}}" }, "TabList": "Tabs in Gegenstand-Detailansicht", "ToggleSidecar": "Gegenstand-Aktionen erweitern oder einklappen", "TrackUntrack": { "Track": "{{itemType}} verfolgen", "Tracked": "Verfolgt", "Untrack": "{{itemType}} nicht verfolgen", "Untracked": "Unverfolgt" }, "TriageTab": "Vorselektieren", "UnreliablePerkOption": "Dieser Perk erscheint nur in der Sammlungsansicht. Möglicherweise wird er nicht zufällig auf dieses Element rollen.", "Vault": "Tresor", "WeaponLevel": "Waffen-Level {{level}}" }, "Notes": { "Error": "Fehler! Max 120 Zeichen für Notizen.", "Help": "Notizen, #Hashtags und :Symbole: hinzufügen" }, "Notification": { "Cancel": "Abbrechen", "OK": "Schließen" }, "Objectives": { "Complete": "Vollständig", "Incomplete": "Unvollständig" }, "Organizer": { "BulkMove": "Verschieben zu", "BulkMoveLoadoutName": "Im Organizer ausgewählt", "BulkTag": "Markierung", "Columns": { "Ammo": "Munition", "Archetype": "Archetyp", "BaseStats": "Basis-Stats", "Breaker": "Brecher", "Crafted": "Formdatum", "CustomTotal": "Ben.-def. Summe", "Damage": "Schaden", "Energy": "Energie", "Event": "Event", "Featured": "Neue Ausrüstung", "Foundry": "Schmiede", "Frame": "Gehäuse", "Harmonizable": "Harmonisierbar", "Holofoil": "Holofoil", "Icon": "Icon", "ItemTier": "Tier", "KillTracker": "Eleminierungen", "Level": "Level", "Loadouts": "Loadouts", "Location": "Ort", "Locked": "Gesperrt", "MasterworkStat": "MW Stat", "MasterworkTier": "MW Tier", "ModSlot": "Mod-Slot", "Mods": "Mods", "Name": "Name", "New": "Neu", "Notes": "Notizen", "OriginTraits": "Ursprungsmerkmal", "OtherPerks": "Waffenkomponenten", "PercentComplete": "% Abgeschlossen", "Perks": "Perks", "PerksGrid": "Perks Raster", "Power": "Power", "Quality": "Qualität %", "Recency": "Neuheit", "Season": "Saison", "Shaders": "Kosmetik", "Source": "Quelle", "StatQuality": "Stat-Qualität", "StatQualityStat": "{{stat}}%", "Stats": "Stats", "Tag": "Markierung", "TertiaryStat": "3. Stat", "Tier": "Seltenheit", "Traits": "Waffeneigenschaften", "TuningStat": "Tuner", "WishList": "Wunschliste", "WishListNotes": "Wunschlisten-Notizen", "Year": "Jahr" }, "EnabledColumns": "Aktivierte Spalten", "Lock": "Sperren", "NoItems": "Keine Gegenstände stimmen mit den Filtern überein. Wenn du eine Suchanfrage hast, versuche sie zu löschen.", "NoMobile": "Drehe dein Telefon seitlich, um den Organizer zu verwenden.", "Note": "Notizen anpassen", "OpenIn": "Im Organizer anzeigen", "Organizer": "Organizer", "SelectAll": "Alle auswählen", "SelectItem": "{{name}} auswählen oder abwählen", "ShiftTip": "Tipp: Halte die Umschalt-Taste gedrückt und klicke auf eine Zelle, um Objekte zu filtern", "Stats": { "Aim": "Zielen", "Airborne": "Luft-Effektivität", "AmmoGeneration": "Muni.-Gen.", "Power": "Power", "RPM": "U/min", "Recoil": "Rückstoß", "Reload": "Nachladen" }, "Unlock": "Entsperren" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "Die Poststelle ist fast voll! ({{number}}/{{postmasterSize}})", "PostmasterFull": "Die Poststelle ist voll! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "Beutezüge", "CatalystSource": "Quelle: {{source}}", "CrucibleRank": "Ränge", "Items": "Quest-Gegenstände", "Milestones": "Meilensteine & Herausforderungen", "NoEventChallenges": "Du hast alle Event-Herausforderungen abgeschlossen", "NoTrackedTriumph": "Du verfolgst keine Triumphe. Verfolge so viele wie du möchtest in DIM.", "PaleHeartPathfinder": "Bleiches Herz Pfadfinder", "PercentMax": "{{pct}}% des Maximums", "PercentPrestige": "{{pct}}% des max. Rangs", "PointsUsed_one": "1 Punkt verwendet", "PointsUsed_other": "{{count}} Punkte verwendet", "PowerBonusHeader": "+{{powerBonus}} Power-Belohnungen", "PowerBonusHeaderUndefined": "Andere Belohnungen", "Progress": "Fortschritt", "QueryFilteredTrackedTriumphs": "Keiner deiner verfolgten Triumphe entsprach der Suche", "QuestExpired": "Abgelaufen", "QuestExpires": "Läuft ab in ", "Quests": "Quests", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}Pkte", "Resets_one": "1 Zurücksetzung", "Resets_other": "{{count}} Zurücksetzungen", "RewardPassEndsIn": "Prämienpass endet in ", "RewardPassPrestigeRank": "Prestige-Rang {{rank}}", "SeasonalHub": "Saisonaler Hub", "StatTrackers": "Stat Tracker", "TrackedTriumphs": "Verfolgte Triumphe" }, "RecordBooks": { "HideCompleted": "Abgeschlossene Urkunden nicht anzeigen", "RecordBooks": "Urkundenbücher" }, "Records": { "Title": "Sammlungen", "UniversalOrnamentSetOther": "Andere" }, "SearchHistory": { "Date": "Zuletzt verwendet", "DeleteAll": "Alle nicht markierten Suchen löschen", "Description": "Dies sind alle deine vergangenen und gespeicherten Suchen. Du kannst sie von hier aus löschen.", "Item": "Gegenstands-Suchen", "Link": "Suchverlauf anzeigen und bearbeiten", "Loadout": "Loadout-Suchen", "Query": "Suche", "Title": "Suchverlauf", "UsageCount": "# Verwendet" }, "Settings": { "Appearance": "Darstellung", "ArmorArchetypeModslot": "Rüstung Archetyp / Mod-Slot", "AutoLockTagged": "Sperrstatus mit Markierung synchronisieren", "AutoLockTaggedExplanation": "DIM wird Gegenstände automatisch je nach Markierung sperren oder entsperren. Hergestellte Gegenstände bleiben freigeschaltet, um eine Umformung zu ermöglichen. Wenn diese Einstellung aktiviert ist, wird das Schloss-Symbol nicht auf Gegenständen mit einer Markierung angezeigt.", "BadgePostmaster": "Zeige die Anzahl der Poststellen-Gegenstände für den aktuellen Charakter auf dem App-Symbol", "BadgePostmasterExplanation": "Damit dies funktioniert, musst du DIM als App installieren und dein Betriebssystem muss die Anzeige von Abzeichen unterstützen", "BothDescriptions": "Beide Beschreibungen", "BungieDescriptionOnly": "Bungie-Beschreibungen", "CharacterOrder": "Sortiere Charaktere nach", "CharacterOrderFixed": "Alter des Charakters (buggy auf PC)", "CharacterOrderRecent": "Zuletzt aktiver Charakter", "CharacterOrderReversed": "Zuletzt aktiver Charakter (umgekehrt)", "ColumnSize": "{{num}} Items", "ColumnSizeAuto": "Automatisch", "CommunityData": "Community-Perk-Einblicke", "CommunityDescriptionOnly": "Community-Beschreibung", "CsvImport": "Importiere CSV", "CustomErrorLabel": "Ein Stat-Name muss alphanumerische Zeichen enthalten und sich von anderen Stat-Namen für diese Hüterklasse unterscheiden.", "CustomErrorValues": "Stat-Wichtungen müssen positive Zahlen sein.\nMindestens 2 Stat-Wichtungen müssen größer als Null sein.", "CustomStatChooseName": "Wähle einen benutzerdefinierten Stat-Namen", "CustomStatCreate": "Erstelle neuen benutzerdefinierten Stat", "CustomStatDelete": "Lösche diesen benutzerdefinierten Stat", "CustomStatDeleteConfirm": "Diesen benutzerdefinierten Stat löschen?", "CustomStatDesc1": "Wähle die gewünschten Rüstungs-Stats, um daraus eine benutzerdefinierte Summe zu bilden.", "CustomStatDesc3": "Benutzerdefinierte Summen erscheinen im Item-Popup, Organizer und beim Vergleich.", "CustomStatTitle": "Benutzerdefinierte Summe", "Data": "Excel-Tabelle", "DefaultItemSizeNote": "Eine Größe von 50px wird am schärfsten aussehen, ohne das Bild oder den Text zu verwischen.", "DontForgetDupes": "Vergiss nicht, dass du nach is:dupe suchen kannst, um schnell doppelte Gegenstände zu finden und du kannst das Vergleichstool oder den Organizer nutzen, um verwandte Gegenstände zu begutachten.", "EnableAdvancedStats": "Qualitätsbewertung auf Rüstung anzeigen (D1)", "ExpandSingleCharacter": "Alle Charaktere anzeigen", "ExportLoadoutSS": "Loadout-Tabellen", "ExportLoadoutSSHelp": "Lade eine CSV-Liste deiner DIM Loadouts herunter, die du in der Tabellen-App deiner Wahl ansehen kannst.", "ExportProfile": "API-Profilantwort exportieren", "ExportSS": "Inventar-Tabellen", "ExportSSHelp": "Lade eine CSV-Tabelle von deinen Items herunter, die leicht mit jedem Tabellenprogramm angezeigt werden kann.", "HidePullFromPostmaster": "Zeige den \"$t(Loadouts.PullFromPostmaster)\"-Knopf nicht an", "Inventory": "Inventar-Anzeige", "InventoryColumns": "Charakter-Inventar-Breite", "InventoryColumnsMobile": "Charakter-Inventar-Breite auf Mobilgeräten (Portrait)", "InventoryColumnsMobileLine2": "Die Größe der Gegenstände wird an die neue Einstellung angepasst", "InventoryNumberOfSpacesToClear": "Anzahl der leeren Plätze, die beim Benutzen des Farm-Modus bereitgestellt werden sollen", "Items": "Gegenstands-Anzeige", "Language": "Sprache", "LogOut": "Abmelden", "Masterworked": "Gemeisterwerkt", "MaxParallelCores": "Maximale Kerne für parallele Aufgaben", "MaxParallelCoresExplanation": "Bestimmt, wie viele CPU-Kerne DIM für intensive Aufgaben wie Loadout-Optimierer und Loadout-Analysator verwenden kann. Höhere Werte können die Leistung verbessern, aber mehr Systemressourcen nutzen.", "OrnamentDisplay": "Ornamente auf Gegenstands-Icons anzeigen", "OrnamentDisplayExplanationDisabled": "Gegenstände werden niemals ihre Ornamente anzeigen", "OrnamentDisplayExplanationEnabled": "Fahre mit der Maus über die Rüstung oder halte sie gedrückt, um das Ornament auszublenden", "OrnamentDisplayExplanationHide": "Fahre mit der Maus über den Gegenstand oder halte ihn gedrückt, um das Ornament auszublenden", "OrnamentDisplayExplanationShow": "Fahre mit der Maus über den Gegenstand oder halte ihn gedrückt, um das Ornament anzuzeigen", "ResetToDefault": "Zurücksetzen", "RestoreVaultSide": "Zeige Tresor-Gegenstände in einer eigenen Spalte", "ReverseSort": "Sortierung umschalten", "SetSort": "Sortiere Gegenstände nach:", "SetVaultWeaponGrouping": "Gruppiere Waffen im Tresor nach:", "Settings": "Einstellungen", "ShowNewItems": "Einen roten Punkt bei neuen Gegenständen anzeigen", "SingleCharacter": "Einzel-Charakter-Ansicht", "SingleCharacterExplanation": "DIM zeigt nur den zuletzt gespielten Charakter an.\nGegenstände im Inventar von versteckten Charakteren erscheinen im Tresor, wenn sie vom aktuellen Charakter verwendet werden können.\nGegenstände für andere Klassen werden komplett versteckt.", "SizeItem": "Gegenstandsgröße", "SortByAmmoType": "Munitionstyp", "SortByAmount": "Stapelgröße", "SortByClassType": "Benötigte Klasse", "SortByCrafted": "Geformt (D2)", "SortByDeepsight": "Tiefensicht (D2)", "SortByFeatured": "Neue Ausrüstung / Im Brennpunkt (D2)", "SortByPrimary": "Powerlevel", "SortByRarity": "Seltenheit", "SortByRating": "Rüstungsqualität (D1)", "SortByRecent": "Zeitpunkt des Erhalts (D2)", "SortBySeason": "Saison (D2)", "SortByTag": "Markierung ({{taglist}})", "SortByTier": "Tier (D2)", "SortByType": "Typ", "SortByWeaponElement": "Schadensart", "SortCustom": "Benutzerdefinierte Sortierung", "SortName": "Name", "SpacesSize_one": "{{count}} Platz", "SpacesSize_other": "{{count}} Plätze", "Theme": "Aussehen", "Troubleshooting": "Problembehebung", "VaultArmorGroupingStyle": "Separiere Rüstung nach Klassen in verschiedene Zeilen", "VaultGroupingNone": "Keine", "VaultUnder": "Zeige Tresor-Gegenstände unterhalb der ausgerüsteten", "VaultWeaponGroupingStyle": "Separiere Waffengruppen in verschiedene Zeilen", "WeaponFrame": "Waffenrahmen", "WishlistRefreshNotificationBody": "Wenn du keine Updates siehst, stelle sicher, dass die Quelle (wie GitHub) sie widerspiegelt!", "WishlistRefreshNotificationTitle": "Wunschlisten neu geladen" }, "Sockets": { "ApplyPerks": "Perks anwenden", "GridStyle": "Perks als Raster anzeigen", "Insert": { "Ability": "Fähigkeit ausrüsten", "Aspect": "Aspekt einfügen", "Fragment": "Fragment einfügen", "Mod": "Mod einfügen", "Ornament": "Ornament anwenden", "Projection": "Geist-Projektion anwenden", "Shader": "Shader anwenden", "Super": "Super ausrüsten", "Transmat": "Teleporteffekt anwenden" }, "ListStyle": "Perks als Liste anzeigen", "Search": "Suche Namen oder Beschreibungen", "Select": { "Ability": "Fähigkeits-Vorschau", "Aspect": "Aspekt-Vorschau", "Fragment": "Fragment-Vorschau", "Mod": "Mod-Vorschau", "Ornament": "Ornament-Vorschau", "Projection": "Geist-Projektions-Vorschau", "Shader": "Shader-Vorschau", "Super": "Vorschau Super", "Transmat": "Teleporteffekt-Vorschau" }, "SelectWishlistPerks": "Vorschau der Wunschlisten-Perks" }, "Stats": { "CrouchingSpeed": "Schleichen", "Custom": "Ben.-def. Summe", "CustomDesc": "Benutzerdefinierte Summe der ausgewählten Basis-Stats, Mods oder Meisterwerke. Gehe in die Einstellungen, um zu konfigurieren, welche Stats enthalten sind.", "DamageResistance": "PvE-Schaden-Widerstand", "Discipline": "Disziplin", "DropLevel": "Account Power", "DropLevelExplanation1": "Account Power ist das aktuelle Basislevel bei der Berechnung des erhöhten Levels von Mächtiger- und Spitzen-Belohnungen.", "DropLevelExplanation2": "Account Power verwendet den höchsten Level Gegenstand in jedem Slot, unabhängig von der gewünschten Klasse oder der \"Ein Exotik\"-Regel.", "EquippableGear": "Ausrüstbare Ausrüstung", "FlinchResistance": "Zurückweichen-Widerstand", "HP": "LP", "Intellect": "Intellekt", "MaxGearPower": "Maximale Power ausrüstbarer Gegenstände", "MaxGearPowerAll": "Maximale Power aller Gegenstände", "MaxGearPowerOneExoticRule": "Maximale Stärke der ausrüstbaren Ausrüstung\n(nur eine exotische Rüstung ausgerüstet)", "MaxTotalPower": "Maximale Gesamt-Power", "MetersPerSecond": "m/s", "Milliseconds": "ms", "NoBonus": "Kein Bonus", "NotApplicable": "N/V", "OfMaxRoll": "{{range}} des max. Rolls", "PercentHelp": "Klicke um mehr Informationen über die Stats-Qualität zu erhalten.", "Percentage": "%", "PowerModifier": "Power gewährt durch saisonalen Erfahrungsfortschritt", "Prestige": "Prestige Level: {{level}}\n{{exp}}xp bis 5 Lichtpartikel.", "Quality": "Stats-Qualität", "ShieldHP": "Schild-HP", "StrafingSpeed": "Seitwärts", "Strength": "Stärke", "TierProgress": "T{{tier}} {{statName}} ({{progress}}/60 für T{{nextTier}})\n", "TierProgress_Max": "T{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "Zeit bis volle LP", "Total": "Gesamt", "TotalHP": "Gesamt-LP", "WalkingSpeed": "Gehen", "WeaponPart": "Waffenteil" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM kann nun Tags, Loadouts und Einstellungen auf unseren eigenen Servern speichern und zwischen verschiedenen Versionen von DIM synchronisieren, ohne separaten Login. Du kannst deine vorhandenen Daten auf der Einstellungsseite importieren, wenn du DIM Sync zuvor nicht aktiviert hattest. Dies wurde durch die Unterstützung unserer OpenCollective Backers ermöglicht!", "No": "Nicht jetzt", "Title": "DIM Sync aktivieren?", "Yes": "Sync aktivieren" }, "AutoBackup": "Wir haben deine Daten in der Datei dim-data.json in deinem Download Ordner gesichert, für alle Fälle.", "BackUpFirst": "Du MUSST deine Daten zuerst sichern, bevor du alles löschen kannst. Für alle Fälle.", "BrowserMayClearData": "Der Browser könnte diese Informationen eventuell löschen, wenn du zu wenig freien Speicher hast oder DIM nicht regelmäßig besuchst.", "DataIsLocal": "Markierungen und Notizen sind nur lokal gespeichert", "DeleteAllData": "ALLE Daten von DIM Sync-Servern löschen", "DeleteAllDataConfirm": "Bist du sicher, dass du ALLE deine Daten für alle Konten von DIM Sync löschen möchtest? Du kannst dies nicht rückgängig machen.", "Details": { "IndexedDBStorage": "Lokaler Speicher, speichert deine Daten nur in diesem Browser. Beim Löschen deiner Browserdaten werden diese Informationen gelöscht." }, "DimApiFinePrint": "DIM speichert deine Tags, Loadouts und Einstellungen auf den DIM-Servern und synchronisiert sie zwischen verschiedenen Versionen von DIM.", "DimSyncDown": "DIM Sync konnte sich, aufgrund eines Kommunikationsfehlers mit dem Server, nicht verbinden.", "DimSyncEnabled": "DIM Sync aktiviert", "DimSyncNotEnabled": "DIM Sync ist nicht aktiviert. Dies bedeutet, dass deine Einstellungen, Markierungen, Loadouts und Suchanfragen nur lokal gespeichert sind und verlorengehen werden, wenn du deinen Browserspeicher löschst. Du kannst DIM Sync in den Einstellungen aktivieren, um deine Daten automatisch zu sichern; oder sie regelmäßig zum Sichern manuell herunterladen.", "EnableDimApi": "DIM Sync aktivieren (empfohlen)", "Export": "Datensicherung herunterladen", "ExportError": "Fehler beim Herunterladen des Backups von DIM Sync", "ExportErrorBody": "DIM Sync ist möglicherweise offline, oder du hast Probleme mit deiner Verbindung. Wir werden stattdessen eine Kopie deiner lokal gespeicherten Daten herunterladen.", "Import": "Datensicherung importieren", "ImportConfirmDimApi": "Bist du sicher, dass du deine aktuellen Tags, Loadouts und Einstellungen mit dieser Version überschreiben möchtest? Dies wird deine aktuelle Version komplett ersetzen.", "ImportExport": "Backup & Import", "ImportFailed": "Importieren fehlgeschlagen! {{error}}", "ImportNoFile": "Keine Datei ausgewählt!", "ImportNotification": { "FailedBody": "Daten konnten nicht importiert werden. {{error}}", "FailedTitle": "Import fehlgeschlagen", "NoData": "Keine Loadouts oder Markierungen in der Sicherung gefunden", "SuccessBodyForced": "Einstellungen, {{loadouts}} Loadouts und {{tags}} markierte Gegenstände wurden aus deiner Sicherung in DIM Sync importiert, und ersetzten das, was bereits vorhanden war.", "SuccessBodyLocal": "Importierte Einstellungen, {{loadouts}} Loadouts und {{tags}} markierte Gegenstände aus deiner Sicherung in den lokalen Speicher und ersetzten das, was bereits vorhanden war. Wir können nicht garantieren, dass der lokale Speicher nicht verloren geht - denke daran, DIM Sync zu aktivieren.", "SuccessTitle": "Import erfolgreich" }, "ImportTooManyFiles": "Bitte nur eine Datei zum Importieren wählen.", "ImportWrongFileType": "Die Datei ist keine JSON-Datei. Womöglich ist es keine DIM-Sicherung.", "IndexedDBStorage": "Lokaler Browserspeicher", "LearnMore": "Erfahre mehr über DIM Sync", "MenuTitle": "Sync & Backups", "ProfileErrorBody": "Wir hatten ein Problem bei der Kommunikation mit DIM Sync. Deine neuesten Einstellungen, Markierungen, Loadouts und Suchanfragen werden möglicherweise nicht angezeigt. Ihre Daten sind immer noch auf unseren Servern und alle Updates, die du lokal vornimmst, werden gespeichert, sobald wir eine erneute Verbindung herstellen können. Wir werden es immer wieder versuchen, solange DIM geöffnet ist.", "ProfileErrorTitle": "DIM-Sync-Downloadfehler", "RefreshDimSync": "Remote-Daten von DIM Sync neu laden", "UpdateErrorBody": "Beim Speichern deiner Daten in DIM Sync ist ein Problem aufgetreten. Wir versuchen es weiter, solange DIM geöffnet ist.", "UpdateErrorTitle": "DIM-Sync-Speicherfehler", "UpdateInvalid": "Fehler beim Speichern der Daten in DIM Sync", "UpdateInvalidBody": "An DIM Sync gesendete Daten waren ungültig und werden nicht gespeichert.", "UpdateInvalidBodyLoadout": "Das Loadout \"{{name}}\" ist ungültig und wird nicht gespeichert. Wenn du es von einer anderen Seite importiert hast, lasse sie bitte wissen, dass sie ungültige Loadouts exportieren.", "UpdateQueueLength_one": "{{count}} neue Änderung wird bei der nächsten erfolgreichen Verbindung gespeichert werden.", "UpdateQueueLength_other": "{{count}} neue Änderungen werden bei der nächsten erfolgreichen Verbindung gespeichert werden.", "Usage": "DIM benutzt insgesamt {{usage, humanBytes}} von {{quota, humanBytes}}, auf diesem Gerät. Das beinhaltet die heruntergeladene Destiny-Datenbank von Bungie.net." }, "StreamDeck": { "Authorize": "Anwendung verbinden", "Enable": "Stream Deck Plugin", "Error": { "Body": "Beim Senden der Daten an das Stream-Deck-Plugin ist ein Fehler aufgetreten. Bitte kontaktieren Sie den Plugin Entwickler. {{error}}", "Title": "Stream-Deck-Plugin-Fehler" }, "FinePrint": "Aktiviere die Verbindung mit dem DIM Stream Deck Plugin. Dieses Plugin ist ein separates Projekt, das weder vom DIM-Team geschrieben noch unterstützt wird.", "Install": "Plugin installieren", "MissingAuthorization": "Du musst die Stream Deck App autorisieren, um sie mit DIM zu verbinden. Gehe zu den Einstellungen und klicke auf \"Anwendung verbinden\".", "Tooltip": { "Application": "Stream Deck Anwendung", "AuthRequired": "Klicke auf diese Schaltfläche oder gehe zu den Einstellungen und klicke auf \"Anwendung verbinden\".", "Error": "Dein Stream Deck Plugin wird nicht länger unterstützt. Bitte aktualisiere sie auf die neueste Version. Dieses Plugin erfordert mindestens:", "ErrorConnection": "Wenn du bereits die neueste Version verwendest, überprüfe, ob eine Browser-Erweiterung die Verbindung blockiert.", "ExtensionIssue": "Problem mit Erweiterung", "Plugin": "Plugin", "Title": "DIM Stream Deck Plugin", "Version": "Version:" } }, "StripSockets": { "Action": "Fassungen leeren", "ArmorMods": "{{count}}x Rüstungsmod", "Button": "{{numSockets}} Fassungen leeren", "Cancel": "Abbrechen", "Choose": "Fassungen zum Leeren auswählen", "DiscountedMods": "{{count}}x Rabatt-Mod", "Done": "Fassungen geleert", "NoSockets": "Keine Fassungen zu leeren", "Ok": "OK", "Ornaments": "{{count}}x Ornament", "Others": "{{count}}x Geist-Projektion", "Running": "Fassungen werden geleert", "Shaders": "{{count}}x Shader", "Subclass": "{{count}}x Fokus-Option", "WeaponMods": "{{count}}x Waffenmod" }, "Tags": { "Archive": "Archiv", "ClearTag": "Markierung löschen", "Favorite": "Favorisieren", "Infuse": "Infundieren", "Junk": "Müll", "Keep": "Behalten", "LockAll": "Sperre Gegenstände", "TagItem": "Markieren", "UnlockAll": "Entsperre Gegenstände" }, "Triage": { "AccountsForArtifice": "Dies testet, ob eine Kunstvolle Rüstung besser sein könnte, wenn diese einen +3-Stat-Mod verwendet.", "BetterArmor": "Strikt bessere Rüstung", "BetterArtificeArmor": "Bessere Kunstvolle Rüstung", "BetterStatArmor": "Rüstung mit besseren Stats", "BetterStatArtificeArmor": "Bessere Kunstvolle Rüstung", "BetterWorseArmor": "Bessere/Schlechtere Rüstung", "BetterWorseIncludes": "Zeigt Rüstungen mit:", "HighStats": "Hohe Stats", "InLoadouts": "In Loadouts", "OwnedCount": "# In Besitz", "PerkBetterArmorDesc": "Die gleichen, oder mehr intrinsische Perks oder spezielle Mod-Slots.", "PerkWorseArmorDesc": "Dem gleichen intrinsischen Perk, oder keinen.", "SimilarItems": "Ähnliche Gegenstände", "StatBetterArmorDesc": "Alle Stats mindestens gleich hoch, und mindestens einen Stat besser.", "StatNotPerkArmorDesc": "Dies testet nur Stats. Ein schlechterer Gegenstand kann noch spezielle Mod-Slots oder intrinsische Perks haben.", "StatWorseArmorDesc": "Keinen besseren Stats, und mindestens einem schlechteren Stat.", "ThisItem": "Dieser Gegenstand", "WorseArmor": "Strikt schlechtere Rüstung", "WorseArtificeArmor": "Schlechtere Nicht-Kunstvolle Rüstung", "WorseStatArmor": "Schlechtere Rüstung", "WorseStatArtificeArmor": "Schlechtere Nicht-Kunstvolle Rüstung", "YourBestItem": "Dein bester Gegenstand" }, "Triumphs": { "GildingTriumph": "Triumph-Vergoldung", "HideCompleted": "Abgeschlossene Triumphe ausblenden", "RevealRedacted": "Versteckte Triumphe enthüllen", "SortRecords": "Triumphe nach Fertigstellung sortieren" }, "Vendors": { "Collections": "Sammlungen", "Engram": "Rang", "FilterToUnacquired": "Nur nicht erworbene Gegenstände anzeigen", "HideSilverItems": "Silber-Gegenstände ausblenden", "NoItems": "Dieser Händler bietet zurzeit keine Gegenstände an.", "RefreshTime": "Inventar wird aktualisiert in:", "Vendors": "Händler" }, "Views": { "About": { "APIHistory": "Übersicht aller Aktionen von DIM (und anderen Destiny-Apps) anzeigen", "BungieCopyright": "Alle Bilder und Inhalte sind Eigentum von Bungie.", "CommunityInsight": "Community-Perk-Einblicke und Character Stats von {{clarityLink}}. Wenn du Ungenauigkeiten bemerkst oder Fragen hast, tritt dem {{clarityDiscordLink}} bei.", "Discord": "Discord", "DiscordHelp": "Stelle Fragen, gib Feedback und erhalte Unterstützung in unseren Discord-Kanälen.", "FAQ": "Häufig Gestellte Fragen", "FAQAccess": "Wie bekommt DIM Zugriff auf meine Destiny-Daten?", "FAQAccessAnswer": "Wir benutzen Bungies App-Authentifizierung, um DIM deine Items sehen und verschieben lassen zu können. DIM sieht niemals deinen Benutzernamen oder Passwort. So arbeitet auch Bungies eigene Gefährten-App.", "FAQKeyboard": "Unterstützt DIM Tastaturkürzel?", "FAQKeyboardAnswer": "Ja! Drücke \"?\" um eine Liste der Kürzel zu sehen.", "FAQLogout": "Wie kann ich mich von DIM abmelden?", "FAQLogoutAnswer": "Öffne das Menü mit dem linken oberen Symbol und wähle \"Abmelden\"", "FAQLostItem": "Ich habe durch euer Tool ein Item verloren!", "FAQLostItemAnswer": "Bungie erlaubt es Apps nicht, Gegenstände zu löschen (selbst der eigenen nicht!). Wahrscheinlicher ist, dass die Übertragung fehlgeschlagen ist und dein Gegenstand im Tresor oder auf einem anderen Charakter verblieben ist. Du könntest nach ihm suchen. Wenn es dir nicht angezeigt wird, lade die Seite erneut. Überprüfe {{link}} oder im Spiel, um zu sehen, ob der Gegenstand noch existiert. Wir sind sicher, er ist immer noch da.", "FAQMobile": "Wird DIM auf mobilen Endgeräten unterstützt? Wird es eine App geben?", "FAQMobileAnswer": "Die DIM-Website kann heute auf Smartphones und Tablets geladen werden, und du kannst es zu deinem Homescreen für eine App-ähnliche Erfahrung hinzufügen.", "GitHub": "GitHub", "GitHubHelp": "Wenn du an dem Projekt mitwirken möchtest, besuche uns auf unserer Projektseite auf {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM ist eine kostenlose Open-Source-App, die von Community-Entwicklern, mit den gleichen Diensten von Bungie.net und der Destiny Companion App, gebaut wurde.", "Schedule": { "beta": "Diese Beta-Version von DIM wird jedes Mal aktualisiert wenn wir etwas am Code ändern - sie erhält die neuesten Funktionen und Fehlerbehebungen, aber auch die neuesten Fehler!", "release": "Diese Version von DIM wird pro Woche einmal um circa Mitternacht am Sonntag, US Pazifischer Zeit aktualisiert." }, "Translation": "Tritt dem Übersetzer-Team bei!", "TranslationText": "Wir benutzen {{link}} für eine unkomplizierte Übersetzung. Wenn du die DIM-Übersetzung in einer Sprache verbessern möchtest, tritt dem Team bei.", "Version": "Version {{version}} ({{flavor}}), erstellt am {{date}}", "Wiki": "DIM-Benutzerhandbuch", "WikiHelp": "Erfahre, wie du die DIM-Funktionen verwendest." }, "Login": { "Auth": "Autorisiere mit Bungie.net", "EnableDimSyncWarning": "Du hattest DIM Sync zuvor deaktiviert und nur lokalen Datenspeicher verwendet. Das Aktivieren von DIM Sync ersetzt alle lokalen Daten durch die Daten von DIM Sync. Du solltest deine Daten sichern bevor du DIM Sync aktivierst. Du kannst sie in den Einstellungen wiederherstellen.", "Explanation": "Erlaube DIM, deine Destiny-Charaktere, deinen Tresor und Fortschritt einzusehen und zu bearbeiten.", "LearnMore": "Erfahre mehr über Konten und Login", "NewAccount": "Mit einem anderen Bungie.net-Konto anmelden", "Permission": "Wir brauchen dein Einverständnis..." }, "Support": { "BackersDetail": "Unterstütze uns mit einer einmaligen oder monatlichen Spende, um uns zu helfen aktiv weiterzuentwickeln.", "FreeToDownload": "DIM ist ein kostenloses Tool. Der Quellcode ist Open-Source und kann von jedem verbessert werden. Du wirst niemals Werbung in DIM sehen. Das ist unser Versprechen.", "OpenCollective": "Wir benutzen den {{link}}-Service, um die Hingabe und Zeit zu kompensieren, die unsere Entwickler in dieses Projekt gesteckt haben.", "Store": "Wir haben Fan-Artikel mit unserem Logo und anderen Designs zum Verkauf auf {{link}}", "Support": "Unterstütze DIM" } }, "WishListRoll": { "BestRatedTip_one": "Dieser Perk passt genau zu einem Waffenroll auf deiner Wunschliste.", "BestRatedTip_other": "Diese Perks entsprechen genau einem Waffenroll auf deiner Wunschliste.", "Clear": "Wunschliste löschen", "CopiedLine": "Wunschlisten-Roll in Zwischenablage kopiert", "CopyLine": "Ausgewählte Perks als Wunschlisten-Roll kopieren", "DupeRolls": " (+{{num, number}} ignorierte Duplikate)", "ExternalSource": "Weitere Wunschliste hinzufügen", "ExternalSourcePlaceholder": "Wunschlisten-URL hier einfügen", "Header": "Wunschliste", "Import": "Wunschlisten-Rolls laden", "ImportError": "Fehler beim Laden der Wunschliste von \"{{url}}\": {{error}}", "ImportFailed": "Keine deiner Wunschlisten enthielt gültige Listen.", "ImportNoFile": "Keine Datei ausgewählt.", "InvalidExternalSource": "Bitte gib eine gültige URL für die externe Wunschlistenquelle ein. Die URL muss wie folgt beginnen:", "JustAnotherTeam": "Nur ein anderes Team", "LastUpdated": "Zuletzt aktualisiert: {{lastUpdatedDate}} um {{lastUpdatedTime}}", "Num": "{{num, number}} Rolls in deiner Wunschliste", "NumRolls": "{{num, number}} Rolls", "Refresh": "Wunschliste aktualisieren", "SourceAlreadyAdded": "Wunschliste bereits hinzugefügt", "UpdateExternalSource": "Wunschliste hinzufügen", "Voltron": "Voltron (Standard)", "WishListNotes": "Wunschlisten-Notizen:", "WorstRatedTip_one": "Dieser Perk passt genau zu einem Waffenroll auf deiner Müll-Liste.", "WorstRatedTip_other": "Diese Perks entsprechen genau einem Waffenroll auf deiner Müll-Liste." }, "no-space": "no-space", "wrong-level": "wrong-level" } ================================================ FILE: src/locale/en.json ================================================ { "AWA": { "ConfirmDescription": "Please use the Destiny 2 Companion App to approve DIM to modify your items.", "ConfirmTitle": "Confirm Action", "Error": "Error changing mods or perks", "ErrorMessage": "We couldn't equip {{plug}} into {{item}}.\n\n{{error}}", "FailedToken": "Couldn't get permission to change item", "IrreversiblePlugging": "You don't own {{plug}}, so we won't overwrite it." }, "Accounts": { "Choose": "Profiles for {{bungieName}}", "ErrorLoadInventory": "Unable to load your Destiny {{version}} characters and inventory", "ErrorLoadManifest": "Unable to load Destiny info database from Bungie", "ErrorLoading": "Unable to load Destiny accounts from Bungie.net", "MissingAccountWarning": "If you don't see your account here, you may not have logged in to the right Bungie.net account, or Bungie.net may be down for maintenance.", "MissingDescription": "The account you're trying to view is not an account linked to your Bungie.net profile. Select one of your accounts below.", "MissingTitle": "Account Not Found", "NoCharacters": "You have no Destiny characters associated with this Bungie.net account. Try logging into a different account.", "NoCharactersTitle": "No Characters Found", "SwitchAccounts": "You can switch accounts later from the menu in the header.", "Title": "Accounts" }, "Activities": { "Activities": "Activities", "Hard": "Hard", "Nightfall": "Nightfall Strike", "Normal": "Normal", "WeeklyHeroic": "Weekly Heroic Strike" }, "Armory": { "AlternateItems": "Alternate Versions", "Armory": "Armory", "DifferentSeason": "Reissue from a different season", "NoNotes": "No Notes", "OpenInArmory": "view in Armory", "Season": "Season {{season}}, Year {{year}}", "TrashlistedRolls_one": "Trashlisted Roll", "TrashlistedRolls_other": "{{count, number}} Trashlisted Rolls", "Unknown": "Unknown Item", "UnknownPerkHash": "The perk hash {{hash}} ({{perkName}}) does not appear on this item, so this wish list roll is invalid. Please contact the wish list author to correct this. Note that wish lists should always specify the non-enhanced version of perks.", "WishlistedRolls_one": "Wishlisted Roll", "WishlistedRolls_other": "{{count, number}} Wishlisted Rolls", "YourItems": "Your Items" }, "Browsercheck": { "Samsung": "Samsung Internet can make sites look too dark when dark mode is on. Enable Settings > Labs > Use website dark theme or switch to another browser.", "Steam": "The Steam overlay browser is very old and some or all DIM features may not work. We cannot provide support for it.", "Unsupported": "The DIM team does not support using this browser. Some or all DIM features may not work." }, "Bucket": { "Armor": "Armor", "Class": "Subclass", "General": "General", "Ghost": "Ghost", "Inventory": "Inventory", "Postmaster": "Postmaster", "Progress": "Progress", "Reputation": "Reputation", "Unknown": "Unknown", "Vault": "Vault", "Weapons": "Weapons" }, "BulkNote": { "Append": "Append to notes / add #hashtags", "Confirm": "Update Notes", "Remove": "Remove from notes / remove #hashtags", "Replace": "Replace notes", "Title_one": "Change notes for 1 item", "Title_other": "Change notes for {{count}} items" }, "BungieAlert": { "Title": "A message from Bungie:" }, "BungieService": { "AppNotPermitted": "DIM does not have permission to perform this action.", "DestinyCannotPerformActionAtThisLocation": "You cannot equip items or change mods while in an activity. Try heading to orbit or a social area. This is a limitation of the Bungie.net API, not DIM.", "DestinyItemUnequippable": "You cannot equip this item. If this character's last activity locked their equipment, try logging into the character again.", "DestinyLegacyPlatform": "Bungie's services currently have a bug that prevents DIM from loading info for your Destiny 2 account if you played Destiny 1 on a last-gen console. Bungie will fix this soon, but until then you must play Destiny 1 on a current-gen console to be able to access your info.", "DevVersion": "Are you running a development version of DIM? You must register your chrome extension with Bungie.net.", "Difficulties": "Bungie.net is currently experiencing difficulties.", "ErrorTitle": "Bungie.net Error", "ItemUniquenessExplanation": "A character can only have one of '{{name}}' on it.", "Maintenance": "Bungie.net servers are down for maintenance.", "MissingInventory": "Bungie.net did not return your inventory, possibly because your privacy settings prevent it. Try logging out and logging back in.", "NetworkError": "Network error - {{status}} {{statusText}}", "NoAccount": "No Destiny account was found. Do you have the right platform selected?", "NoAccountForPlatform": "Failed to find a Destiny account for you on {{platform}}.", "NotConnected": "You may not be connected to the internet.", "NotConnectedOrBlocked": "You may not be connected to the internet, or an ad blocking or privacy extension may be blocking Bungie.net.", "NotLoggedIn": "Please authorize DIM in order to use this app.", "Slow": "Bungie.net is slow right now", "SlowDetails": "Bungie.net is taking a long time to return your information. This can happen when a lot of players are in the game at once, or if Bungie.net is having issues. You also might be having an internet connection issue. We'll keep waiting for a response.", "SlowResponse": "Bungie.net was too slow to respond.", "Throttled": "Bungie.net is limiting how many requests DIM can make.", "Twitter": "Get status updates on:", "UnknownError": "Bungie.net message: {{message}}", "VendorNotFound": "Vendor data is unavailable." }, "Compare": { "Archetype": "Archetype", "AssumeMasterworked": "Assume Masterworked", "AssumeMasterworkedDescription": "Stats if fully Masterworked, without current Mods", "BaseStatsDescription": "Base stats, without Masterwork or Mods", "Button": "Compare", "ButtonHelp": "Compare Items", "CompareBaseStats": "Show Base Stats", "CurrentStats": "Current Stats", "CurrentStatsDescription": "Current stats, including Mods and Masterwork level", "Error": { "Invalid": "There are no valid items for comparison.", "Unmatched": "This item doesn't match the type of items being compared." }, "InitialItem": "This is the item the Compare tool was launched from", "IsVendorItem": "This item is not in your inventory, but {{vendorName}} sells it.", "NoModArmor": "Pre-mods" }, "Cooldown": { "Grenade": "Grenade cooldown: {{cooldown}}", "Melee": "Melee cooldown: {{cooldown}}", "Super": "Super cooldown: {{cooldown}}" }, "Countdown": { "Days_compact_one": "{{count}}d", "Days_compact_other": "{{count}}d", "Days_one": "1 Day", "Days_other": "{{count}} Days" }, "Csv": { "EmptyFile": "There were no rows in the file.", "ImportConfirm": "Are you sure you want to import tags/notes from CSV? This will overwrite tags/notes for all items contained in your spreadsheet.", "ImportFailed": "Failed to import tags/notes from CSV: {{error}}", "ImportSuccess_one": "Tags/notes loaded for one item.", "ImportSuccess_other": "Tags/notes loaded for {{count}} items.", "ImportWrongFileType": "File is not a CSV file.", "WrongFields": "CSV must have 'Id', 'Notes', 'Tag', and 'Hash' columns." }, "Dialog": { "Cancel": "Cancel", "OK": "OK" }, "EnergyMeter": { "Energy": "Energy", "Unused": "Unused", "UpgradeNeeded": "This item's current energy capacity is {{energyCapacity}}. To fit the selected mods, its energy capacity must be {{energyUsed}}.", "Used": "Used" }, "ErrorBoundary": { "Title": "Something went wrong" }, "ErrorPanel": { "BrowserTooOld": "Your browser is too old to use DIM. Please update your browser to the latest version.", "BrowserTooOldTitle": "Incompatible browser", "Description": "Try loading your inventory in the Destiny 2 Companion App to see if Bungie.net is down.", "ReadTheGuide": "Read our User Guide (linked from the menu) for troubleshooting steps.", "SystemDown": "This affects all Destiny apps, and the DIM team cannot fix or bypass it.", "Troubleshooting": "Troubleshooting Guide" }, "FarmingMode": { "D2Desc_female_one": "DIM is preventing items from going to the Postmaster by making sure there's always one empty space per item type on {{store}}.", "D2Desc_female_other": "DIM is preventing items from going to the Postmaster by making sure there's always {{count}} empty spaces per item type on {{store}}.", "D2Desc_male_one": "DIM is preventing items from going to the Postmaster by making sure there's always one empty space per item type on {{store}}.", "D2Desc_male_other": "DIM is preventing items from going to the Postmaster by making sure there's always {{count}} empty spaces per item type on {{store}}.", "D2Desc_one": "DIM is preventing items from going to the Postmaster by making sure there's always one empty space per item type on {{store}}.", "D2Desc_other": "DIM is preventing items from going to the Postmaster by making sure there's always {{count}} empty spaces per item type on {{store}}.", "Desc_female_one": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping one empty space open per item type to prevent anything from going to the Postmaster.", "Desc_female_other": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping {{count}} spaces open per item type to prevent anything from going to the Postmaster.", "Desc_male_one": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping one empty space open per item type to prevent anything from going to the Postmaster.", "Desc_male_other": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping {{count}} spaces open per item type to prevent anything from going to the Postmaster.", "Desc_one": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping one empty space open per item type to prevent anything from going to the Postmaster.", "Desc_other": "DIM is moving Engram and Glimmer items from {{store}} to the vault and keeping {{count}} spaces open per item type to prevent anything from going to the Postmaster.", "FarmingMode": "Farming Mode", "FarmingModeNote": "(maintain space for drops)", "MakeRoom": { "Desc": "DIM is moving only Engram and Glimmer items from {{store}} to the vault or other characters to prevent anything from going to the Postmaster.", "Desc_female": "DIM is moving only Engram and Glimmer items from {{store}} to the vault or other characters to prevent anything from going to the Postmaster.", "Desc_male": "DIM is moving only Engram and Glimmer items from {{store}} to the vault or other characters to prevent anything from going to the Postmaster.", "MakeRoom": "Make room to pick up items by moving equipment", "Tooltip": "If checked, DIM will move weapons and armor around to make space in the vault for engrams." }, "OutOfRoom": "You're out of space to move items off of {{character}}. Time to clear out the trash!", "OutOfRoomTitle": "Out of Room", "Stop": "Stop", "Vault": "It will move items to the vault to make room." }, "FashionDrawer": { "Accept": "Save fashion", "CannotFitOrnament": "This item does not have an ornament socket or you have no ornaments for it.", "CannotFitShader": "This item cannot fit a shader", "ClearOrnaments": "Clear Ornaments", "ClearOrnamentsTitle": "Reset all ornaments to \"no preference\"", "ClearShaders": "Clear Shaders", "ClearShadersTitle": "Reset all shaders to \"no preference\"", "NoPreference": "No preference - this socket won't be changed", "Reset": "Clear fashion", "Sync": "Sync", "SyncOrnaments": "Sync Ornaments", "SyncOrnamentsTitle": "Use ornaments from the same set on all items, if they're unlocked", "SyncShaders": "Sync Shaders", "SyncShadersTitle": "Use the same shader on all items", "Title": "Choose shaders and ornaments", "UseEquipped": "Use equipped fashion" }, "FileUpload": { "Instructions": "Click or drag files" }, "Filter": { "Adept": "\\(Adept\\)", "AmmoType": "Shows items based on their ammo type.", "Armor": "Shows items that are armor.", "Armor3": "Shows items that use the Armor 3.0 stat system introduced in Edge of Fate.", "ArmorCategory": "Shows armors based on their category.", "ArmorIntrinsic": "Shows legendary armor which has an intrinsic perk, such as Artifice Armor.", "Artifice": "Shows Artifice armor.", "Ascended": "Shows items that have an ascend node which have been ascended.", "Breaker": "Filter by breaker type or corresponding champion type. breaker:instrinsic shows items with intrinsic breaker ability.", "BulkClear_one": "Removed tag from 1 item.", "BulkClear_other": "Removed tags from {{count}} items.", "BulkRevert_one": "Reverted tag on 1 item.", "BulkRevert_other": "Reverted tags on {{count}} items.", "BulkTag_one": "Tagged selected item as {{tag}}.", "BulkTag_other": "Tagged {{count}} selected items as {{tag}}.", "Catalyst": "Shows catalysts based on their status. catalyst:complete shows catalysts you have completed and applied, catalyst:incomplete shows catalysts you have unlocked but either not completed the objective or applied the catalyst, and catalyst:missing shows items that can have a catalyst but you haven't found it yet.", "Class": "Shows items based on their class affinity.", "Combine": "Filters can be combined or grouped with parentheses, \"or\" and \"and\" to narrow down your search, for example \"{{example}}\".", "ContributePower": "Shows items that have power and can contribute to your power level.", "Cosmetic": "Shows items that are flair or cosmetic.", "Craftable": "Shows items that are craftable.", "CraftedDupe": "Shows duplicate weapons where at least one of the duplicates is crafted.", "Curated": "Shows items that are a curated roll.", "CurrentClass": "Shows items that are equippable on the currently logged in guardian.", "CustomStatLower": "Shows armor whose stats are strictly lower than another of the same type of armor, only taking into account stats that are in any of that class' custom stat total list.", "DamageType": "Shows items based on their damage type.", "Deepsight": "Shows weapons with Deepsight Resonance, which can have their pattern extracted, or which can have Deepsight Resonance enabled using a Deepsight Harmonizer.", "Deprecated": "This filter is no longer supported.", "Description": "Description", "DescriptionFilter": "Shows items whose description has a partial match to the filter text. Search for entire phrases using quotes.", "DisabledModSlot": "Shows items with a disabled mod.", "Dupe": "Shows duplicate items, including reissues", "DupeArchetype": "Groups armor with the same stat Archetype.", "DupeCount": "Items that have the specified number of duplicates.", "DupeLower": "Duplicate items, including reissues, that are not the highest power dupe. Only one duplicate is chosen as the highest, and the rest are considered lower.", "DupePerks": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "DupeSetBonus": "Groups armor with the same set bonus.", "DupeStats": "Shows armor with identical base stats, and matching stat adjustment mods like Artifice or Tuners.", "DupeTertiary": "Groups armor with the same tertiary stat.", "DupeTraits": "Weapons whose traits are either a duplicate of, or a subset of, another weapon of the same type.", "DupeTunedStat": "Groups armor with the same Tuned stat.", "DupeUntunedStats": "Groups armor with identical base stats, ignoring stat adjustment mods.", "DupeZeroStats": "Groups armor with the same 3 non-zero base stats.", "Energy": "Shows items that use the Armor 2.0 mod system introduced in Shadowkeep.", "EnergyCapacity": "Shows items based on their current energy capacity.", "Engrams": "Shows engrams.", "Enhanceable": "Shows weapons that can be enhanced.", "Enhanced": "Shows weapons based on their enhancement tier.", "EnhancedPerk": "Shows weapons that have the specified number of enhanced perk columns.", "EnhancementReady": "Shows weapons that have reached level thresholds for perk enhancement.", "Equipment": "Items that can be equipped.", "Equipped": "Items that are currently equipped on a character.", "Event": "Shows items from which event in Destiny 2 they appeared in.", "ExtraPerk": "Shows random-rolled Legendary weapons with an additional selectable perk.", "Featured": "Items that count as one of the \"New Gear\" or \"Featured Items\" in the current season.", "Filter": "Filter", "FilterWith": "Filter with:", "Focusable": "Shows items that can be focused at a vendor", "Foundry": "Shows items by which foundry created them.", "Glimmer": "Shows items that are consumables that are related to gaining glimmer.", "Harrowed": "\\(Harrowed\\)", "HasNotes": "Show items that have notes applied.", "HasOrnament": "Shows items that have an ornament applied.", "HasShader": "Shows items that have a shader applied.", "Holofoil": "Shows holofoil weapons.", "InDimLoadout": "is:indimloadout shows items that are included in any DIM loadout.", "InInGameLoadout": "is:iningameloadout shows items that are included in any in-game loadout.", "InInventory": "Shows items that you have at least one copy of in your inventory. Only really useful in the Vendors and Records screens.", "InLoadout": "is:inloadout shows items that are included in any loadout. Searching with inloadout: shows items that are included in loadouts with matching titles. When used with a hashtag, inloadout: shows items whose loadouts have the hashtag in the title or notes. When used with a range, it shows items that are in that many loadouts.", "Infusable": "Shows items that can be infused.", "InfusionFodder": "Shows items that could be infused into lower-power versions of the same item for only glimmer.", "IsAdept": "Shows weapons compatible with Adept mods.", "IsCrafted": "Shows weapons that have been crafted.", "ItemHash": "Shows the items with the given inventory item hash. For advanced users.", "ItemId": "Shows the item with the given inventory item ID. For advanced users.", "Leveling": { "Complete": "{{term}} - shows items that are totally complete - every upgrade unlocked.", "Incomplete": "{{term}} - shows items that are not complete - there's still at least one upgrade to unlock.", "NeedsXP": "{{term}} - shows items that can still have XP put into them.", "Upgraded": "{{term}} - shows items that have enough XP to unlock all their nodes, but not all the nodes have been unlocked.", "XPComplete": "{{term}} - shows items that cannot have XP put into them (whether or not their upgrades have been unlocked)." }, "Location": "Shows items based on their location within the app. left/middle/right are the visual location of the char, and while inleftchar will always work, the other two are based on how many characters you have. current is your last/current logged char (that is marked with a yellow triangle).", "LockAllFailed": "Failed to lock items", "LockAllSuccess": "Locked {{num}} items", "Locked": "Shows items based on their locked status.", "Masterwork": "Shows items based on their masterwork stat or masterwork level.", "MasterworkKills": "Shows items based on their masterwork kill tracker count.", "MaxPower": "Shows the items at the highest power per slot.", "MaxPowerLoadout": "Shows the items in the loadout that would maximize your Power Level for each character class.", "Memento": "Shows weapons that have a memento socket.", "ModSlot": "Shows armor with a specific mod type slot.", "Mods": { "Y3": "Shows items with any mods applied." }, "Name": "Shows items whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.", "NamedStat": "Shows armor that has points in the named stat.", "Negate": "To negate a search, prefix that search term with a minus sign or the word \"not\", for example \"{{notexample}}\" or \"{{notexample2}}\".", "NewItems": "Shows new items.", "Notes": "Search for items that you have tagged with custom notes.", "OriginTrait": "Shows weapons that have an origin trait perk.", "Ornament": "Shows items with ornaments and filters for their status.", "PartialMatch": "Shows items where their name, description, any perk, or any mod has a partial match to the filter text. Search for entire phrases using quotes.", "PatternUnlocked": "Shows items that have a crafting pattern unlocked, even if the item itself isn't crafted.", "Perk": "Shows items where one of their perks or mods has a partial match to the filter text in their name or description. Search for entire phrases using quotes.", "PerkName": "Shows items with a perk or mod whose name matches (exactperk:) or partially matches (perkname:) the filter text. Search for entire phrases using quotes.", "PinnacleReward": "Shows pursuits which produce a pinnacle reward.", "Postmaster": "Items that are currently in the Postmaster.", "PowerKeywords": "Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.", "PowerLevel": "Shows items based on their power level. $t(Filter.PowerKeywords)", "PowerfulReward": "Shows pursuits which produce a powerful reward.", "PrismaticDamageType": "Shows items based on if they are a light or darkness damage type. Light types are arc, solar, and void. Darkness types are stasis and strand.", "Quality": "Shows items based on their total stat quality percentage. '{{percentage}}' is an alias for '{{quality}}'.", "RandomRoll": "Shows items that drop with random rolls.", "RarityTier": "Shows items based on their rarity tier.", "Reforgeable": "Shows items that can be reforged at the Gunsmith.", "Release": "Shows items available from a specific release or event.", "RequiredLevel": "Shows items based on their required level.", "RetiredPerk": "Shows weapons with perks that no longer obtainable.", "SearchPrompt": "Search available filter commands", "Season": "Shows items from which season of Destiny 2 they appeared in.", "StackFull": "Show items which are at-capacity for their stack (Enhancement Cores, Strange Coins, Gunsmith Materials etc)", "StackLevel": "Shows items based on the quantity of items in its stack.", "Stackable": "Shows items that can stack (ammo synths, strange coin, etc)", "StatLower": "Shows armor whose stats are strictly lower than another of the same type of armor.", "Stats": "Shows items based on a specific stat value. $t(Filter.StatsExtras)", "StatsBase": "Filters armor based on its base stat value, not including attached mods or masterworking. $t(Filter.StatsExtras)", "StatsExtras": "Supports stat addition by connecting multiple stat names with the + or & symbol. There are also special keywords highest, secondhighest, thirdhighest, etc. which match stats based on their rank within an item's stats. Each custom stats also has its own search term, shown in the Custom Stats settings.", "StatsLoadout": "Finds a set of items to equip for the maximum total value of a specific stat.", "StatsMax": "Finds armor with the highest number for a specific stat. Includes all items with the highest stat.", "StatsOrdinal": "Finds armor 3.0 with the specified stat focusing.", "Tags": { "Tag": "Shows items that have a specific tag.", "Tagged": "Shows items that have any tag." }, "Tier": "Shows items based on their tier from 0-5.", "Timelost": "\\(Timelost\\)", "Tracked": "Shows quests/bounties based on their tracked status.", "Transferable": "Items that can be moved between characters.", "Trashlist": "Shows items that match your wish list's trash list.", "TunedStat": "Shows items with tuning mods for the specified stat.", "Unascended": "Shows items that have an ascend node which have not been ascended.", "Undo": "Undo", "UnlockAllFailed": "Failed to unlock items", "UnlockAllSuccess": "Unlocked {{num}} items", "Vendor": "Item is available from a specific vendor.", "VendorItem": "Item is from a vendor, not in your inventory. Useful for excluding vendor items from Loadout Optimizer.", "Weapon": "Shows items that are weapons.", "WeaponLevel": "Shows weapons based on their Weapon Level.", "WeaponType": "Shows weapons based on their weapon type.", "Wishlist": "Shows items that match your wish list.", "WishlistDupe": "Shows duplicate items where at least one of the duplicates is on your wish list.", "WishlistEnabled": "Shows items that are eligible to have wish list rolls.", "WishlistNotes": "Shows wish listed items whose notes match the search.", "WishlistUnknown": "Shows items with no roll recommendations in the loaded wish lists.", "Year": "Shows items from which year of Destiny they appeared in." }, "General": { "ClickForDetails": "Click for details", "Close": "Close", "Confirm": "Confirm?", "UserGuideLink": "User guide" }, "Glyphs": { "Axe": "Axe", "DarkAbility": "Darkness Ability", "Gilded": "Gilded", "Harmonic": "Harmonic", "HiveSword": "Hive Sword", "LightAbility": "Light Ability", "LightLevel": "Light Level", "Misadventure": "Misadventure", "Missing": "Missing", "OpenSymbolsPicker": "Open Symbols Picker", "Prismatic": "Prismatic", "Quickfall": "Quickfall", "RespawnRestricted": "Respawn Restricted", "ScorchCannon": "Scorch Cannon", "SearchSymbols": "Search Symbols...", "Smoke": "Smoke" }, "Header": { "About": "About DIM", "AutoRefresh": "DIM will automatically reload as long as you are still playing.", "BulkTag": "Bulk tag items", "BungieNetAlert": "Bungie Alert", "Clear": "Clear search filter", "CompareMatching": "Compare Items", "DeleteSearch": "Delete Search", "FilterHelp": "Search item/perk, {{example}}, and more", "FilterHelpBrief": "Search items", "FilterHelpLoadouts": "Search loadout names and notes", "FilterHelpMenuItem": "Filters Help...", "FilterHelpOptimizer": "Filter armor included in builds, e.g.: {{example}}", "FilterHelpProgress": "Search milestones and bounties", "FilterHelpRecords": "Search triumphs and collections", "FilterMatchCount_one": "1 item", "FilterMatchCount_other": "{{count}} items", "Filters": "Filters", "InstallDIM": "Install as an App", "InstallDIMBanner": "Install DIM as an app on your home screen", "Inventory": "Inventory", "IosPwaPrompt": "In Safari, click the share icon (middle button on the bottom) and select \"Add to Home Screen\".", "KeyboardShortcuts": "Keyboard Shortcuts", "LaunchDIMAlone": "Separate Window", "MaterialCounts": "Material Counts", "Menu": "Menu", "ProfileAge": "Destiny servers last sent updated data {{age}} ago.\nRefreshing from DIM may get newer data, but Bungie.net may also repeat cached information.", "Refresh": "Refresh Destiny Data [R]", "ReloadApp": "Reload App", "ReportBug": "Report a Bug", "SaveSearch": "Save Search", "SearchActions": "Open Search Actions", "SearchResults": "Show Items", "Shop": "Shop", "TagAs": "Tag as '{{tag}}'", "UpgradeDIM": "Update DIM", "WhatsNew": "What's New" }, "Help": { "CannotMove": "Cannot move that item off this character.", "NoStorage": "DIM can't store data", "NoStorageMessage": "DIM can't store data in your browser. This can be caused by browsing in private or incognito mode, or when you have low disk space, or a browser bug. Try restarting your computer! You won't be able to log in to or use DIM until you fix this." }, "Hotkey": { "Armory": "Show Armory for an item", "CheatSheetTitle": "Keyboard Shortcuts:", "ClearDialog": "Dismiss dialog", "ClearNewItems": "Clear new items", "Enter": "ENTER", "ItemPopupTab": "Switch item details tab", "LockUnlock": "Lock or unlock an item", "MarkItemAs": "Mark item as '{{tag}}'", "Menu": "Toggle menu", "Note": "Enter notes", "Pull": "Pull item to active character", "RefreshInventory": "Refresh inventory", "ShowHotkeys": "Show keyboard shortcuts", "StartSearch": "Start a search", "StartSearchClear": "Start a fresh search", "Tab": "TAB", "Vault": "Send item to vault" }, "InGameLoadout": { "ClearSlot": "Clear Slot {{index}}", "Create": "Create Loadout", "CreateTitle": "Create In-Game Loadout From Current Equipment", "CurrentlyEquipped": "Currently Equipped", "DeleteFailed": "Failed to delete loadout", "Deleted": "Loadout Deleted", "DeletedBody": "Cleared the in-game loadout at slot {{index}}", "EditFailed": "Failed to update loadout", "EditIdentifiers": "Edit Identifiers", "EditTitle": "Edit Loadout Name and Icon", "EquipNotReady": "In-game Equip Not Ready", "EquipReady": "In-game Equip Ready", "LoadoutDetails": "Loadout Details", "MatchingLoadouts": "Matching Loadouts:", "PrepareEquip": "Prepare Equip", "Replace": "Replace Loadout {{index}}", "Save": "Update Loadout", "SaveIdentifiers": "Update Identifiers", "SnapshotFailed": "Failed to snapshot equipped loadout" }, "Infusion": { "Filter": "Filter items", "InfuseSource": "Select item to infuse {{name}} into", "InfuseTarget": "Select item to infuse into {{name}}", "InfusionMaterials": "Infusion Materials", "NoItems": "No infusable items available.", "NoTransfer": "Transfer infusion material\n {{target}} cannot be moved.", "SwitchDirection": "Switch", "TransferItems": "Transfer" }, "Inventory": { "ClickToExpand": "(Click to expand)", "MissingSilver": "Your Silver balance is only available while you are playing the game." }, "Item": { "SetBonus": { "NPiece_one": "{{count}} Piece", "NPiece_other": "{{count}} Piece" }, "ThumbsDown": "Thumbs Down", "ThumbsUp": "Thumbs Up" }, "ItemFeed": { "ClearFeed": "Clear Feed", "Description": "Item Feed", "HideTagged": "Hide Tagged", "NoNewItems": "No new items", "ShowOlderItems": "Show older items" }, "ItemMove": { "Consolidate": "Consolidated {{name}}", "Distributed": "Distributed {{name}}\n {{name}} is now equally divided between characters.", "MovingItem": "Transfer to vault", "MovingItem_female": "Transfer to {{target}}", "MovingItem_male": "Transfer to {{target}}", "ToStore": "All {{name}} are now on your {{store}}.", "ToVault": "All {{name}} are now in your vault." }, "ItemPicker": { "ChooseItem": "Choose an item:", "SearchPlaceholder": "Search items" }, "ItemService": { "BucketFull": { "Guardian": "There are too many '{{itemtype}}' items on your {{store}}.", "Guardian_female": "There are too many '{{itemtype}}' items on your {{store}}.", "Guardian_male": "There are too many '{{itemtype}}' items on your {{store}}.", "Vault": "There are too many '{{itemtype}}' items in the {{store}}." }, "Classified": "This item is classified and cannot be transferred at this time.", "Classified2": "Classified item. Bungie does not yet provide information about this item. Add notes to this item and use the \"notes:\" search filter to find it.", "Deequip": "Cannot find another item to equip in order to deequip {{itemname}}", "ExoticError": "'{{itemname}}' cannot be equipped because the exotic in the {{slot}} slot cannot be unequipped. ({{error}})", "NotEnoughRoom": "There's nothing we can move out of {{store}} to make room for {{itemname}}", "NotEnoughRoomGeneral": "There's not enough room to move this item.", "OnlyEquippedClassLevel": "This can only be equipped on a {{class}} at or above level {{level}}.", "OnlyEquippedLevel": "This can only be equipped on characters at or above level {{level}}.", "PostmasterAlmostFull": "Almost full!", "PostmasterFull": "Full!", "PreviewVendor": "Preview {{type}} contents", "StackFull": "You already have a full stack of {{name}}", "StoreName": "{{genderRace}} {{className}}" }, "KillType": { "ClassAbilities": "Class Ability", "Finisher": "Finisher", "Grenade": "Grenade", "Melee": "Melee", "Precision": "Precision", "Super": "Super" }, "LB": { "AddStack": "Add another copy of this mod", "AdvancedOptions": "Advanced Options", "ChooseAMod": "Choose your mods", "ChooseASetBonus": "Choose your set bonuses", "ChooseAnExotic": "Choose your exotic", "ClearLocked": "Clear Locked", "ContainsVendorItems": "This loadout contains vendor items", "Current": "Current", "Equip": "Equip on {{character}}", "Exclude": "Excluded Items", "ExcludeHelp": "Shift + click an item (or drag and drop into this bucket) to build sets without specific gear.", "ExistingBuildStats": "Existing Build Stats", "ExistingBuildStatsNote": "Only showing builds with strictly higher stats.", "FilterSets": "Filter sets", "Help": { "And": "Armor with all of these perks will be used (\"and\")", "ChangeNodes": "Change the Intellect, Discipline, or Strength nodes in game to what is displayed to create each loadout.", "Discipline": "Discipline speeds up Grenade recharge time", "DragAndDrop": "Drag and drop items into the locked buckets to build sets with only that gear", "Help": "Need help?", "HigherTiers": "Higher Tiers are better", "Intellect": "Intellect speeds up Super recharge time", "Lock": "Lock a set of perks by clicking a lock bucket and selecting perks", "MultiPerk": "To use armor with multiple perks together shift+click the desired perks", "NoPerk": "If a perk doesn't appear it means that you own no armor with that perk", "Or": "Armor with any of these perks will be used (\"or\")", "ShiftClick": "Shift click an item to build sets without that gear", "StatsIncrease": "As an items defense level increases, the stats on that item (int/dis/str) also increase.", "Strength": "Strength speeds up Melee recharge time", "Synergy": "Try to find armor that has ammo increasing perks for weapon types that you use.", "Tier11Example": "4/5/2 (a Tier 11 build) is 4 Intellect, 5 Discipline, 2 Strength (4+5+2 = Tier 11)" }, "HideAllConfigs": "Hide all configurations", "HideConfigs": "Hide configurations", "IncompatibleWithOptimizer": "This item is incompatible with the Optimizer. Please reacquire a new version from Collections.", "LB": "Loadout Optimizer", "LightMode": { "HelpCurrent": "Calculates loadouts at current defense levels.", "HelpScaled": "Calculates loadouts as if all items were 350 defense.", "LightMode": "Light mode" }, "Loading": "Loading best sets", "LockEquipped": "Lock Equipped", "LockPerk": "Lock perk", "Locked": "Locked Items", "LockedHelp": "Drag and drop any item into its bucket to build set with that specific gear. Shift + click to exclude items.", "Missing2": "Missing rare, legendary, or exotic pieces to build a full set!", "ProcessingMode": { "Fast": "Fast", "Full": "Full", "HelpFast": "Only looks at your best gear.", "HelpFull": "Looks at more gear, but takes longer.", "ProcessingMode": "Processing mode" }, "RemoveStack": "Remove a copy of this mod", "Scaled": "Scaled", "SearchAMod": "Search for mod name or description", "SearchASetBonus": "", "SearchAnExotic": "Search for exotic name or description", "SelectExotic": "Select exotic", "SelectMods": "Select Mods", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Activity Mods", "SelectSetBonus": "Select Set Bonuses", "SelectSubclassOptions": "Customize subclass", "ShowAllConfigs": "Show all configurations", "ShowConfigs": "Show configurations", "ShowGear": "{{class}} Armor", "Vendor": "Include Vendor items" }, "Loading": { "Accounts": "Loading Destiny accounts...", "Code": "Loading DIM code...", "FilterHelp": "Loading search help...", "Profile": "Loading Destiny profile...", "Vendors": "Loading Destiny vendors..." }, "LoadoutAnalysis": { "Analyzed": "Analyzed {{numLoadouts}} Loadouts", "Analyzing": "Analyzing {{numAnalyzed}}/{{numLoadouts}} Loadouts", "BetterStatsAvailable": { "Description": "Choosing different armor or mods for this loadout will allow reaching higher stats. Choose \"$t(Loadouts.OpenInOptimizer)\" to view better builds.", "Name": "Better Stats Available" }, "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat to exceed 200. DIM may identify better stats by reducing the amount of excess stats. If this is undesired, disable \"$t(Loadouts.IncludeRuntimeStatBenefits)\" in the Loadout.", "DoesNotRespectExotic": { "Description": "This loadout's Loadout Optimizer settings specify an exotic choice, but the loadout does not match that exotic.", "Name": "Wrong Exotic" }, "DoesNotSatisfyStatConstraints": { "Description": "Loadout Optimizer settings for this Loadout specify stat minimums, but the Loadout does not reach them.", "Name": "Wrong Stat Minimums" }, "EmptyFragmentSlots": { "Description": "There are empty fragment slots in this subclass.", "Name": "Empty Fragment Slots" }, "InvalidMods": { "Description": "Some mods in this loadout are deprecated or do not otherwise fit into any of your armor pieces.", "Name": "Deprecated Mods" }, "InvalidSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer that is not valid.", "Name": "Invalid Search Query" }, "ItemsDoNotMatchSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer, and that search query excludes at least one of the items in the loadout.", "Name": "Search Excludes Items" }, "MissingItems": { "Description": "Some of the items in this loadout are no longer in your inventory.", "Name": "Missing Items" }, "ModsDontFit": { "Description": "Armor in this loadout cannot accommodate all loadout mods, even if the armor was upgraded.", "Name": "Unassigned Mods" }, "NeedsArmorUpgrades": { "Description": "Armor in this loadout needs to be upgraded to accommodate all mods or reach specified stats.", "Name": "Needs Armor Upgrades" }, "NotAFullArmorSet": { "Description": "This loadout could not be analyzed further because it does not include a full set of armor.", "Name": "Not A Full Armor Set" }, "TooManyFragments": { "Description": "There are more fragments configured on the subclass than granted by aspects.", "Name": "Too Many Fragments" }, "UsesSeasonalMods": { "Description": "This loadout relies on mods that are only available in some seasons. When the season ends, some mods will be unavailable or exceed armor energy capacity.", "Name": "Uses Seasonal Mods" } }, "LoadoutBuilder": { "All": "All", "AlwaysAutoMods": "Artifice and Tuning mods will always be chosen automatically.", "AnyExotic": "Any Exotic", "AnyExoticDescription": "Sets must contain an exotic, but any exotic will do.", "Artifice": "Artifice", "AssumeMasterwork": "Assume Masterwork", "AssumeMasterworkOptions": { "All": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nArmor 2.0 Exotics: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Enhanced to accept Artifice stat mods.", "Current": "Current stats, assumed energy level at least {{minLoItemEnergy}}.", "Legendary": "Legendary: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExotic: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Full masterwork stat bonuses, assumed energy level at least 10.", "None": "All armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "Automatically add stat mods", "AutomaticallyPicked": "This mod was added automatically to improve build stats.", "CompareLoadout": "Compare Loadout", "ConfirmOverwrite": "Are you sure you want to replace the armor in the loadout \"{{name}}\" with this new set of armor?", "DecreaseStatPriority": "Decrease stat priority", "DisabledByAutoStatMods": "Stat mods are being chosen automatically by Loadout Optimizer.", "DisabledDueToMaintenance": "The Loadout Optimizer is currently disabled due to Bungie API maintenance.", "EquipItems": "Equip", "ExcludeItem": "Exclude Item", "ExcludeVendors": "Search \"not:vendor\" to exclude vendor items from Loadout Optimizer.", "ExcludedItems": "Excluded Items", "ExistingLoadout": "Existing Loadout", "Exotic": "Exotic Armor", "ExoticClassItemPerks": "If you want specific perks, use searches like exactperk:\"spirit of verity\". Click perks in the Optimizer results to add or remove them from the item filter.", "ExoticSpecialCategory": "Special", "FOTLWildcardWarning": "This set contains a Festival of the Lost mask. Manually apply the correct mod to activate desired set bonuses.", "Filter": "Settings", "IgnoreStat": "If unchecked, Loadout Optimizer will pretend this stat doesn't exist when building sets", "IncreaseStatPriority": "Increase stat priority", "Legendary": "Legendary", "LimitToNewFeaturedGear": "Limit to new/featured gear", "LockItem": "Pin item", "MissingClass": "Build is for: {{className}}", "MissingClassDescription": "The build you're trying to view is for a character class you don't have.", "MwExotic": "Exotic", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "An active search query is restricting the items DIM can include in builds", "AllowAutoStatMods": "Allow DIM to automatically include additional stat mods", "AlwaysInvalidMods": "These mods don't fit into any of your owned items:", "AssumeMasterworked": "Allow DIM to recommend masterworking armor", "AssumptionsRestricted": "DIM cannot recommend armor energy changes:", "BadSlot": "In the {{bucketName}} slot, none of the allowed items could accommodate these mods:", "ExoticDoesNotExist": "You don't have any of the selected exotic armor in your inventory.", "Header": "No builds were found. Here are possible reasons DIM couldn't find any builds:", "LowerBoundsFailed": "Many sets did not meet minimum stat requirements", "MaybeAllowMoreItems": "Consider allowing other items:", "MaybeDecreaseLowerBounds": "Consider reducing minimum stat requirements", "MaybeRemoveMods": "Consider removing some mods:", "MaybeRemoveSearchQuery": "Consider clearing or changing the filter in the search bar", "ModAssignmentFailed": "Many sets could not accommodate all requested mods", "RemoveMods": "Remove these mods", "RemoveSetBonuses": "Consider removing some set bonuses", "SetBonuses": "You have chosen some set bonuses, maybe you don't have the right items to use them." }, "NoExotic": "No Exotic", "NoExoticDescription": "Equivalent to searching \"not:exotic\" in the search bar - sets will not use any exotic armor.", "NoExoticPreference": "No Exotic Selected", "NoExoticPreferenceDescription": "Exotic armor will be used if it maximizes stats.", "NoLoadoutsToCompare": "No loadouts to compare", "None": "None", "OptimizerExplanationGuide": "Read the User Guide for more info and a video tutorial.", "OptimizerExplanationMods": "Choose an exotic, mods, and a subclass. These will contribute stats to the build, while any mods already on the armor are ignored.", "OptimizerExplanationSearch": "Use the search bar to narrow down which armor to consider, e.g. {{example}}. If no armor in a slot matches the search, all items will be considered for that slot.", "OptimizerExplanationStats": "Drag the most important stats to the top, and uncheck stats you don't want to maximize.", "OptimizerSet": "Optimizer Set", "PinnedItems": "Pinned Items", "PinnedItemsFinePrint": "Search filters are saved with Loadout Optimizer settings, but pinned and excluded items are not. Pins and exclusions will be ignored when DIM checks existing Loadouts for better stat builds.", "ProcessingSets": "Finding highest stat sets...", "SaveAs": "Save as", "SetBonus": "Set Bonuses", "SpeedReport": "Evaluated {{combos, number}} combinations in {{time}} seconds using {{cpus}} CPU cores.", "StatConstraints": "Stat Priorities & Ranges", "StatMax": "Max", "StatMin": "Min", "StatRangeTooltip": "With the current min/max setting, loadouts exist which have {{min}} to {{max}} points in this stat. Double-click to set min to {{max}}.", "StatTotal": "Total: {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "Unable to add all mods.", "UnableToAddAllModsBody": "There weren't enough mod slots available to fit {{mods}}.", "UnlockItem": "Unpin Item" }, "LoadoutFilter": { "Contains": "Shows loadouts which have an item or a mod matching the filter text. Search for items with spaces in their name using quotes.", "FashionOnly": "Shows loadouts that contain only fashion (shaders or ornaments).", "LoadoutLight": "Shows loadouts based on their calculated light level. Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.", "ModsOnly": "Shows loadouts that only contain armor mods.", "Name": "Shows loadouts whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.", "Notes": "Search for loadouts by their notes field.", "PartialMatch": "Shows loadouts where their name or notes has a partial match to the filter text. Search for entire phrases using quotes.", "Season": "Shows loadouts by which season of Destiny 2 they were last modified in.", "Subclass": "Shows loadouts whose subclass name or damage type partially matches the filter text." }, "Loadouts": { "Abilities": "Abilities", "Actions": "Actions for {{title}}", "AddEquippedItems": "Add Equipped", "AddNotes": "Add Notes", "AddUnequippedItems": "Add Unequipped", "Any": "Any class", "Apply": "Apply", "ApplyInGameLoadoutInGame": "Your loadout is ready to equip but since you're in an activity you need to equip it in-game.", "ApplyMods": "Applying mods", "ApplySearch": "Transfer search \"{{query}}\"", "ArmorStats": "Armor Stats", "ArtifactUnlocks": "Artifact Unlocks", "ArtifactUnlocksDesc": "Due to Bungie.net limitations, DIM cannot automatically configure your artifact. You need to perform these unlocks in-game before applying the Loadout.", "ArtifactUnlocksWithSeason": "Artifact Unlocks – S{{seasonNumber}}", "BadLoadoutShare": "Unable to load shared loadout", "BadLoadoutShareBody": "The loadout you're trying to load is invalid: {{error}}", "Before": "Before '{{name}}'", "CancelEditing": "Cancel Editing", "CannotCustomizeSubclass": "This subclass cannot be configured", "ChooseItem": "Add {{name}}", "ClassType": "Any class loadout", "ClassTypeMismatch": "A {{className}} item cannot be added to this loadout", "ClassTypeMissing": "You do not have a {{className}} to create a loadout for", "ClassType_female": "{{className}} loadout", "ClassType_male": "{{className}} loadout", "Classified": "Some of your items are classified, and cannot be included in the max power calculation.", "ClearLoadoutParameters": "Remove Loadout Optimizer settings", "ClearSection": "Remove all", "ClearSpace": "Move others away", "ClearSpaceArmor": "Move other armor away", "ClearSpaceWeapons": "Move other weapons away", "ClearUnsetMods": "Remove other mods", "ClearingSpace": "Moving other items away", "CopyAndEdit": "Edit Copy", "Create": "Create Loadout", "CurrentlyEquipped": "Currently Equipped", "Deequip": "De-equipping items from other characters", "Delete": "Delete", "DimLoadouts": "DIM Loadouts", "Edit": "Edit Loadout", "EditBrief": "Edit", "EquipInGameLoadout": "Equipping in-game loadout", "EquipItems": "Equipping items", "EquippableDifferent1": "Multiple Exotic items were used to calculate your Maximum Power, so the number shown may not be achievable when equipping your items in-game.", "EquippableDifferent2": "Maximum Power isn't limited by the \"One Exotic\" rule when determining the Power of your drops, powerful, and pinnacle rewards.", "Failed": "Loadout failed to apply completely", "Fashion": "Choose fashion", "FashionOnly": "Fashion-only", "FillFromEquipped": "Fill in using equipped", "FillFromInventory": "Fill in using non-equipped", "FilteredItems": "Filtered Items", "FindAnother": "Find another {{name}}", "FromEquipped": "Equipped", "Generated": "{{statTotal}} Stat Point Loadout", "HashtagTip": "Tip: Use #hashtags in your loadout names or notes and they'll show up here.", "Import": { "BadURL": "Not a valid loadout share URL.", "Error": "Error getting loadout:", "Error404": "This loadout doesn't exist.", "PasteHere": "Paste a loadout link to open the loadout." }, "ImportLoadout": "Import Loadout", "InGameActions": "In-Game Loadout Actions", "InGameLoadouts": "In-Game Loadouts", "IncludeRuntimeStatBenefits": "Include Font mod stats", "IncludeRuntimeStatBenefitsDesc": "\"Font of ...\" armor mods provide a flat boost to character stats while you have Armor Charges.\n\nWith this setting, DIM considers these mods active and adds their benefits to this Loadout's stats in calculations and optimizations.", "ItemErrorSummary_one": "1 item error:", "ItemErrorSummary_other": "{{count}} item errors:", "ItemLeveling": "Item Leveling", "LoadoutName": "Loadout name", "LoadoutParameters": "Loadout Optimizer settings", "LoadoutParametersExotic": "Loadout must include this exotic: {{exoticName}}", "LoadoutParametersQuery": "Items must match this search filter", "LoadoutParametersStats": "Stat priorities and minimum/maximum stat ranges", "Loadouts": "Loadouts", "MakeRoom": "Make Room for Postmaster", "MakeRoomDone_female_one": "Finished making room for 1 Postmaster item by moving 1 item off of {{store}}.", "MakeRoomDone_female_other": "Finished making room for {{count}} Postmaster items by moving {{movedNum}} items off of {{store}}.", "MakeRoomDone_male_one": "Finished making room for 1 Postmaster item by moving 1 item off of {{store}}.", "MakeRoomDone_male_other": "Finished making room for {{count}} Postmaster items by moving {{movedNum}} items off of {{store}}.", "MakeRoomDone_one": "Finished making room for 1 Postmaster item by moving 1 item off of {{store}}.", "MakeRoomDone_other": "Finished making room for {{count}} Postmaster items by moving {{movedNum}} items off of {{store}}.", "MakeRoomError": "Unable to make room for all Postmaster items: {{error}}.", "ManageLoadouts": "Manage Loadouts", "MaxSlots": "You can only have {{slots}} {{bucketName}} in a loadout.", "MaximizeLight": "Max Light", "MaximizePower": "Max Power", "MaximizeStat": "Maximize Stat", "MissingItemsWarning": "Some of the items in this loadout are no longer in your inventory.", "ModErrorSummary_one": "1 mod error:", "ModErrorSummary_other": "{{count}} mod errors:", "ModPlacement": { "InvalidMods": "Invalid Mods", "InvalidModsDesc_one": "1 mod cannot fit into any armor piece.", "InvalidModsDesc_other": "{{count}} mods cannot fit into any armor piece.", "ModPlacement": "Mod Placement", "StackableMod": "Stackable", "UnassignedMods": "Unassigned Mods", "UnassignedModsDesc_one": "1 mod did not fit due to insufficient energy capacity or mod slots. Energy upgrades to the selected armor will not fix the issue.", "UnassignedModsDesc_other": "{{count}} mods did not fit due to insufficient energy capacity or mod slots. Energy upgrades to the selected armor will not fix the issue.", "UnstackableMod": "Not Stackable", "UpgradeCosts": "Upgrade Costs", "UpgradeCostsDesc": "Some armor needs energy capacity upgrades to fit the requested mods. In total, these upgrades cost:" }, "Mods": "Mods", "ModsOnly": "Mods-only", "MoveItems": "Moving items", "NoSpace": "You're out of space in the vault and any other characters.", "NoneMatch": "None of your loadouts matched the filters.", "NotStarted": "Waiting for other actions to complete, or an inventory refresh to finish loading", "NotesPlaceholder": "Write some notes about this loadout, or use #hashtags to categorize it", "NotificationTitle": "Loadout: {{name}}", "OnWrongCharacterAdvice": "Click here to find this character's highest Power items.", "OnWrongCharacterWarning": "This character's most powerful armor is on another character. To count toward the Power of drops, powerful, and pinnacle rewards, armor must be on this character or in the Vault.", "OnlyItems": "Only equippable items, materials, and consumables can be added to a loadout.", "OpenInOptimizer": "Optimize Armor", "OpenOnStreamDeck": "Open on Stream Deck", "PickArmor": "Pick Armor", "PickMods": "Add armor mods", "Prismatic": { "Aspect": "Prismatic Aspect", "Grenade": "Prismatic Grenade", "Melee": "Prismatic Melee", "Super": "Super Ability" }, "PullFromPostmaster": "Collect Postmaster", "PullFromPostmasterError": "Unable to pull from Postmaster: {{error}}.", "PullFromPostmasterGeneralError": "Unable to pull all items from Postmaster.", "PullFromPostmasterNotification_female_one": "Pulling 1 Postmaster item to {{store}}.", "PullFromPostmasterNotification_female_other": "Pulling {{count}} Postmaster items to {{store}}.", "PullFromPostmasterNotification_male_one": "Pulling 1 Postmaster item to {{store}}.", "PullFromPostmasterNotification_male_other": "Pulling {{count}} Postmaster items to {{store}}.", "PullFromPostmasterNotification_one": "Pulling 1 Postmaster item to {{store}}.", "PullFromPostmasterNotification_other": "Pulling {{count}} Postmaster items to {{store}}.", "PullFromPostmasterPopupTitle": "Pull from Postmaster", "Random": "Random", "Randomize": "Randomize Loadout", "RandomizeButton": "Randomize", "RandomizeNew": "Create Random", "RandomizeQueryHint": "Tip: Search for items first to restrict what items can be randomly chosen.", "RandomizeSearch": "Randomize from Search", "RandomizeSearchPrompt": "Randomize your equipped items from search \"{{query}}\"?", "Redo": "Redo", "RestoreAllItems": "All Items", "SalvationsEdgeMods": "Salvation's Edge Mods", "Save": "Save", "SaveAsDIM": "Save as DIM Loadout", "SaveAsNew": "Save as New", "SaveAsNewTooltip": "Keep the original loadout and save this as a new loadout", "SaveDisabled": { "AlreadyExists": "Choose a new name for the loadout.", "Empty": "The loadout is empty.", "NoName": "The loadout needs a name." }, "SaveLoadout": "Save Loadout", "Season": "Season {{season}}", "SetBonusesDesc": "Required set bonuses", "Share": { "Copied": "Copied loadout link to clipboard", "CopyButton": "Copy Link", "Error": "Error getting share link", "Fashion": "Fashion (shaders & ornaments)", "LoadoutOptimizer": "Loadout Optimizer settings", "NativeShare": "Share Link", "Notes": "Notes", "NumItems_one": "{{count}} item - recipients will be prompted to select a comparable item from their inventory", "NumItems_other": "{{count}} items - recipients will be prompted to select comparable items from their inventory", "NumMods_one": "{{count}} mod", "NumMods_other": "{{count}} mods", "Placeholder": "Loading share link", "Subclass": "Subclass customization", "Summary": "Share this loadout containing:", "Title": "Share \"{{name}}\"" }, "ShareLoadout": "Share", "ShowModPlacement": "Show Mod Placement", "Snapshot": "Save As In-Game Loadout", "SocketOverrides": "Changing subclass options", "SortByEditTime": "Sort by last edited", "SortByName": "Sort by name", "SubclassOptions": "{{subclass}} options", "SubclassOptionsSearch": "Search {{subclass}} options", "Succeeded": "Loadout succeeded", "SyncFromEquipped": "Sync from equipped", "TooManyRequested": "You have {{total}} {{itemname}} but your loadout asks for {{requested}}. We transferred all you had.", "TuningMods": "Tuning Mods", "UnassignedModError": "Mod didn't fit on your current armor", "Undo": "Undo", "Update": "Save Changes", "UpdateLoadout": "Update Loadout", "VendorsCannotEquip": "You don't have these items. Tap to pick a replacement or click the X to remove:" }, "Manifest": { "Download": "Downloading latest Destiny info database from Bungie...", "Error": "Error loading Destiny info database:\n{{error}}\nReload to retry.", "Load": "Loading Destiny info database..." }, "Milestone": { "Daily": "Daily Challenge", "OneTime": "One Time Challenge", "SeasonalRank": "Seasonal Rank {{rank}}", "Special": "Special Event Challenge", "Tutorial": "Tutorial Challenge", "Unknown": "Challenge", "Weekly": "Weekly Challenge" }, "Mods": { "HarmonicModDescription": "This mod's effect comes at a reduced cost and changes element depending on the equipped subclass." }, "MoveAmount": { "Amount": "Amount:" }, "MovePopup": { "Acquired": "This item is unlocked in collections.", "AcquiredMod": "This mod is unlocked in collections.", "AddNote": "Add notes", "AddToLoadout": "Loadout", "AddToLoadoutTitle": "Add this to a loadout", "All": "All", "ArtifactBreaker": "This weapon has {{breaker}} because of an unlocked artifact perk.", "CannotCurrentlyRoll": "This perk cannot be rolled on the current version of this item.", "CantPullFromPostmaster": "You must visit the postmaster in game to retrieve this item.", "CatalystProgress": "Catalyst Progress", "CommunityData": "Community Insight", "Consolidate": "Consolidate", "DistributeEvenly": "Distribute Evenly", "EnhancementTier": "Tier {{tier}}", "Equip": "Equip on:", "EquipWithName": "Equip on {{character}}", "FavoriteUnFavorite": { "Favorite": "Favorite {{itemType}}", "Favorited": "Favorited", "Unfavorite": "Unfavorite {{itemType}}", "Unfavorited": "Unfavorited" }, "Infuse": "Infuse", "InfuseTitle": "Open the infusion fuel finder", "IntrinsicBreaker": "This weapon intrinsically has {{breaker}}.", "LoadingSockets": "Perk and stat details have not loaded yet for this item.", "LockUnlock": { "AutoLock": "Lock state is synced to this item's tag", "Lock": "Lock {{itemType}}", "Locked": "Locked", "Unlock": "Unlock {{itemType}}", "Unlocked": "Unlocked" }, "MissingSockets": "Perk and mod details are unavailable while Bungie is updating their services. It will return when they are done, usually in a few hours.", "Notes": "Notes:", "OpenOnStreamDeck": "Open on Stream Deck", "OverviewTab": "Overview", "Owned": "This item is in your inventory.", "OwnedMod": "This mod is in your modifications inventory.", "PullItem": "Pull from {{bucket}} to {{store}}", "PullPostmaster": "Pull from Postmaster", "ReadLore": "Read lore on Ishtar Collective", "ReadLoreLink": "Read lore", "Rewards": "Rewards:", "SendToVault": "Send to Vault", "Store": "Pull to:", "StoreWithName": "Pull to {{character}}", "Subtitle": { "QuestProgress": "Step {{questStepNum}} of {{questStepsTotal}}", "Type": "{{classType}} {{typeName}}" }, "TabList": "Item detail tabs", "ToggleSidecar": "Expand or collapse item actions", "TrackUntrack": { "Track": "Track {{itemType}}", "Tracked": "Tracked", "Untrack": "Untrack {{itemType}}", "Untracked": "Untracked" }, "TriageTab": "Triage", "UnreliablePerkOption": "This perk appears only in the collections view. It might not roll randomly on this item.", "Vault": "Vault", "WeaponLevel": "Weapon Level {{level}}" }, "Notes": { "Error": "Error! Max 120 characters for notes.", "Help": "Add notes, #hashtags, and :symbols:" }, "Notification": { "Cancel": "Cancel", "OK": "Dismiss" }, "Objectives": { "Complete": "Complete", "Incomplete": "Incomplete" }, "Organizer": { "BulkMove": "Move To", "BulkMoveLoadoutName": "Selected in Organizer", "BulkTag": "Tag", "Columns": { "Ammo": "Ammo", "Archetype": "Archetype", "BaseStats": "Base Stats", "Breaker": "Breaker", "Crafted": "Shaped Date", "CustomTotal": "Custom Total", "Damage": "Damage", "Energy": "Energy", "Event": "Event", "Featured": "New Gear", "Foundry": "Foundry", "Frame": "Frame", "Harmonizable": "Harmonizable", "Holofoil": "Holofoil", "Icon": "Icon", "ItemTier": "Tier", "KillTracker": "Kills", "Level": "Level", "Loadouts": "Loadouts", "Location": "Location", "Locked": "Locked", "MasterworkStat": "MW Stat", "MasterworkTier": "MW Tier", "ModSlot": "Mod Slot", "Mods": "Mods", "Name": "Name", "New": "New", "Notes": "Notes", "OriginTraits": "Origin Trait", "OtherPerks": "Weapon Components", "PercentComplete": "% Complete", "Perks": "Perks", "PerksGrid": "Perks Grid", "Power": "Power", "Quality": "Quality %", "Recency": "Recency", "Season": "Season", "Shaders": "Cosmetics", "Source": "Source", "StatQuality": "Stat Quality", "StatQualityStat": "{{stat}}%", "Stats": "Stats", "Tag": "Tag", "TertiaryStat": "3rd Stat", "Tier": "Rarity", "Traits": "Weapon Traits", "TuningStat": "Tuner", "WishList": "Wish List", "WishListNotes": "Wish List Notes", "Year": "Year" }, "EnabledColumns": "Enabled Columns", "Lock": "Lock", "NoItems": "No items match the filters. If you have a search query, try clearing it.", "NoMobile": "Turn your phone sideways to use the Organizer.", "Note": "Set Notes", "OpenIn": "Show in Organizer", "Organizer": "Organizer", "SelectAll": "Select All", "SelectItem": "Select or unselect {{name}}", "ShiftTip": "Tip: Hold the Shift key and click on a cell to filter items", "Stats": { "Aim": "Aim", "Airborne": "Airborne", "AmmoGeneration": "Ammo Gen", "Power": "Power", "RPM": "RPM", "Recoil": "Recoil", "Reload": "Reload" }, "Unlock": "Unlock" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "The postmaster is almost full! ({{number}}/{{postmasterSize}})", "PostmasterFull": "The postmaster is full! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "Bounties", "CatalystSource": "Source: {{source}}", "CrucibleRank": "Ranks", "Items": "Quest Items", "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", "PercentMax": "{{pct}}% to maximum", "PercentPrestige": "{{pct}}% to reset", "PointsUsed_one": "1 point used", "PointsUsed_other": "{{count}} points used", "PowerBonusHeader": "+{{powerBonus}} Power Rewards", "PowerBonusHeaderUndefined": "Other Rewards", "Progress": "Progress", "QueryFilteredTrackedTriumphs": "None of your tracked triumphs matched the search", "QuestExpired": "Expired", "QuestExpires": "Expires in ", "Quests": "Quests", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}pts", "Resets_one": "1 reset", "Resets_other": "{{count}} resets", "RewardPassEndsIn": "Reward Pass ends in ", "RewardPassPrestigeRank": "Prestige Rank {{rank}}", "SeasonalHub": "Seasonal Hub", "StatTrackers": "Stat Trackers", "TrackedTriumphs": "Tracked Triumphs" }, "RecordBooks": { "HideCompleted": "Hide completed records", "RecordBooks": "Record Books" }, "Records": { "Title": "Records", "UniversalOrnamentSetOther": "Other" }, "SearchHistory": { "Date": "Last Used", "DeleteAll": "Delete all non-starred searches", "Description": "These are all your past and saved searches. You can delete them from here.", "Item": "Item Searches", "Link": "View and edit search history", "Loadout": "Loadout Searches", "Query": "Search", "Title": "Search History", "UsageCount": "# Used" }, "Settings": { "Appearance": "Appearance", "ArmorArchetypeModslot": "Armor Archetype / Modslot", "AutoLockTagged": "Sync item lock state with tag", "AutoLockTaggedExplanation": "DIM will automatically lock and unlock items to match their tag. Crafted items will remain unlocked to allow reshaping. When this setting is enabled, the lock icon will not be shown on the item tile for tagged items.", "BadgePostmaster": "Show the number of postmaster items for the current character on app icon", "BadgePostmasterExplanation": "For this to work you must install DIM as an app and your OS must support displaying badges", "BothDescriptions": "Both Descriptions", "BungieDescriptionOnly": "Bungie Descriptions", "CharacterOrder": "Sort characters by", "CharacterOrderFixed": "Character age (buggy on PC)", "CharacterOrderRecent": "Most recent character", "CharacterOrderReversed": "Most recent character (reversed)", "ColumnSize": "{{num}} items", "ColumnSizeAuto": "Auto", "CommunityData": "Community Perk Insights", "CommunityDescriptionOnly": "Community Descriptions", "CsvImport": "Import CSV", "CustomErrorLabel": "A stat name must contain word characters, and be different from other stat names for this Guardian class.", "CustomErrorValues": "Stat weights must be positive numbers.\nAt least 2 stat weights must be above zero.", "CustomStatChooseName": "Choose a Custom Stat name", "CustomStatCreate": "Create a new custom stat", "CustomStatDelete": "Delete this Custom Stat", "CustomStatDeleteConfirm": "Delete this Custom Stat?", "CustomStatDesc1": "Choose desired armor stats to make a custom total stat.", "CustomStatDesc3": "Custom stats will appear in the Item Popup, Organizer, and Compare.", "CustomStatTitle": "Custom Stat Total", "Data": "Spreadsheets", "DefaultItemSizeNote": "An item size of 50px will look the sharpest, without blurring the item picture or text.", "DontForgetDupes": "Don't forget you can search is:dupe to quickly find duplicate items, and you can use the comparison tool or organizer to evaluate related items.", "EnableAdvancedStats": "Show stat quality rating on armor (D1)", "ExpandSingleCharacter": "Show all characters", "ExportLoadoutSS": "Loadout spreadsheets", "ExportLoadoutSSHelp": "Download a CSV list of your DIM Loadouts that can be easily viewed in the spreadsheet app of your choice.", "ExportProfile": "Export API profile response", "ExportSS": "Inventory spreadsheets", "ExportSSHelp": "Download a CSV list of your items that can be easily viewed in the spreadsheet app of your choice.", "HidePullFromPostmaster": "Hide the \"$t(Loadouts.PullFromPostmaster)\" button", "Inventory": "Inventory Display", "InventoryColumns": "Character inventory width", "InventoryColumnsMobile": "Character inventory width on mobile portrait", "InventoryColumnsMobileLine2": "The items will be resized to accommodate the new setting", "InventoryNumberOfSpacesToClear": "Number of empty spaces to make when using Farming Mode", "Items": "Item Display", "Language": "Language", "LogOut": "Log out", "Masterworked": "Masterworked", "MaxParallelCores": "Maximum cores for parallel tasks", "MaxParallelCoresExplanation": "Controls how many CPU cores DIM can use for intensive tasks like Loadout Optimizer and Loadout Analyzer. Higher values may improve performance but use more system resources.", "OrnamentDisplay": "Show Ornaments on item tiles", "OrnamentDisplayExplanationDisabled": "Items will never display their ornaments", "OrnamentDisplayExplanationEnabled": "Hovering or long-pressing armor will hide its ornament", "OrnamentDisplayExplanationHide": "Hovering or long-pressing an item will hide its ornament", "OrnamentDisplayExplanationShow": "Hovering or long-pressing an item will show its ornament", "ResetToDefault": "Reset", "RestoreVaultSide": "Show vaulted items in their own column", "ReverseSort": "Toggle forward/reverse sort", "SetSort": "Sort items by:", "SetVaultWeaponGrouping": "Group vault weapons by:", "Settings": "Settings", "ShowNewItems": "Show a red dot on new items", "SingleCharacter": "Single-Character View", "SingleCharacterExplanation": "DIM will show only the most recently played character.\nItems held by hidden characters will appear in the vault, if they can be used by the current character.\nItems specific to other classes will be hidden entirely.", "SizeItem": "Item size", "SortByAmmoType": "Ammo Type", "SortByAmount": "Stack Size", "SortByClassType": "Required Class", "SortByCrafted": "Crafted (D2)", "SortByDeepsight": "Deepsight (D2)", "SortByFeatured": "New Gear / Featured (D2)", "SortByPrimary": "Power level", "SortByRarity": "Rarity", "SortByRating": "Armor Quality (D1)", "SortByRecent": "Recently Acquired (D2)", "SortBySeason": "Season (D2)", "SortByTag": "Tag ({{taglist}})", "SortByTier": "Tier (D2)", "SortByType": "Type", "SortByWeaponElement": "Damage Type", "SortCustom": "Custom Sort", "SortName": "Name", "SpacesSize_one": "{{count}} space", "SpacesSize_other": "{{count}} spaces", "Theme": "Theme", "Troubleshooting": "Troubleshooting", "VaultArmorGroupingStyle": "Separate armor on different lines by class", "VaultGroupingNone": "None", "VaultUnder": "Show vaulted items under equipped items", "VaultWeaponGroupingStyle": "Separate weapon groups on different lines", "WeaponFrame": "Weapon Frame", "WishlistRefreshNotificationBody": "If you do not see any updates, be sure the source (such as GitHub) reflects them!", "WishlistRefreshNotificationTitle": "Wishlists Reloaded" }, "Sockets": { "ApplyPerks": "Apply Perks", "GridStyle": "Display perks as a grid", "Insert": { "Ability": "Equip Ability", "Aspect": "Insert Aspect", "Fragment": "Insert Fragment", "Mod": "Insert Mod", "Ornament": "Apply Ornament", "Projection": "Apply Ghost Projection", "Shader": "Apply Shader", "Super": "Equip Super", "Transmat": "Apply Transmat Effect" }, "ListStyle": "Display perks as a list", "Search": "Search names or descriptions", "Select": { "Ability": "Preview Ability", "Aspect": "Preview Aspect", "Fragment": "Preview Fragment", "Mod": "Preview Mod", "Ornament": "Preview Ornament", "Projection": "Preview Ghost Projection", "Shader": "Preview Shader", "Super": "Preview Super", "Transmat": "Preview Transmat Effect" }, "SelectWishlistPerks": "Preview Wishlist Perks" }, "Stats": { "CrouchingSpeed": "Crouching", "Custom": "Custom Total", "CustomDesc": "Custom total of selected base stats, ignoring mods or masterworks. Visit Settings to configure which stats are included.", "DamageResistance": "PvE Damage Resist", "Discipline": "Discipline", "DropLevel": "Account Power", "DropLevelExplanation1": "Account Power is the base power level when calculating the increased level of rewards.", "DropLevelExplanation2": "Account Power uses the highest level item in each slot, regardless of required Class or the \"One Exotic\" rule.", "EquippableGear": "Equippable Gear", "FlinchResistance": "Flinch Resist", "HP": "HP", "Intellect": "Intellect", "MaxGearPower": "Maximum Power of equippable gear", "MaxGearPowerAll": "Maximum Power of all gear", "MaxGearPowerOneExoticRule": "Maximum Power of equippable gear\n(only one Exotic armor piece equipped)", "MaxTotalPower": "Maximum total Power", "MetersPerSecond": "m/s", "Milliseconds": "ms", "NoBonus": "No Bonus", "NotApplicable": "N/A", "OfMaxRoll": "{{range}} of max roll", "PercentHelp": "Click for more information about what Stats Quality is.", "Percentage": "%", "PowerModifier": "Power granted by seasonal experience progression", "Prestige": "Prestige Level: {{level}}\n{{exp}}xp until 5 motes of light.", "Quality": "Stats quality", "ShieldHP": "Shield HP", "StrafingSpeed": "Strafing", "Strength": "Strength", "TierProgress": "T{{tier}} {{statName}} ({{progress}}/60 for T{{nextTier}})\n", "TierProgress_Max": "T{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "Time to Full HP", "Total": "Total", "TotalHP": "Total HP", "WalkingSpeed": "Walking", "WeaponPart": "Weapon Part" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM can now store your tags, loadouts, and settings on our own servers and sync that data between different versions of DIM, with no separate login. You can import your existing data from the Settings page if you haven't enabled DIM Sync before. This was made possible by the support of our OpenCollective backers!", "No": "Not right now", "Title": "Enable DIM Sync?", "Yes": "Enable Sync" }, "AutoBackup": "We've backed up your data to a file in your downloads folder called dim-data.json, just in case.", "BackUpFirst": "You MUST back up your data first, before you delete it all. Just in case.", "BrowserMayClearData": "The browser may delete this information if you run out of space or don't visit DIM frequently.", "DataIsLocal": "Tag and notes data is local only", "DeleteAllData": "Delete ALL Data from DIM Sync Servers", "DeleteAllDataConfirm": "Are you sure you want to delete ALL your data, for all accounts, from DIM Sync? You can't undo this.", "Details": { "IndexedDBStorage": "Local storage will save your information only on this browser. Clearing your browsing data will delete this information." }, "DimApiFinePrint": "DIM will save your tags, loadouts, and settings to the DIM servers and sync them between different versions of DIM.", "DimSyncDown": "DIM Sync is not connected due to a problem talking to the server.", "DimSyncEnabled": "DIM Sync Enabled", "DimSyncNotEnabled": "DIM Sync is not enabled, so your settings, tags, loadouts, and searches are only stored locally and will be lost if you clear your browser storage. Enable DIM Sync in Settings to back up your data automatically, or regularly back up your data manually.", "EnableDimApi": "Enable DIM Sync (recommended)", "Export": "Download Data Backup", "ExportError": "Failed to download backup from DIM Sync", "ExportErrorBody": "DIM Sync may be down, or you are having trouble with your connection. We will download a copy of your locally saved data instead.", "Import": "Import Data Backup", "ImportConfirmDimApi": "Are you sure you want to overwrite your current tags, loadouts, and settings with this version? It will completely replace what you had.", "ImportExport": "Backup & Import", "ImportFailed": "Import Failed! {{error}}", "ImportNoFile": "No file selected!", "ImportNotification": { "FailedBody": "Unable to import data. {{error}}", "FailedTitle": "Import Failed", "NoData": "No loadouts or tags found in the backup", "SuccessBodyForced": "Imported settings, {{loadouts}} loadouts, and {{tags}} tagged items from your backup into DIM Sync, replacing what was already there.", "SuccessBodyLocal": "Imported settings, {{loadouts}} loadouts, and {{tags}} tagged items from your backup into local storage, replacing what was already there. We cannot guarantee that local storage won't be lost - consider enabling DIM Sync.", "SuccessTitle": "Import Successful" }, "ImportTooManyFiles": "Please only select one file to import.", "ImportWrongFileType": "File is not a JSON file. It may not be a DIM backup.", "IndexedDBStorage": "Local Browser Storage", "LearnMore": "Learn more about DIM Sync", "MenuTitle": "Sync & Backups", "ProfileErrorBody": "We had a problem communicating with DIM Sync. Your latest settings, tags, loadouts, and searches may not be shown. Your data is still on our servers, and any updates you make locally will be saved when we can reconnect. We'll keep retrying while DIM is open.", "ProfileErrorTitle": "DIM Sync Download Error", "RefreshDimSync": "Reload remote data from DIM Sync", "UpdateErrorBody": "We had a problem saving your data to DIM Sync. We'll keep retrying while DIM is open.", "UpdateErrorTitle": "DIM Sync Save Error", "UpdateInvalid": "Failed to save data to DIM Sync", "UpdateInvalidBody": "Data sent to DIM Sync was invalid and will not be saved.", "UpdateInvalidBodyLoadout": "The loadout \"{{name}}\" is invalid and will not be saved. If you imported it from another site, please let them know that they are exporting invalid loadouts.", "UpdateQueueLength_one": "{{count}} new change will be saved when we can reconnect.", "UpdateQueueLength_other": "{{count}} new changes will be saved when we can reconnect.", "Usage": "DIM is using {{usage, humanBytes}} out of {{quota, humanBytes}} available to it on this device. This includes the downloaded Destiny item databases from Bungie.net." }, "StreamDeck": { "Authorize": "Connect application", "Enable": "Stream Deck Plugin", "Error": { "Body": "There was an error sending data to the Stream Deck plugin. Please contact the plugin developer. {{error}}", "Title": "Stream Deck Plugin Error" }, "FinePrint": "Enable the connection with the DIM Stream Deck plugin. This plugin is a separate project that is neither written by nor supported by the DIM team.", "Install": "Install plugin", "MissingAuthorization": "You must authorize the Stream Deck application to connect to DIM. Go to settings and click \"Connect application\".", "Tooltip": { "Application": "Stream Deck Application", "AuthRequired": "Click this button or go to settings and click \"Connect application\".", "Error": "Your Stream Deck plugin is no longer supported. Please update to the latest version. This plugin requires at least:", "ErrorConnection": "if you're already using the latest version, check if some browser extension is blocking the connection.", "ExtensionIssue": "Extensions Issue", "Plugin": "Plugin", "Title": "DIM Stream Deck Plugin", "Version": "Version:" } }, "StripSockets": { "Action": "Strip Sockets", "ArmorMods": "{{count}}x Armor Mod", "Button": "Strip {{numSockets}} Sockets", "Cancel": "Cancel", "Choose": "Choose Sockets to strip", "DiscountedMods": "{{count}}x Discounted Mod", "Done": "Stripped Sockets", "NoSockets": "No Sockets to clear", "Ok": "Ok", "Ornaments": "{{count}}x Ornament", "Others": "{{count}}x Ghost Projection", "Running": "Stripping Sockets", "Shaders": "{{count}}x Shader", "Subclass": "{{count}}x Subclass Option", "WeaponMods": "{{count}}x Weapon Mod" }, "Tags": { "Archive": "Archive", "ClearTag": "Clear Tag", "Favorite": "Favorite", "Infuse": "Infuse", "Junk": "Junk", "Keep": "Keep", "LockAll": "Lock Items", "TagItem": "Tag Item", "UnlockAll": "Unlock Items" }, "Triage": { "AccountsForArtifice": "This tests whether an Artifice armor piece could be better, if a +3 stat mod were used.", "BetterArmor": "Strictly Better Armor", "BetterArtificeArmor": "Better Artifice Armor", "BetterStatArmor": "Better Stats Armor", "BetterStatArtificeArmor": "Better Stat Artifice Armor", "BetterWorseArmor": "Better/Worse Armor", "BetterWorseIncludes": "Identifies armor pieces with:", "HighStats": "High Stats", "InLoadouts": "In Loadouts", "OwnedCount": "# Owned", "PerkBetterArmorDesc": "The same, or more, intrinsic perks or special mod slots.", "PerkWorseArmorDesc": "The same intrinsic perk, or none.", "SimilarItems": "Similar Items", "StatBetterArmorDesc": "All stats at least as high, and at least one stat better.", "StatNotPerkArmorDesc": "This tests only stats. A lower piece may still have special mod slots or intrinsic perks.", "StatWorseArmorDesc": "No better stats, and at least one worse stat.", "ThisItem": "This item", "WorseArmor": "Strictly Worse Armor", "WorseArtificeArmor": "Worse Non-Artifice Armor", "WorseStatArmor": "Worse Stat Armor", "WorseStatArtificeArmor": "Worse Stat Non-Artifice Armor", "YourBestItem": "Your best item" }, "Triumphs": { "GildingTriumph": "Gilding Triumph", "HideCompleted": "Hide completed triumphs", "RevealRedacted": "Reveal redacted triumphs", "SortRecords": "Sort triumphs by completion" }, "Vendors": { "Collections": "Collections", "Engram": "Rank", "FilterToUnacquired": "Only show uncollected items", "HideSilverItems": "Hide Silver items", "NoItems": "This Vendor is currently not offering any items.", "RefreshTime": "Inventory refreshes in:", "Vendors": "Vendors" }, "Views": { "About": { "APIHistory": "View the history of all actions taken by DIM (and other Destiny apps)", "BungieCopyright": "All images and content are property of Bungie.", "CommunityInsight": "Community Insights for Perks and Character Stats courtesy of {{clarityLink}}. If you notice inaccuracies or have questions, join the {{clarityDiscordLink}}.", "Discord": "Discord", "DiscordHelp": "Ask questions, give feedback, and get support in our Discord channels.", "FAQ": "Frequently Asked Questions", "FAQAccess": "How does DIM get access to my Destiny data?", "FAQAccessAnswer": "We use Bungie's app authentication to grant access to DIM to see and move your items. DIM never sees your username or password. This is the same way the Companion app works.", "FAQKeyboard": "Does DIM support keyboard shortcuts?", "FAQKeyboardAnswer": "Yes! Press \"?\" to see a list of available shortcuts.", "FAQLogout": "How can I log out of DIM?", "FAQLogoutAnswer": "Open the menu from the top left icon and choose \"Log Out\"", "FAQLostItem": "I lost my item using your tool!", "FAQLostItemAnswer": "Bungie doesn't allow apps to delete items (even their own app!). More than likely a transfer failed, leaving your item in the vault or on another character. You could search for the item. If that doesn't turn it up, reload the page. Check {{link}} or in game to see if your item still exists. We're sure it's still there.", "FAQMobile": "Does DIM support mobile? Will there be an app?", "FAQMobileAnswer": "The DIM website can be loaded on phones and tablets today, and you can add it to your home screen for an app-like experience.", "GitHub": "GitHub", "GitHubHelp": "If you're interested in contributing to the project, visit us at our project page on {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM is a free, open source app built by community developers upon the same services used by Bungie.net and the Destiny Companion App.", "Schedule": { "beta": "This beta version of DIM is updated every time we change the code - it gets the latest features and fixes, but also the latest bugs!", "release": "This version of DIM is updated once a week, at approximately midnight on Sundays, US Pacific time." }, "Translation": "Join the Translation Team!", "TranslationText": "We use {{link}} for ease of translation. If you want to improve one of DIM's translations, join the team.", "Version": "Version {{version}} ({{flavor}}), built on {{date}}", "Wiki": "DIM User Guide", "WikiHelp": "Learn how to use DIM's features." }, "Login": { "Auth": "Authorize with Bungie.net", "EnableDimSyncWarning": "You had previously disabled DIM Sync and were only using local data storage. Enabling DIM Sync will replace any local data with the data from DIM Sync. You should back up your data before enabling DIM Sync. You can restore from that backup in Settings.", "Explanation": "Allow DIM to view and modify your Destiny characters, vault, and progression.", "LearnMore": "Learn more about accounts and login", "NewAccount": "Log in with a different Bungie.net account", "Permission": "We need your permission..." }, "Support": { "BackersDetail": "Support us with a one-time or monthly donation and help us continue our active development.", "FreeToDownload": "DIM is a product that is free to download and use. The source code for DIM is open source and free for anyone to enhance. You will never see an ad in DIM. That is our commitment.", "OpenCollective": "We are using {{link}} as a service to provide compensation to our developers for their dedication and time spent on this project.", "Store": "We have merch with our logo and other designs for sale on {{link}}", "Support": "Support DIM" } }, "WishListRoll": { "BestRatedTip_one": "This perk exactly matches a weapon roll on your wishlist.", "BestRatedTip_other": "These perks exactly match a weapon roll on your wishlist.", "Clear": "Clear Wish List", "CopiedLine": "Wish List roll copied to clipboard", "CopyLine": "Copy Selected Perks as Wish List Roll", "DupeRolls": " (+{{num, number}} ignored dupes)", "ExternalSource": "Add another wish list", "ExternalSourcePlaceholder": "Paste wish list URL here", "Header": "Wish List", "Import": "Load Wish List Rolls", "ImportError": "Error loading wish list from \"{{url}}\": {{error}}", "ImportFailed": "None of your wish lists contained any valid rolls.", "ImportNoFile": "No file selected.", "InvalidExternalSource": "Please enter a valid URL for your external wish list source. The URL must start with one of the following:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "Last updated: {{lastUpdatedDate}} at {{lastUpdatedTime}}", "Num": "{{num, number}} rolls in your wish list", "NumRolls": "{{num, number}} rolls", "Refresh": "Refresh Wishlist", "SourceAlreadyAdded": "Wish List already added", "UpdateExternalSource": "Add Wish List", "Voltron": "voltron (default)", "WishListNotes": "Wish List Notes:", "WorstRatedTip_one": "This perk exactly matches a weapon roll on your trashlist.", "WorstRatedTip_other": "These perks exactly match a weapon roll on your trashlist." }, "no-space": "no-space", "wrong-level": "wrong-level" } ================================================ FILE: src/locale/es.json ================================================ { "AWA": { "ConfirmDescription": "Por favor, use la Aplicación del Acompañante de Destiny 2 para aprobar que DIM modifique tus objetos.", "ConfirmTitle": "Confirmar Acción", "Error": "Error cambiando modificadores o ventajas", "ErrorMessage": "No pudimos equipar {{plug}} en {{item}}.\n\n{{error}}", "FailedToken": "No se pudo obtener permiso para cambiar el objeto", "IrreversiblePlugging": "No tienes ningún {{plug}}, así que no lo sobrescribiremos." }, "Accounts": { "Choose": "Perfiles para {{bungieName}}", "ErrorLoadInventory": "No se pudieron cargar tu inventario y personajes de Destiny {{version}}", "ErrorLoadManifest": "No se pudo cargar información de la base de datos de Destiny desde Bungie", "ErrorLoading": "No se pudieron cargar cuentas de Destiny desde Bungie.net", "MissingAccountWarning": "Si no ves tu cuenta aquí, puede que no hayas iniciado sesión con la cuenta de Bungie.net correcta, o puede que Bungie.net esté caído por mantenimiento.", "MissingDescription": "La cuenta que estás intentando ver no es una cuenta enlazada a tu perfil de Bungie.net. Selecciona una de tus cuentas debajo.", "MissingTitle": "Cuenta No Encontrada", "NoCharacters": "No tienes ningún personaje de Destiny asociado a esta cuenta de Bungie.net. Trata de iniciar sesión en una cuenta diferente.", "NoCharactersTitle": "No Se Encontraron Personajes", "SwitchAccounts": "Puedes cambiar de cuentas más tarde desde el menú en el encabezado.", "Title": "Cuentas" }, "Activities": { "Activities": "Actividades", "Hard": "Difícil", "Nightfall": "Asalto de Ocaso", "Normal": "Normal", "WeeklyHeroic": "Asalto Heroico Semanal" }, "Armory": { "AlternateItems": "Versiones Alternativas", "Armory": "Armería", "DifferentSeason": "Reeditada de una temporada diferente", "NoNotes": "Sin Notas", "OpenInArmory": "ver en la Armería", "Season": "Temporada {{season}}, Año {{year}}", "TrashlistedRolls_one": "Tirada en Lista de Basura", "TrashlistedRolls_other": "{{count, number}} Tiradas en Lista de Basura", "Unknown": "Objeto Desconocido", "UnknownPerkHash": "El hash de la ventaja {{hash}} ({{perkName}}) no aparece en este objeto, así que esta tirada de la lista de deseos es inválida. Por favor contacta con el autor de la lista de deseos para corregir esto. Toma en cuenta que la listas de deseos siempre deberían especificar las versiones no mejoradas de las ventajas.", "WishlistedRolls_one": "Tirada en Lista de Deseos", "WishlistedRolls_other": "{{count, number}} Tiradas en Lista de Deseos", "YourItems": "Tus Objetos" }, "Browsercheck": { "Samsung": "Internet de Samsung puede hacer que los sitios parezcan demasiado oscuros cuando el modo oscuro está activo. Habilita Opciones > Laboratorios > Usar tema oscuro o cambia a otro navegador.", "Steam": "El navegador de la interfaz de Steam es muy antiguo y podría hacer que algunas o todas las características de DIM no funcionen. No podemos proveer de soporte para ello.", "Unsupported": "El equipo de DIM no soporta usar este navegador. Algunas o todas las características de DIM podrían no funcionar." }, "Bucket": { "Armor": "Armadura", "Class": "Subclase", "General": "General", "Ghost": "Espectro", "Inventory": "Inventario", "Postmaster": "Administración", "Progress": "Progreso", "Reputation": "Reputación", "Unknown": "Desconocido", "Vault": "Depósito", "Weapons": "Armas" }, "BulkNote": { "Append": "Adjuntar a notas / añadir #hashtags", "Confirm": "Actualizar Notas", "Remove": "Eliminar de notas / eliminar #hashtags", "Replace": "Reemplazar notas", "Title_one": "Cambiar notas para 1 objeto", "Title_other": "Cambiar notas para {{count}} objetos" }, "BungieAlert": { "Title": "Mensaje de Bungie:" }, "BungieService": { "AppNotPermitted": "DIM no tiene permiso para realizar esta acción.", "DestinyCannotPerformActionAtThisLocation": "No puedes equipar objetos o cambiar modificadores mientras estés en una actividad. Intenta dirigirte a órbita o a un área social. Esto es una limitación de la API de Bungie.net, no de DIM.", "DestinyItemUnequippable": "No puedes equipar este objeto. Si la última actividad del personaje bloqueó su equipamiento, intenta iniciar sesión con el personaje de nuevo.", "DestinyLegacyPlatform": "Actualmente, los servicios de Bungie tienen un error que impide que DIM pueda cargar tu perfil de tu cuenta de Destiny 2 si has jugado Destiny 1 en una consola old-gen. Bungie arreglará esto pronto, pero hasta entonces debes jugar Destiny 1 en una consola de generación actual para poder acceder a tu perfil.", "DevVersion": "¿Estás ejecutando una versión en desarrollo de DIM? Debes registrar tu extensión de Chrome con Bungie.net.", "Difficulties": "Bungie.net está experimentando dificultades actualmente.", "ErrorTitle": "Error de Bungie.net", "ItemUniquenessExplanation": "Solo un personaje puede tener un '{{name}}' en él.", "Maintenance": "Los servidores de Bungie.net se encuentran caídos por mantenimiento.", "MissingInventory": "Bungie.net no devolvió tu inventario, posiblemente porque tu configuración de privacidad lo impide. Intenta cerrar y volver a iniciar sesión.", "NetworkError": "Error de red - {{status}} {{statusText}}", "NoAccount": "No se encontró ninguna cuenta de Destiny. ¿Tienes seleccionada la plataforma correcta?", "NoAccountForPlatform": "Fallo encontrando una cuenta de Destiny para ti en {{platform}}.", "NotConnected": "Es posible que no tengas conexión a Internet.", "NotConnectedOrBlocked": "Podría no estar conectado a Internet, o una extensión de privacidad o bloqueador de anuncios bloqueando Bungie.net.", "NotLoggedIn": "Por favor autoriza DIM a fin de usar esta app.", "Slow": "Bungie.net está lento ahora mismo", "SlowDetails": "Bungie.net está tomando mucho tiempo en devolverte la información. Esto puede pasar cuando muchos jugadores están en el juego a la vez o si Bungie.net está teniendo problemas. También podrías estar teniendo un problema de conexión a internet. Estaremos esperando por una respuesta.", "SlowResponse": "Bungie.net fue demasiado lento en responder.", "Throttled": "Bungie.net está limitando ahora cuántas solicitudes puede hacer de DIM.", "Twitter": "Entérate de las actualizaciones de estados en:", "UnknownError": "Mensaje de Bungie.net: {{message}}", "VendorNotFound": "Los datos de los comerciantes no están disponibles." }, "Compare": { "Archetype": "Arquetipo", "AssumeMasterworked": "Asumir Obra Maestra Completa", "AssumeMasterworkedDescription": "Estadísticas si es Obra Maestra Completa, sin los Modificadores actuales", "BaseStatsDescription": "Estadísticas base, sin Obras Maestras o Modificadores", "Button": "Comparar", "ButtonHelp": "Comparar objetos", "CompareBaseStats": "Mostrar Estadísticas Base", "CurrentStats": "Estadísticas Actuales", "CurrentStatsDescription": "Estadísticas actuales, incluyendo Modificadores y nivel de Obra Maestra", "Error": { "Invalid": "No hay objetos válidos para la comparación.", "Unmatched": "Este artículo no coincide con el tipo de objetos siendo comparados." }, "InitialItem": "Este es el objeto por el cual la herramienta de comparación fue lanzada", "IsVendorItem": "Este objeto no está en tu inventario, pero {{vendorName}} lo vende.", "NoModArmor": "Antes de 2.0" }, "Cooldown": { "Grenade": "Enfriamiento de Granada: {{cooldown}}", "Melee": "Enfriamiento de Cuerpo a Cuerpo: {{cooldown}}", "Super": "Enfriamiento de la Súper: {{cooldown}}" }, "Countdown": { "Days_compact_one": "{{count}}d", "Days_compact_other": "{{count}}d", "Days_one": "1 Día", "Days_other": "{{count}} Días" }, "Csv": { "EmptyFile": "No había filas en el archivo.", "ImportConfirm": "¿Estás seguro de que quieres importar las etiquetas/notas desde el CSV? Esto sobreescribirá las etiquetas/notas para todos los objetos contenidos en tu hoja de cálculo.", "ImportFailed": "Fallo al importar etiquetas/notas desde el CSV: {{error}}", "ImportSuccess_one": "Etiquetas/notas cargadas para un objeto.", "ImportSuccess_other": "Etiquetas/notas cargadas para {{count}} objetos.", "ImportWrongFileType": "El archivo no es un archivo CSV.", "WrongFields": "El CSV debe tener las columnas 'Id', 'Notes', 'Tag' y 'Hash'." }, "Dialog": { "Cancel": "Cancelar", "OK": "OK" }, "EnergyMeter": { "Energy": "Energía", "Unused": "Sin usar", "UpgradeNeeded": "La capacidad de energía del objeto actual es {{energyCapacity}}. Para encajar los modificadores seleccionados, su capacidad de energía debe ser {{energyUsed}}.", "Used": "Usado" }, "ErrorBoundary": { "Title": "Algo fue mal" }, "ErrorPanel": { "BrowserTooOld": "Tu navegador es demasiado antiguo para usar DIM. Por favor actualiza tu navegador a la última versión.", "BrowserTooOldTitle": "Navegador Incompatible", "Description": "Intenta cargar tu inventario en la Aplicación del Acompañante de Destiny 2 para ver si Bungie está caído.", "ReadTheGuide": "Lea nuestra Guía de Usuario (enlazada desde el menú) para pasos de solución de problemas.", "SystemDown": "Esto afecta a todas las aplicaciones de Destiny, y el equipo de DIM no puede arreglarlo ni evitarlo.", "Troubleshooting": "Guía de Solución de Problemas" }, "FarmingMode": { "D2Desc_female_one": "DIM está impidiendo que los objetos vayan a Administración asegurándose de que siempre haya un espacio vacío por tipo de objeto en {{store}}.", "D2Desc_female_other": "DIM está impidiendo que los objetos vayan a Administración asegurándose de que siempre haya {{count}} espacios vacíos por tipo de objeto en {{store}}.", "D2Desc_male_one": "DIM está impidiendo que los objetos vayan a Administración asegurándose de que siempre haya un espacio vacío por tipo de objeto en {{store}}.", "D2Desc_male_other": "DIM está impidiendo que los objetos vayan a Administración asegurándose de que siempre haya {{count}} espacios vacíos por tipo de objeto en {{store}}.", "D2Desc_one": "DIM está impidiendo que los objetos vayan a Administración asegurándose de que siempre haya un espacio vacío por tipo de objeto en {{store}}.", "D2Desc_other": "DIM está impidiendo que los objetos vayan a Administración asegurándose de que siempre haya {{count}} espacios vacíos por tipo de objeto en {{store}}.", "Desc_female_one": "DIM está moviendo objetos de Engramas y Lumen desde {{store}} al depósito y manteniendo un espacio vacío abierto por tipo de objeto para impedir que cualquier cosa se vaya a Administración.", "Desc_female_other": "DIM está moviendo objetos de Engramas y Lumen desde {{store}} al depósito y manteniendo {{count}} espacios abiertos por tipo de objeto para impedir que cualquier cosa se vaya a Administración.", "Desc_male_one": "DIM está moviendo objetos de Engramas y Lumen desde {{store}} al depósito y manteniendo un espacio vacío abierto por tipo de objeto para impedir que cualquier cosa se vaya a Administración.", "Desc_male_other": "DIM está moviendo objetos de Engramas y Lumen desde {{store}} al depósito y manteniendo {{count}} espacios abiertos por tipo de objeto para impedir que cualquier cosa se vaya a Administración.", "Desc_one": "DIM está moviendo objetos de Engramas y Lumen desde {{store}} al depósito y manteniendo un espacio vacío abierto por tipo de objeto para impedir que cualquier cosa se vaya a Administración.", "Desc_other": "DIM está moviendo objetos de Engramas y Lumen desde {{store}} al depósito y manteniendo {{count}} espacios abiertos por tipo de objeto para impedir que cualquier cosa se vaya a Administración.", "FarmingMode": "Modo Recolección", "FarmingModeNote": "(mantenga espacio para objetos soltados)", "MakeRoom": { "Desc": "DIM solo está moviendo Engramas y objetos de Lumen desde {{store}} al depósito o a otros personajes para prevenir que se vayan a la Administración.", "Desc_female": "DIM solo está moviendo Engramas y objetos de Lumen desde {{store}} al depósito o a otros personajes para prevenir que se vayan a la Administración.", "Desc_male": "DIM solo está moviendo Engramas y objetos de Lumen desde {{store}} al depósito o a otros personajes para prevenir que se vayan a la Administración.", "MakeRoom": "Haz espacio para coger objetos moviendo el equipamiento", "Tooltip": "Si está marcado, DIM moverá armas y armadura para hacer espacio en el depósito para Engramas." }, "OutOfRoom": "Te has quedado sin espacio para mover objetos fuera de tu {{character}}. ¡Es momento de limpiar la basura!", "OutOfRoomTitle": "Sin Espacio", "Stop": "Detener", "Vault": "Moverá objetos al depósito para hacer espacio." }, "FashionDrawer": { "Accept": "Guardar cosméticos", "CannotFitOrnament": "Este objeto no tiene ranura para diseño o no tienes diseños para él.", "CannotFitShader": "A este objeto no le encaja un shader", "ClearOrnaments": "Limpiar Diseños", "ClearOrnamentsTitle": "Reiniciar todos los diseños a \"sin preferencia\"", "ClearShaders": "Limpiar Shaders", "ClearShadersTitle": "Reiniciar todos los shaders a \"sin preferencia\"", "NoPreference": "Sin preferencia - este espacio no será cambiado", "Reset": "Limpiar cosméticos", "Sync": "Sincronizar", "SyncOrnaments": "Sincronizar Diseños", "SyncOrnamentsTitle": "Usar diseños de la misma equipación en todos los objetos, si están desbloqueados", "SyncShaders": "Sincronizar Shaders", "SyncShadersTitle": "Usar el mismo shader en todos los objetos", "Title": "Elegir shaders y diseños", "UseEquipped": "Usar cosméticos equipados" }, "FileUpload": { "Instructions": "Haga clic o arrastre archivos" }, "Filter": { "Adept": "\\(Experto\\)", "AmmoType": "Muestra objetos basados en su tipo de munición.", "Armor": "Muestra objetos que sean armaduras.", "Armor3": "Muestra objetos que utilicen el sistema de estadística de Armadura 3.0 introducidos en Los Confines del Destino.", "ArmorCategory": "Muestra armaduras basadas en su categoría.", "ArmorIntrinsic": "Muestra armadura legendaria que tenga una ventaja intrínseca, como Armadura Artificiosa.", "Artifice": "Mostrar armadura Artificiosa.", "Ascended": "Muestra objetos que tengan un nodo ascendente en el que haya sido ascendido.", "Breaker": "Filtrar por tipo de rompedor o tipo de campeón correspondiente. breaker:intrinsic muestra objetos con habilidad de rompedor intrínseca.", "BulkClear_one": "Etiqueta eliminada de 1 objeto.", "BulkClear_other": "Etiquetas eliminadas de {{count}} objetos.", "BulkRevert_one": "Etiqueta revertida en 1 objeto.", "BulkRevert_other": "Etiquetas revertidas en {{count}} objetos.", "BulkTag_one": "Etiquetado objeto seleccionado como {{tag}}.", "BulkTag_other": "Etiquetados {{count}} objetos seleccionados como {{tag}}.", "Catalyst": "Muestra catalizadores basados en su estado. catalyst:complete muestra catalizadores que hayas completado y aplicado, catalyst:incomplete muestra catalizadores que hayas desbloqueado pero puede que no has completado el objetivo o aplicado el catalizador, y catalyst:missing muestra objetos que pueden tener un catalizador pero no se han encontrado todavía.", "Class": "Muestra objetos basados en su afinidad de clase.", "Combine": "Los filtros pueden ser combinados o agrupados con paréntesis, \"or\" y \"and\" para limitar tu búsqueda, por ejemplo \"{{example}}\".", "ContributePower": "Muestra los objetos que tienen poder y contribuyen a subir tu nivel de poder.", "Cosmetic": "Muestra objetos que son cosméticos o estilo.", "Craftable": "Muestra objetos que sean fabricables.", "CraftedDupe": "Muestras armas duplicadas donde al menos una de las duplicadas está fabricada.", "Curated": "Muestra objetos que tengan una tirada de curador.", "CurrentClass": "Muestra objetos que sean equipables en el personaje actualmente conectado.", "CustomStatLower": "Muestra armadura cuyas estadísticas son estrictamente más bajas que cualquiera otra del mismo tipo de armadura, solo tomando en cuenta estadísticas que están en cualquiera de esa lista personalizada de estadísticas totales de clase.", "DamageType": "Muestra objetos basados en su tipo de daño.", "Deepsight": "Muestra armas con Resonancia de Visión Profunda, que pueden tener su patrón extraído, o que puedan tener Resonancia de Visión Profunda habilitada usando un Armonizador de Visión Profunda.", "Deprecated": "Este filtro ya no es compatible.", "Description": "Descripción", "DescriptionFilter": "Muestra objetos cuya descripción tiene una coincidencia parcial con el texto filtrado. Busca frases enteras usando comillas.", "DisabledModSlot": "Muestra objetos con un modificador deshabilitado.", "Dupe": "Muestra objetos duplicados, incluyendo reeditadas", "DupeArchetype": "Agrupa armadura con el mismo Arquetipo de estadística.", "DupeCount": "Objetos que tienen el número especificado de duplicados.", "DupeLower": "Objetos duplicados, incluyendo reeditadas, que no son el poder duplicado más alto. Solo un duplicado es elegido como el más alto, y el resto son considerados más bajos.", "DupePerks": "Muestra objetos cuyas ventajas ya sean duplicadas de, o un subconjunto de, otro objeto del mismo tipo.", "DupeSetBonus": "Agrupa armadura con la misma bonificación de conjunto.", "DupeStats": "Muestra armadura con estadísticas base idénticas, y modificadores de estadísticas de ajuste coincidentes como Artificiosa o de Ajuste.", "DupeTertiary": "Agrupa armadura con la misma estadística terciaria.", "DupeTraits": "Armas cuyos rasgos ya sean un duplicado de (o un subconjunto de) otro arma del mismo tipo.", "DupeTunedStat": "Agrupa armadura con la misma estadística Ajusta.", "DupeUntunedStats": "Agrupa armadura con las mismas estadísticas idénticas, ignorando modificadores de ajuste de estadísticas.", "DupeZeroStats": "Agrupa armadura con las 3 mismas estadísticas base que no sean cero.", "Energy": "Muestra objetos que utilicen el sistema de modificadores de Armadura 2.0 introducidos en Bastión de Sombras.", "EnergyCapacity": "Muestra objetos basados en su capacidad de energía actual.", "Engrams": "Muestra engramas.", "Enhanceable": "Muestra armas que pueden ser mejoradas.", "Enhanced": "Muestra armas basadas en su nivel de mejora.", "EnhancedPerk": "Muestra armas que tiene un número especificado de columnas de ventajas mejoradas.", "EnhancementReady": "Muestra armas que han alcanzado niveles umbrales para mejora de ventajas.", "Equipment": "Objetos que pueden ser equipados.", "Equipped": "Objetos que actualmente están equipados en un personaje.", "Event": "Muestra objetos según el evento de Destiny 2 en que aparecieron.", "ExtraPerk": "Muestra tiradas aleatorias de armas legendarias con una ventaja adicional seleccionable.", "Featured": "Objetos que cuenten como uno de los objetos \"Nuevo Equipamiento\" u \"Objetos Destacados\" en la temporada actual.", "Filter": "Filtro", "FilterWith": "Filtrar con:", "Focusable": "Muestra objetos que pueden ser concentrados en un comerciante", "Foundry": "Muestra objetos basados en la fundición que los creó.", "Glimmer": "Muestra objetos que son consumibles que están relacionados con la ganancia de lumen.", "Harrowed": "\\(Sepulcral\\)", "HasNotes": "Muestra objetos que contengan notas aplicadas.", "HasOrnament": "Muestra objetos que tengan un diseño aplicado.", "HasShader": "Muestra objetos que tengan un shader aplicado.", "Holofoil": "Muestra armas holometalizadas.", "InDimLoadout": "is:indimloadout muestra objetos que están incluidos en cualquier equipación en DIM.", "InInGameLoadout": "is:iningameloadout muestra objetos que están incluidos en cualquier equipación en el juego.", "InInventory": "Muestra objetos en los que tengas al menos una copia en tu inventario. Solo son realmente útiles en los Comerciantes y en las pantallas de Hazañas.", "InLoadout": "is:inloadout muestra objetos que estén incluidos en cualquier equipación. Buscando con inloadout: muestra objetos que estén incluidos en equipaciones con títulos coincidentes. Cuando es usado con un hashtag, inloadout: muestra objetos cuyas equipaciones tengan el hashtag en el título o en las notas. Cuando con se usa con un operador de comparación, muestra objetos que están en tantas equipaciones.", "Infusable": "Muestra objetos que pueden ser infundidos.", "InfusionFodder": "Muestra objetos que podrían ser infundidos en versiones más bajas de poder del mismo objeto sólo por lumen.", "IsAdept": "Muestra armas compatibles con modificadores Expertos.", "IsCrafted": "Muestra armas que han sido fabricadas.", "ItemHash": "Muestra los objetos con un hash de objeto de inventario obtenido. Para usuarios avanzados.", "ItemId": "Muestra el objeto con un ID de inventario obtenido. Para usuarios avanzados.", "Leveling": { "Complete": "{{term}} - muestra objetos que están totalmente completos - todas las mejoras desbloqueadas.", "Incomplete": "{{term}} - muestra objetos que no están completos - aún queda al menos una mejora por desbloquear.", "NeedsXP": "{{term}} - muestra objetos que aún pueden adquirir más XP en ellos.", "Upgraded": "{{term}} - muestra objetos que tienen suficiente XP para desbloquear todos sus nodos, pero no todos sus nodos han sido desbloqueados.", "XPComplete": "{{term}} - muestra objetos que no pueden adquirir mas XP en ellos (sin importar si sus mejoras han sido o no desbloqueadas)." }, "Location": "Muestra objetos basados en su localización dentro de la app. \"Left/middle/right\" son la localización visual del personaje, y mientras \"inleftchar\" siempre funcionará, los otros dos están basados en cuántos personajes tengas. \"Current\" es tu último/actual personaje iniciado (que está marcado con un triángulo amarillo).", "LockAllFailed": "Error al bloquear objetos", "LockAllSuccess": "Bloqueados {{num}} objetos", "Locked": "Muestra objetos basados en su estado de bloqueo.", "Masterwork": "Muestra objetos basados en su nivel de estadística de Obra Maestra o nivel de Obra Maestra.", "MasterworkKills": "Muestra objetos basados en su registro de contador de bajas de Obra Maestra.", "MaxPower": "Muestra los objetos en el nivel más alto de poder por ranura.", "MaxPowerLoadout": "Muestra los objetos en la equipación que podrían maximizar tu Poder de Luz para cada clase de personaje.", "Memento": "Muestra armas que tengan una ranura para recuerdo.", "ModSlot": "Muestra armadura con un tipo de modificador específico.", "Mods": { "Y3": "Muestra objetos nuevos sin modificadores aplicados." }, "Name": "Muestra objetos cuyo nombre coincida exactamente (exactname:) o parcialmente (name:) con el texto filtrado. Busca frases enteras usando comillas.", "NamedStat": "Muestra armadura que tiene puntos en la estadística nombrada.", "Negate": "Para negar una búsqueda, prefija el término de búsqueda con el signo menos o la palabra \"not\", como por ejemplo \"{{notexample}}\" o \"{{notexample2}}\".", "NewItems": "Muestra objetos nuevos.", "Notes": "Busca por objetos que tengas etiquetados con notas personalizadas.", "OriginTrait": "Muestra armas que tienen una ventaja de rasgo original.", "Ornament": "Muestra objetos con diseños y filtros para su estado.", "PartialMatch": "Muestra objetos donde su nombre, descripción, cualquier ventaja o cualquier modificador tenga una coincidencia parcial al texto filtrado. Busca frases enteras usando comillas.", "PatternUnlocked": "Muestra objetos que tienen un patrón de fabricación desbloqueado, incluso si el objeto en sí no está fabricado.", "Perk": "Muestra objetos donde una de sus ventajas o modificadores tienen una coincidencia parcial al texto filtrado en su nombre o descripción. Busca por frases enteras usando comillas.", "PerkName": "Muestra objetos con una ventaja o modificador cuyo nombre coincida exactamente (exactperk:) o parcialmente (perkname:) con el texto filtrado. Busca frases completas usando comillas.", "PinnacleReward": "Muestra aventuras que produzcan una recompensa pináculo.", "Postmaster": "Objetos que están actualmente en la Administración.", "PowerKeywords": "Usa la palabra clave \"pinnaclecap\" o \"softcap\" en vez de un número para referirte a los límites de poder de la temporada actual.", "PowerLevel": "Muestra objetos basados en su nivel de poder. $t(Filter.PowerKeywords)", "PowerfulReward": "Muestra aventuras que tengan una recompensa poderosa.", "PrismaticDamageType": "Muestra objetos pasados en si su tipo de daño es de Luz u Oscuridad. Los tipos de Luz son arco, solar y vacío. Los tipos de Oscuridad son estasis y atadura.", "Quality": "Muestra objetos basados en su porcentaje de calidad estadística total. '{{percentage}}' es un sobrenombre de '{{quality}}'.", "RandomRoll": "Muestra objetos que suelten tiradas aleatorias.", "RarityTier": "Muestra objetos basados en su nivel de rareza.", "Reforgeable": "Muestra objetos que pueden ser reforjados en el armero.", "Release": "Muestra objetos disponibles de un lanzamiento o evento específico.", "RequiredLevel": "Muestra objetos basados en su nivel requerido.", "RetiredPerk": "Muestra armas con ventajas que ya no se pueden obtener.", "SearchPrompt": "Buscar filtros de comandos disponibles", "Season": "Muestra objetos en cuya temporada de Destiny 2 aparecieron.", "StackFull": "Muestra objetos que están al límite de capacidad para su montón (Núcleos de Mejora, Monedas Extrañas, Materiales de Armero, etc)", "StackLevel": "Muestra objetos basados según la cantidad de objetos acumulados.", "Stackable": "Muestra objetos que pueden ser apilados (municiones sintéticas, monedas extrañas, etc)", "StatLower": "Muestra armadura cuyas estadísticas son estrictamente más bajas que otra del mismo tipo de armadura.", "Stats": "Muestra objetos basados en un valor específico de estadística. $t(Filter.StatsExtras)", "StatsBase": "Filtra armadura basada en su valor de estadística base, sin incluir modificadores adjuntos u obras maestras. $t(Filter.StatsExtras)", "StatsExtras": "Soporta la adición de estadísticas conectando múltiples nombres de estadísticas con el símbolo \"+\" o \"&\". También hay palabras clave especiales (highest, secondhighest, thirdhighest, etc.) que coinciden con estadísticas basadas en su nivel dentro de las estadísticas de un objeto. Cada estadística personalizada también tiene su propio término de búsqueda, mostrado en la configuración de Estadísticas Personalizadas.", "StatsLoadout": "Encuentra un conjunto de objetos para equipar por el valor máximo total de una estadística específica.", "StatsMax": "Encuentra armadura con el número más alto para una estadística específica. Incluye todos los objetos con la estadística más alta.", "StatsOrdinal": "Encuentra armadura 3.0 con un enfoque de estadísticas especificadas.", "Tags": { "Tag": "Muestra objetos que tengan una etiqueta específica.", "Tagged": "Muestra objetos que tengan cualquier etiqueta." }, "Tier": "Muestra objetos basados en su nivel del 0-5.", "Timelost": "\\(Perdido en el Tiempo\\)", "Tracked": "Muestra misiones/contratos basados en su estado de seguimiento.", "Transferable": "Objetos que pueden moverse entre personajes.", "Trashlist": "Muestra objetos que coincidan con la lista basura de tu lista de deseos.", "TunedStat": "Muestra objetos con modificadores de ajuste para la estadística especificada.", "Unascended": "Muestra objetos que tengan un nodo ascendente en el que no haya sido ascendido.", "Undo": "Deshacer", "UnlockAllFailed": "Error al desbloquear objetos", "UnlockAllSuccess": "Desbloqueados {{num}} objetos", "Vendor": "El objeto está disponible desde un comerciante específico.", "VendorItem": "El objeto es de un comerciante, no está en tu inventario. Útil para excluir los objetos de un comerciante desde el Optimizador de Equipaciones.", "Weapon": "Muestra objetos que sean armas.", "WeaponLevel": "Muestra armas basadas en su Nivel de Arma.", "WeaponType": "Muestra armas basadas en su tipo de arma.", "Wishlist": "Muestra objetos que coincidan con tu lista de deseos.", "WishlistDupe": "Muestra objetos duplicados donde al menos uno de los duplicados esté en tu lista de deseos.", "WishlistEnabled": "Muestra objetos que sean elegibles para tener tiradas de lista de deseos.", "WishlistNotes": "Muestra objetos de listas de deseos cuyas notas coincidan con la búsqueda.", "WishlistUnknown": "Muestra objetos sin recomendaciones de tiradas en las listas de deseos cargadas.", "Year": "Muestra los objetos según el año de Destiny en que aparecieron." }, "General": { "ClickForDetails": "Haga clic para detalles", "Close": "Cerrar", "Confirm": "¿Confirmar?", "UserGuideLink": "Guía de usuario" }, "Glyphs": { "Axe": "Hacha", "DarkAbility": "Habilidad de Oscuridad", "Gilded": "Dorado", "Harmonic": "Armónico", "HiveSword": "Espada de la Colmena", "LightAbility": "Habilidad de Luz", "LightLevel": "Nivel de Luz", "Misadventure": "Accidente", "Missing": "Desaparecido", "OpenSymbolsPicker": "Abrir el Selector de Símbolos", "Prismatic": "Prismática", "Quickfall": "Caída en picado", "RespawnRestricted": "Reaparición Limitada", "ScorchCannon": "Cañón Calcinante", "SearchSymbols": "Buscar Símbolos...", "Smoke": "Humo" }, "Header": { "About": "Acerca de DIM", "AutoRefresh": "DIM se recargará automáticamente mientras todavía estés jugando.", "BulkTag": "Grupos de etiquetas para objetos", "BungieNetAlert": "Alerta de Bungie", "Clear": "Limpiar filtro de búsqueda", "CompareMatching": "Comparar Objetos", "DeleteSearch": "Borrar Búsqueda", "FilterHelp": "Buscar objeto/ventaja, {{example}}, y más", "FilterHelpBrief": "Buscar objetos", "FilterHelpLoadouts": "Buscar nombres y notas de equipaciones", "FilterHelpMenuItem": "Ayuda de Filtros...", "FilterHelpOptimizer": "Filtrar armadura incluida en equipaciones, ej.: {{example}}", "FilterHelpProgress": "Buscar hazañas y contratos", "FilterHelpRecords": "Buscar triunfos y colecciones", "FilterMatchCount_one": "1 objeto", "FilterMatchCount_other": "{{count}} objetos", "Filters": "Filtros", "InstallDIM": "Instalar como App", "InstallDIMBanner": "Instalar DIM como app en tu pantalla de inicio", "Inventory": "Inventario", "IosPwaPrompt": "En Safari, haga clic en el icono compartir (botón central en la parte abajo) y selecciona \"Añadir a la Pantalla de Inicio\".", "KeyboardShortcuts": "Atajos de Teclado", "LaunchDIMAlone": "Separar Ventana", "MaterialCounts": "Recuento de Materiales", "Menu": "Menú", "ProfileAge": "Los servidores de Destiny enviaron por última vez información hace {{age}}.\nActualizar desde DIM podría obtener nuevos datos, pero Bungie.net también podría repetir información en la caché.", "Refresh": "Actualizar Datos de Destiny [R]", "ReloadApp": "Recargar App", "ReportBug": "Informar de un Error", "SaveSearch": "Guardar Búsqueda", "SearchActions": "Abrir Acciones de Búsqueda", "SearchResults": "Mostrar Objetos", "Shop": "Tienda", "TagAs": "Etiquetar como '{{tag}}'", "UpgradeDIM": "Actualizar DIM", "WhatsNew": "Que Hay de Nuevo" }, "Help": { "CannotMove": "No puedes mover ese objeto de este personaje.", "NoStorage": "DIM no puede almacenar información", "NoStorageMessage": "DIM no puede almacenar información en tu navegador. Esto puede ser causado por navegar en privado o en modo incógnito, o cuando tengas poco espacio en disco, o por un fallo del navegador. ¡Intenta reiniciar tu ordenador! No podrás ser capaz de iniciar sesión o usar DIM hasta que no arregles esto." }, "Hotkey": { "Armory": "Mostrar la Armería para un objeto", "CheatSheetTitle": "Atajos del Teclado:", "ClearDialog": "Descartar diálogo", "ClearNewItems": "Despejar objetos nuevos", "Enter": "ENTER", "ItemPopupTab": "Cambiar a pestaña de detalles de objeto", "LockUnlock": "Bloquear o desbloquear un objeto", "MarkItemAs": "Marcar objeto como '{{tag}}'", "Menu": "Alternar menú", "Note": "Introducir notas", "Pull": "Traer objeto al personaje activo", "RefreshInventory": "Actualizar inventario", "ShowHotkeys": "Mostrar atajos del teclado", "StartSearch": "Empezar una búsqueda", "StartSearchClear": "Comenzar nueva búsqueda", "Tab": "TABULADOR", "Vault": "Enviar objeto al depósito" }, "InGameLoadout": { "ClearSlot": "Limpiar Ranura {{index}}", "Create": "Crear Equipación", "CreateTitle": "Crear Equipación En El Juego con el Equipamiento Actual", "CurrentlyEquipped": "Actualmente Equipado", "DeleteFailed": "Fallo al borrar equipación", "Deleted": "Equipación Borrada", "DeletedBody": "Limpiar equipación en el juego en la ranura {{index}}", "EditFailed": "Fallo al actualizar equipación", "EditIdentifiers": "Editar Identificadores", "EditTitle": "Editar Nombre e Icono de la Equipación", "EquipNotReady": "Equipar En El Juego No Listo", "EquipReady": "Equipar En El Juego Listo", "LoadoutDetails": "Detalles de la Equipación", "MatchingLoadouts": "Equipaciones Coincidentes:", "PrepareEquip": "Preparar Equipación", "Replace": "Reemplazar Equipación {{index}}", "Save": "Actualizar Equipación", "SaveIdentifiers": "Actualizar Identificadores", "SnapshotFailed": "Fallo al capturar equipación equipada" }, "Infusion": { "Filter": "Filtrar objetos", "InfuseSource": "Selecciona objeto en el que infundir {{name}}", "InfuseTarget": "Selecciona objeto para infundir en {{name}}", "InfusionMaterials": "Materiales de Infusión", "NoItems": "No hay materiales de infusión disponibles.", "NoTransfer": "El material de infusión para transferir\n {{target}} no se pudo mover.", "SwitchDirection": "Cambiar", "TransferItems": "Transferir" }, "Inventory": { "ClickToExpand": "(Clic para expandir)", "MissingSilver": "Tu balance de Plata solo está disponible mientras estás jugando al juego." }, "Item": { "SetBonus": { "NPiece_one": "{{count}} Pieza", "NPiece_other": "{{count}} Piezas" }, "ThumbsDown": "Pulgar Abajo", "ThumbsUp": "Pulgar Arriba" }, "ItemFeed": { "ClearFeed": "Limpiar Recientes", "Description": "Objetos Recientes", "HideTagged": "Ocultar Etiquetados", "NoNewItems": "No hay nuevos objetos", "ShowOlderItems": "Mostrar objetos más antiguos" }, "ItemMove": { "Consolidate": "Consolidado {{name}}", "Distributed": "{{name}} Distribuidos.\nAhora {{name}} se encuentra dividido entre los personajes de manera equitativa.", "MovingItem": "Transferir al depósito", "MovingItem_female": "Transferir a {{target}}", "MovingItem_male": "Transferir a {{target}}", "ToStore": "Todos los {{name}} ahora están en tu {{store}}.", "ToVault": "Todos los {{name}} ahora están en tu depósito." }, "ItemPicker": { "ChooseItem": "Elige un objeto:", "SearchPlaceholder": "Buscar objetos" }, "ItemService": { "BucketFull": { "Guardian": "Hay demasiados '{{itemtype}}' objetos en tu {{store}}.", "Guardian_female": "Hay demasiados '{{itemtype}}' objetos en tu {{store}}.", "Guardian_male": "Hay demasiados '{{itemtype}}' objetos en tu {{store}}.", "Vault": "Hay demasiados '{{itemtype}}' objetos en tu {{store}}." }, "Classified": "Este objeto está \"clasificado\" y no puede transferirse en este momento.", "Classified2": "Objeto Clasificado. Bungie no aporta ningún tipo de información sobre este objeto. Añade notas a este objeto y usa el filtro de búsqueda \"notes:\" para encontrarlo.", "Deequip": "No se pudo encontrar otro objeto para equipar con el fin de desequipar {{itemname}}", "ExoticError": "'{{itemname}}' no puede ser equipado porque el objeto exótico en el espacio {{slot}} no puede ser desequipado. ({{error}})", "NotEnoughRoom": "No hay nada que podamos sacar fuera de {{store}} para hacerle espacio a {{itemname}}", "NotEnoughRoomGeneral": "No hay espacio suficiente para mover este objeto.", "OnlyEquippedClassLevel": "Esto solo puede equiparse en un {{class}} de nivel igual o superior a {{level}}.", "OnlyEquippedLevel": "Esto solo puede equiparse en personajes de nivel igual o superior a {{level}}.", "PostmasterAlmostFull": "¡Casi lleno!", "PostmasterFull": "¡Lleno!", "PreviewVendor": "Previsualizar {{type}} contenidos", "StackFull": "Ya tienes un montón completo de {{name}}", "StoreName": "{{genderRace}}{{className}}" }, "KillType": { "ClassAbilities": "Habilidad de Clase", "Finisher": "Remate", "Grenade": "Granada", "Melee": "Cuerpo a Cuerpo", "Precision": "Precisión", "Super": "Súper" }, "LB": { "AddStack": "Añadir otra copia de este modificador", "AdvancedOptions": "Opciones Avanzadas", "ChooseAMod": "Elige tus modificadores", "ChooseASetBonus": "Elige tu bonificación de conjunto", "ChooseAnExotic": "Elige tu exótico", "ClearLocked": "Limpiar Bloqueados", "ContainsVendorItems": "Esta equipación contiene objetos de comerciantes", "Current": "Actual", "Equip": "Equipar al {{character}}", "Exclude": "Objetos excluidos", "ExcludeHelp": "SHIFT+CLICK en un objeto (o arrástralo y suéltalo en esta caja) para crear equipaciones sin equipo específico.", "ExistingBuildStats": "Estadísticas de Equipación Existentes", "ExistingBuildStatsNote": "Mostrando solo equipaciones con estadísticas estríctamente más altas.", "FilterSets": "Conjuntos de filtros", "Help": { "And": "Armaduras con todas estas ventajas serán usadas (\"y\")", "ChangeNodes": "Cambia los nodos de Intelecto, Disciplina y Fuerza en el juego a lo que se muestra para crear cada equipo.", "Discipline": "La Disciplina acelera el tiempo de recarga de la Granada", "DragAndDrop": "Arrastra y suelta objetos en las cajas fijas para crear equipaciones con ese equipo solamente", "Help": "¿Necesitas ayuda?", "HigherTiers": "Niveles Altos son mejores", "Intellect": "El Intelecto acelera el tiempo de recarga de la Súper", "Lock": "Fija un conjunto de ventajas dando clic al botón de fijar y seleccionando las mejoras", "MultiPerk": "Para usar armadura con múltiples ventajas juntas, presiona SHIFT+CLIC en las ventajas deseadas", "NoPerk": "Si una ventaja no aparece, quiere decir que no tienes armadura que cuente con esa ventaja", "Or": "Las armaduras con cualquiera de estas ventajas serán usadas (\"o\")", "ShiftClick": "Haga SHIFT+CLIC en un objeto para crear equipaciones sin ese equipo", "StatsIncrease": "Mientras el nivel de defensa de los objetos incremente, las estadísticas en ese objeto (int/dis/fuer) también incrementan.", "Strength": "La Fuerza acelera el tiempo de recarga del Cuerpo a Cuerpo", "Synergy": "Intenta encontrar armadura que tenga ventajas de incremento de munición para tipos de armas que uses.", "Tier11Example": "4/5/2 (una equipación de Nivel 11) es 4 de Intelecto, 5 de Disciplina y 2 de Fuerza (4+5+2 = Nivel 11)" }, "HideAllConfigs": "Ocultar todas las configuraciones", "HideConfigs": "Ocultar configuraciones", "IncompatibleWithOptimizer": "Este objeto es incompatible con el Optimizador. Por favor, readquiera una versión nueva desde Colecciones.", "LB": "Optimizador de Equipaciones", "LightMode": { "HelpCurrent": "Calcula las equipaciones con los niveles de defensa actuales.", "HelpScaled": "Calcula las equipaciones como si todos los objetos tuvieran 350 de defensa.", "LightMode": "Modo claro" }, "Loading": "Cargando las mejores equipaciones", "LockEquipped": "Bloquear Equipados", "LockPerk": "Bloquear ventaja", "Locked": "Objetos Bloqueados", "LockedHelp": "Arrastrar y soltar cualquier objeto en su caja para crear conjunto con ese equipamiento específico. SHIFT+CLIC para excluir objetos.", "Missing2": "¡Faltan piezas raras, legendarias o exóticas para crear una equipación completa!", "ProcessingMode": { "Fast": "Rápido", "Full": "Completo", "HelpFast": "Solo mira tu mejor equipamiento.", "HelpFull": "Mira más equipamiento, pero tarda más.", "ProcessingMode": "Modo de procesamiento" }, "RemoveStack": "Eliminar una copia de este modificador", "Scaled": "Escalado", "SearchAMod": "Buscar por nombre del modificador o descripción", "SearchASetBonus": "", "SearchAnExotic": "Buscar por nombre de exótico o descripción", "SelectExotic": "Seleccionar exótico", "SelectMods": "Seleccionar Modificadores", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Modificadores de Actividad", "SelectSetBonus": "Elige Bonificación De Conjunto", "SelectSubclassOptions": "Personalizar subclase", "ShowAllConfigs": "Mostrar todas las configuraciones", "ShowConfigs": "Mostrar configuraciones", "ShowGear": "{{class}} Armadura", "Vendor": "Incluir objetos de Comerciantes" }, "Loading": { "Accounts": "Cargando cuentas de Destiny...", "Code": "Cargando código de DIM...", "FilterHelp": "Cargando ayuda de búsqueda...", "Profile": "Cargando perfil de Destiny...", "Vendors": "Cargando comerciantes de Destiny..." }, "LoadoutAnalysis": { "Analyzed": "Analizadas {{numLoadouts}} Equipaciones", "Analyzing": "Analizando {{numAnalyzed}}/{{numLoadouts}} Equipaciones", "BetterStatsAvailable": { "Description": "Eligiendo armaduras o modificadores diferentes para esta equipación permitirá alcanzar estadísticas más altas. Elige \"$t(Loadouts.OpenInOptimizer)\" para ver mejores equipaciones.", "Name": "Mejores Estadísticas Disponibles" }, "BetterStatsAvailableFontNote": "Nota: Esta Equipación usa los modificadores \"Manantial de ...\" que causan que una estadística exceda 200. DIM podría identificar mejores estadísticas reduciendo la cantidad de exceso de estadísticas. Si no se desea esto, desactiva \"$t(Loadouts.IncludeRuntimeStatBenefits)\" en la Equipación.", "DoesNotRespectExotic": { "Description": "Esta configuración de equipación del Optimizador de Equipaciones especifica una elección de exótico, pero la equipación no coincide con ese exótico.", "Name": "Exótico Incorrecto" }, "DoesNotSatisfyStatConstraints": { "Description": "La configuración del Optimizador de Equipaciones para esta Equipación especifica unos mínimos de estadísticas, pero la Equipación no las alcanza.", "Name": "Mínimos de Estadísticas Incorrectos" }, "EmptyFragmentSlots": { "Description": "Hay ranuras de fragmentos vacías en esta subclase.", "Name": "Ranuras de Fragmentos Vacías" }, "InvalidMods": { "Description": "Algunos modificadores de esta equipación están obsoletos o no encajan de otra manera en cualquiera de tus piezas de armadura.", "Name": "Modificadores Obsoletos" }, "InvalidSearchQuery": { "Description": "Esta equipación fue creada con una consulta de búsqueda en el Optimizador de Equipaciones que no es válida.", "Name": "Consulta de Búsqueda Inválida" }, "ItemsDoNotMatchSearchQuery": { "Description": "Esta equipación fue creada con una consulta de búsqueda en el Optimizador de Equipaciones, y esa consulta de búsqueda excluye al menos uno de los objetos en la equipación.", "Name": "La Búsqueda Excluye Objetos" }, "MissingItems": { "Description": "Algunos de los objetos en esta equipación ya no están en tu inventario.", "Name": "Faltan Objetos" }, "ModsDontFit": { "Description": "La armadura en esta equipación no se puede acomodar a todos los modificadores de la equipación, incluso si la armadura fue mejorada.", "Name": "Modificadores Sin Asignar" }, "NeedsArmorUpgrades": { "Description": "La armadura en esta equipación necesita ser mejorada para acomodar todos los modificadores o alcanzar estadísticas especificadas.", "Name": "Necesita Mejoras de Armadura" }, "NotAFullArmorSet": { "Description": "Esta equipación no pudo ser analizada a fondo porque no incluía un conjunto completo de armadura.", "Name": "No Es Un Set De Armadura Completo" }, "TooManyFragments": { "Description": "Hay más fragmentos configurados en la subclase que los otorgados por los aspectos.", "Name": "Demasiados Fragmentos" }, "UsesSeasonalMods": { "Description": "Esta equipación se basa en modificadores que sólo están disponibles en algunas temporadas. Cuando la temporada termina, algunos modificadores no estarán disponibles o excederán la capacidad de energía de la armadura.", "Name": "Usa Modificadores de Temporada" } }, "LoadoutBuilder": { "All": "Todo", "AlwaysAutoMods": "Los modificadores Artificiosos y de Ajuste siempre serán elegidos automáticamente.", "AnyExotic": "Cualquier Exótico", "AnyExoticDescription": "Las equipaciones deben contener un exótico, pero cualquier exótico servirá.", "Artifice": "Artificiosa", "AssumeMasterwork": "Asumir Obra Maestra", "AssumeMasterworkOptions": { "All": "Todas las Armaduras: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "Todas las Armaduras: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExóticos de Armadura 2.0: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Mejorada para aceptar modificadores de estadísticas Artificiosas.", "Current": "Estadísticas actuales, asumiendo nivel de energía de al menos {{minLoItemEnergy}}.", "Legendary": "Legendarias: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExóticas: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Bonificación de estadísticas de Obra Maestra Completa, asumiendo nivel de energía de al menos 10.", "None": "Todas las armaduras: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "Añadir automáticamente modificadores de estadísticas", "AutomaticallyPicked": "Este modificador fue añadido automáticamente para mejorar las estadísticas de la equipación.", "CompareLoadout": "Comparar Equipación", "ConfirmOverwrite": "¿Estás seguro de que quieres reemplazar la armadura en la equipación \"{{name}}\" con este nuevo set de armadura?", "DecreaseStatPriority": "Reducir prioridad a las estadísticas", "DisabledByAutoStatMods": "Los modificadores de estadísticas están siendo elegidos automáticamente por el Optimizador de Equipaciones.", "DisabledDueToMaintenance": "El Optimizador de Equipaciones está actualmente desactivado debido al mantenimiento con la API de Bungie.", "EquipItems": "Equipar", "ExcludeItem": "Excluir objeto", "ExcludeVendors": "Busca \"not:vendor\" para excluir objetos de comerciantes del Optimizador de Equipaciones.", "ExcludedItems": "Objetos Excluidos", "ExistingLoadout": "Equipación Existente", "Exotic": "Armadura Exótica", "ExoticClassItemPerks": "Si quieres ventajas específicas, usa búsquedas como exactperk:\"spirit of verity\". Haz clic en las ventajas de los resultados del Optimizador para añadirlas o eliminarlas del filtro de objetos.", "ExoticSpecialCategory": "Especial", "FOTLWildcardWarning": "Este conjunto contiene una máscara de La Fiesta de las Almas Perdidas. Aplica manualmente el modificador correcto para activar los bonificadores de conjunto.", "Filter": "Configuración", "IgnoreStat": "Si está desmarcado, el Optimizador de Equipaciones fingirá que esta estadística no exista cuando se fabriquen equipaciones", "IncreaseStatPriority": "Incrementar prioridad a las estadísticas", "Legendary": "Legendarias", "LimitToNewFeaturedGear": "Limitar a equipo nuevo/destacado", "LockItem": "Anclar objeto", "MissingClass": "La equipación es para: {{className}}", "MissingClassDescription": "La equipación que estás intentando visualizar es para una clase de personaje que no tienes.", "MwExotic": "Exótico", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "Una consulta de búsqueda activa está restringiendo los objetos que DIM puede incluir en sus equipaciones", "AllowAutoStatMods": "Permitir que DIM incluya automáticamente modificadores adicionales de estadística", "AlwaysInvalidMods": "Estos modificadores no encajan en ninguno de los objetos que te pertenecen:", "AssumeMasterworked": "Permitir que DIM recomiende convertir en obra maestra armadura", "AssumptionsRestricted": "DIM no puede recomendar cambios de energía de armadura:", "BadSlot": "En la ranura {{bucketName}} ninguno de los objetos permitidos pudieron acomodar estos modificadores:", "ExoticDoesNotExist": "No tienes ninguna de las armaduras exóticas seleccionadas en tu inventario.", "Header": "No se encontraron equipaciones. Aquí hay posibles razones por las que DIM no pudo encontrar equipaciones:", "LowerBoundsFailed": "Muchas equipaciones no cumplieron los requisitos de estadísticas mínimas", "MaybeAllowMoreItems": "Considera permitir otros objetos:", "MaybeDecreaseLowerBounds": "Considera reducir los requisitos de estadísticas mínimas", "MaybeRemoveMods": "Considera eliminar algunos modificadores:", "MaybeRemoveSearchQuery": "Considera limpiar o cambiar el filtro en la barra de búsqueda", "ModAssignmentFailed": "Muchas equipaciones no pudieron acomodar todos los modificadores solicitados", "RemoveMods": "Elimina estos modificadores", "RemoveSetBonuses": "Considera eliminar algunas bonificaciones de conjunto", "SetBonuses": "Has elegido algunos bonificadores de conjunto, quizás no tengas los objetos correctos para usarlos." }, "NoExotic": "No Exótico", "NoExoticDescription": "Equivalente a buscar \"not:exotic\" en la barra de búsqueda (los conjuntos no usarán ninguna armadura exótica).", "NoExoticPreference": "Sin Exóticos Seleccionados", "NoExoticPreferenceDescription": "Armadura exótica será usada si maximiza las estadísticas.", "NoLoadoutsToCompare": "No hay equipaciones para comparar", "None": "Ninguno", "OptimizerExplanationGuide": "Lea la Guía de Usuario para más información y un vídeotutorial.", "OptimizerExplanationMods": "Elige un exótico, modificadores y una subclase. Esto contribuirá a las estadísticas de la equipación, mientras que los modificadores que ya estaban en la armadura serán ignorados.", "OptimizerExplanationSearch": "Usa la barra de búsqueda para reducir qué armadura considerar, ej. {{example}}. Si ninguna armadura en una ranura coincide con la búsqueda, todos los objetos serán considerados para esa ranura.", "OptimizerExplanationStats": "Arrastra las estadísticas más importantes arriba y desmarca las que no quieras maximizar.", "OptimizerSet": "Set Optimizador", "PinnedItems": "Objetos Fijados", "PinnedItemsFinePrint": "Los filtros de búsqueda están guardados con la configuración del Optimizador de Equipaciones, pero los objetos marcados y excluidos no. Los marcados y las exclusiones serán ignoradas cuando DIM compruebe Equipaciones existentes para mejores estadísticas de equipaciones.", "ProcessingSets": "Encontrando los conjuntos de estadísticas más altas...", "SaveAs": "Guardar como", "SetBonus": "Bonificación de Conjunto", "SpeedReport": "Evaluadas {{combos, number} combinaciones en {{time}} segundos usando {{cpus}} núcleos del CPU.", "StatConstraints": "Prioridades & Rangos de Estadísticas", "StatMax": "Máx", "StatMin": "Min", "StatRangeTooltip": "Con la configuración actual de min/max, existen equipaciones que tienen de {{min}} a {{max}} puntos en esta estadística. Haz doble clic para configurar el min a {{max}}.", "StatTotal": "Total: {{total}}", "TierNumber": "N{{tier}}", "UnableToAddAllMods": "No se pudieron añadir todos los modificadores.", "UnableToAddAllModsBody": "No había suficientes ranuras disponibles para modificadores que encajaran {{mods}}.", "UnlockItem": "Desanclar objeto" }, "LoadoutFilter": { "Contains": "Muestra equipaciones las cuales tengan un objeto o un modificador que coincidan con el texto filtrado. Busca por objetos con espacios en su nombre usando comillas.", "FashionOnly": "Muestra equipaciones que contengan solo cosméticos (shaders o diseños).", "LoadoutLight": "Muestra equipaciones basadas en su nivel de luz calculado. Usa la palabra clave \"pinnaclecap\" o \"softcap\" en vez de un número para referirse a límites de poder de la temporada actual.", "ModsOnly": "Muestra equipaciones que solo contengan modificadores de armadura.", "Name": "Muestra equipaciones cuyo nombre coincida exactamente (exactname:) o parcialmente (name:) con el texto filtrado. Busca frases enteras usando comillas.", "Notes": "Busca por equipaciones por el campo de sus notas.", "PartialMatch": "Muestra equipaciones donde su nombre o notas tengan una coincidencia parcial con el texto filtrado. Busca por frases enteras usando comillas.", "Season": "Muestra equipaciones según la temporada de Destiny 2 en la que fueron modificados por última vez.", "Subclass": "Muestra equipaciones cuyo nombre de subclase o tipo de daño coincida parcialmente con el texto filtrado." }, "Loadouts": { "Abilities": "Habilidades", "Actions": "Acciones para {{title}}", "AddEquippedItems": "Añadir Equipados", "AddNotes": "Añadir Notas", "AddUnequippedItems": "Añadir Desequipados", "Any": "Cualquier clase", "Apply": "Aplicar", "ApplyInGameLoadoutInGame": "Tu equipación está lista para equipar, pero ya que estás en una actividad necesitas equiparla en el juego.", "ApplyMods": "Aplicando modificadores", "ApplySearch": "Transferir búsqueda \"{{query}}\"", "ArmorStats": "Estadísticas de Armadura", "ArtifactUnlocks": "Desbloqueos de Artefacto", "ArtifactUnlocksDesc": "Debido a limitaciones de Bungie.net, DIM no puede configurar automáticamente tu artefacto. Necesitas realizar estos desbloqueos en el juego antes de aplicar la Equipación.", "ArtifactUnlocksWithSeason": "Desbloqueos de Artefacto - T{{seasonNumber}}", "BadLoadoutShare": "No se pudo cargar la equipación compartida", "BadLoadoutShareBody": "La equipación que estás intentando cargar es inválida: {{error}}", "Before": "Antes '{{name}}'", "CancelEditing": "Cancelar Edición", "CannotCustomizeSubclass": "Esta subclase no puede ser configurada", "ChooseItem": "Añadir {{name}}", "ClassType": "Equipación para cualquier clase", "ClassTypeMismatch": "Un objeto de {{className}} no puede ser añadido a esta equipación", "ClassTypeMissing": "No tienes un {{className}} para el que crear una equipación", "ClassType_female": "{{className}} equipación", "ClassType_male": "{{className}} equipación", "Classified": "Algunos de tus objetos son \"Clasificados\", y no pueden ser incluidos en el cálculo de luz máxima.", "ClearLoadoutParameters": "Eliminar configuración del Optimizador de Equipaciones", "ClearSection": "Eliminar todo", "ClearSpace": "Mover otros fuera", "ClearSpaceArmor": "Mover otras armaduras fuera", "ClearSpaceWeapons": "Mover otras armas fuera", "ClearUnsetMods": "Eliminar otros modificadores", "ClearingSpace": "Moviendo otros objetos fuera", "CopyAndEdit": "Editar Copia", "Create": "Crear Equipación", "CurrentlyEquipped": "Actualmente Equipado", "Deequip": "Desequipando objetos de otros personajes", "Delete": "Borrar", "DimLoadouts": "Equipaciones DIM", "Edit": "Editar Equipación", "EditBrief": "Editar", "EquipInGameLoadout": "Equipando tu equipación en el juego", "EquipItems": "Equipando objetos", "EquippableDifferent1": "Múltiples objetos Exóticos fueron usados para calcular tu Poder Máximo, por lo que el número mostrado podría no alcanzarse cuando equipes tus objetos dentro del juego.", "EquippableDifferent2": "El Poder Máximo no está limitado por la regla del \"Exótico Único\" cuando se determina el Poder de tus recompensas de botines/poderosos/pináculo.", "Failed": "La equipación falló por completo al aplicar", "Fashion": "Elegir cosméticos", "FashionOnly": "Solo-Moda", "FillFromEquipped": "Rellenar usando equipados", "FillFromInventory": "Rellenar usando no equipados", "FilteredItems": "Objetos filtrados", "FindAnother": "Encontrar otro {{name}}", "FromEquipped": "Equipado", "Generated": "{{statTotal}} Puntos de Estadísticas de Equipación", "HashtagTip": "Consejo: Usa #hashtags en los nombres o notas de tu equipación y se mostrarán aquí.", "Import": { "BadURL": "No es un enlace válido de compartición de equipación.", "Error": "Error obteniendo equipación:", "Error404": "Esta equipación no existe.", "PasteHere": "Pega un enlace de equipación para abrir la equipación." }, "ImportLoadout": "Importar Equipación", "InGameActions": "Acciones para Equipaciones En El Juego", "InGameLoadouts": "Equipaciones En El Juego", "IncludeRuntimeStatBenefits": "Incluir estadísticas de modificador Manantial", "IncludeRuntimeStatBenefitsDesc": "Los modificadores de armadura de \"Manantial de ...\" proporcionan una mejora lineal a las estadísticas del personaje mientras tengas Cargas de Armadura.\n\nCon esta configuración, DIM considera estos modificadores activos y añade estos beneficios a las estadísticas de esta Equipación en cálculos y optimizaciones.", "ItemErrorSummary_one": "1 error de objeto:", "ItemErrorSummary_other": "{{count}} errores de objetos:", "ItemLeveling": "Nivel de Objetos", "LoadoutName": "Nombre de la equipación", "LoadoutParameters": "Configuración del Optimizador de Equipaciones", "LoadoutParametersExotic": "La equipación debe incluir este exótico: {{exoticName}}", "LoadoutParametersQuery": "Los objetos deben coincidir con este filtro de búsqueda", "LoadoutParametersStats": "Prioridad de estadísticas y rangos de estadísticas mínimos/máximos", "Loadouts": "Equipaciones", "MakeRoom": "Hacer espacio en Administración", "MakeRoomDone_female_one": "Se terminó de hacer espacio para 1 objeto de la Administración al mover 1 objeto fuera de {{store}}.", "MakeRoomDone_female_other": "Se terminó de hacer espacio para {{count}} objetos de la Administración al mover {{movedNum}} objetos fuera de {{store}}.", "MakeRoomDone_male_one": "Se terminó de hacer espacio para 1 objeto de la Administración al mover 1 objeto fuera de {{store}}.", "MakeRoomDone_male_other": "Se terminó de hacer espacio para {{count}} objetos de la Administración al mover {{movedNum}} objetos fuera de {{store}}.", "MakeRoomDone_one": "Se terminó de hacer espacio para 1 objeto de la Administración al mover 1 objeto fuera de {{store}}.", "MakeRoomDone_other": "Se terminó de hacer espacio para {{count}} objetos de la Administración al mover {{movedNum}} objetos fuera de {{store}}.", "MakeRoomError": "Imposible hacer espacio para todos los objetos en la Administración: {{error}}.", "ManageLoadouts": "Administrar Equipaciones", "MaxSlots": "Solo puedes tener {{slots}} {{bucketName}} en un equipamiento.", "MaximizeLight": "Luz Máxima", "MaximizePower": "Poder máximo", "MaximizeStat": "Maximizar Estadística", "MissingItemsWarning": "Algunos de los objetos en esta equipación ya no están en tu inventario.", "ModErrorSummary_one": "1 Error de modificador:", "ModErrorSummary_other": "{{count}} errores de modificadores:", "ModPlacement": { "InvalidMods": "Modificadores No Válidos", "InvalidModsDesc_one": "1 modificador no pudo encajar en ninguna pieza de armadura.", "InvalidModsDesc_other": "{{count}} modificadores no pudieron encajar en ninguna pieza de armadura.", "ModPlacement": "Colocación de Modificadores", "StackableMod": "Acumulable", "UnassignedMods": "Modificadores Sin Asignar", "UnassignedModsDesc_one": "1 modificador no encajó debido a energía insuficiente o ranuras para modificadores. Las mejoras de energía para la armadura seleccionada no arreglarán este problema.", "UnassignedModsDesc_other": "{{count}} modificadores no encajaron debido a energía insuficiente o ranuras para modificadores. Las mejoras de energía para la armadura seleccionada no arreglarán este problema.", "UnstackableMod": "No Acumulable", "UpgradeCosts": "Costes de Mejoras", "UpgradeCostsDesc": "Algunas armaduras necesitan mejoras de capacidad de energía para encajar los modificadores solicitados. En total, estas mejoras cuestan:" }, "Mods": "Modificadores", "ModsOnly": "Solo-Mods", "MoveItems": "Moviendo objetos", "NoSpace": "Te has quedado sin espacio en tu depósito o cualquiera de tus personajes.", "NoneMatch": "Ninguna de tus equipaciones coincidió con los filtros.", "NotStarted": "Esperando a que otras acciones se completen, o a que se actualice el inventario para terminar de cargar", "NotesPlaceholder": "Escribe algunas notas sobre esta equipación, o usa #hashtags para categorizarla", "NotificationTitle": "Equipación: {{name}}", "OnWrongCharacterAdvice": "Haga clic aquí para encontrar los objetos de Poder más altos para este personaje.", "OnWrongCharacterWarning": "La armadura más poderosa de este personaje está en otro personaje. Para contar el Poder del botín, recompensas poderosas e hito, la armadura debe estar en este personaje o en el Depósito.", "OnlyItems": "Solo objetos equipables, materiales y consumibles pueden añadirse a una configuración.", "OpenInOptimizer": "Optimizar Armadura", "OpenOnStreamDeck": "Abrir en Stream Deck", "PickArmor": "Escoger Armadura", "PickMods": "Añadir modificadores de armadura", "Prismatic": { "Aspect": "Aspecto Prismático", "Grenade": "Granada Prismática", "Melee": "Cuerpo a cuerpo Prismático", "Super": "Super Habilidad" }, "PullFromPostmaster": "Recoger de Administración", "PullFromPostmasterError": "No se pudo extraer de Administración: {{error}}.", "PullFromPostmasterGeneralError": "No se pueden extraer todos los objetos de Administración.", "PullFromPostmasterNotification_female_one": "Extrayendo 1 objeto de Administración a {{store}}.", "PullFromPostmasterNotification_female_other": "Extrayendo {{count}} objetos de Administración a {{store}}.", "PullFromPostmasterNotification_male_one": "Extrayendo 1 objeto de Administración a {{store}}.", "PullFromPostmasterNotification_male_other": "Extrayendo {{count}} objetos de Administración a {{store}}.", "PullFromPostmasterNotification_one": "Extrayendo 1 objeto de Administración a {{store}}.", "PullFromPostmasterNotification_other": "Extrayendo {{count}} objetos de Administración a {{store}}.", "PullFromPostmasterPopupTitle": "Extraer de Administración", "Random": "Aleatorio", "Randomize": "Aleatorizar Equipación", "RandomizeButton": "Aleatorizar", "RandomizeNew": "Crear Aleatoria", "RandomizeQueryHint": "Consejo: Busca objetos primero para restringir cuales pueden ser aleatoriamente elegidos.", "RandomizeSearch": "Aleatorizar desde la Búsqueda", "RandomizeSearchPrompt": "¿Aleatorizar tus objetos equipados desde la búsqueda \"{{query}}\"?", "Redo": "Rehacer", "RestoreAllItems": "Todos los Objetos", "SalvationsEdgeMods": "Modificadores de El Filo de la Salvación", "Save": "Guardar", "SaveAsDIM": "Guardar como Equipación para DIM", "SaveAsNew": "Guardar como Nuevo", "SaveAsNewTooltip": "Mantener la equipación original y guardar esta como una nueva", "SaveDisabled": { "AlreadyExists": "Elige un nuevo nombre para la equipación.", "Empty": "La equipación está vacía.", "NoName": "Esta equipación necesita un nombre." }, "SaveLoadout": "Guardar Equipación", "Season": "Temporada {{season}}", "SetBonusesDesc": "Bonificación de conjunto requerida", "Share": { "Copied": "Copiado enlace de la equipación al portapapeles", "CopyButton": "Copiar Enlace", "Error": "Error obteniendo enlace para compartir", "Fashion": "Moda (shaders y diseños)", "LoadoutOptimizer": "Configuración del Optimizador de Equipaciones", "NativeShare": "Compartir Enlace", "Notes": "Notas", "NumItems_one": "{{count}} objeto - el recipiente será preguntado para seleccionar a un objeto comparable de su inventario", "NumItems_other": "{{count}} objetos - los recipientes serán preguntados para seleccionar objetos comparables en su inventario", "NumMods_one": "{{count}} modificador", "NumMods_other": "{{count}} modificadores", "Placeholder": "Cargando enlace compartido", "Subclass": "Personalización de subclase", "Summary": "Compartir esta equipación que contiene:", "Title": "Compartir \"{{name}}\"" }, "ShareLoadout": "Compartir", "ShowModPlacement": "Mostrar Colocación de Modificadores", "Snapshot": "Guardar como Equipación En El Juego", "SocketOverrides": "Cambiando opciones de subclase", "SortByEditTime": "Ordenar por última edición", "SortByName": "Ordenar por nombre", "SubclassOptions": "Opciones de {{subclass}}", "SubclassOptionsSearch": "Buscar opciones de {{subclass}}", "Succeeded": "Equipación exitosa", "SyncFromEquipped": "Sincronizar desde equipados", "TooManyRequested": "Tienes {{total}}{{itemname}} pero tu equipación preguntaba por {{requested}}. Transferimos todo lo que tenías.", "TuningMods": "Modificadores de Ajuste", "UnassignedModError": "El modificador no encajó en tu armadura actual", "Undo": "Deshacer", "Update": "Guardar Cambios", "UpdateLoadout": "Actualizar Equipación", "VendorsCannotEquip": "No tienes estos objetos. Pulsa para escoger otro reemplazo o clica la X para quitar:" }, "Manifest": { "Download": "Descargando la última información de la base de datos desde Bungie...", "Error": "Error cargando información de la base de datos de Destiny:\n{{error}}\nRecarga para reintentar.", "Load": "Cargando información de la base de datos de Destiny..." }, "Milestone": { "Daily": "Desafío Diario", "OneTime": "Desafío Único", "SeasonalRank": "Nivel de Temporada {{rank}}", "Special": "Desafío de Evento Especial", "Tutorial": "Desafío Tutorial", "Unknown": "Desafío", "Weekly": "Desafío Semanal" }, "Mods": { "HarmonicModDescription": "El efecto de este modificador tiene un coste reducido y cambia el elemento dependiendo de la subclase equipada." }, "MoveAmount": { "Amount": "Cantidad:" }, "MovePopup": { "Acquired": "Este objeto está desbloqueado en colecciones.", "AcquiredMod": "Este modificador está desbloqueado en colecciones.", "AddNote": "Añadir notas", "AddToLoadout": "Equipación", "AddToLoadoutTitle": "Añadir esto a la equipación", "All": "Todo", "ArtifactBreaker": "Esta arma tiene {{breaker}} debido a una ventaja de artefacto desbloqueada.", "CannotCurrentlyRoll": "Esta ventaja no se puede obtener en la versión actual de este objeto.", "CantPullFromPostmaster": "Debes visitar Administración en el juego para recuperar este objeto.", "CatalystProgress": "Progreso del Catalizador", "CommunityData": "Información de la Comunidad", "Consolidate": "Consolidar", "DistributeEvenly": "Distribuye Equitativamente", "EnhancementTier": "Nivel {{tier}}", "Equip": "Equipar en:", "EquipWithName": "Equipar al {{character}}", "FavoriteUnFavorite": { "Favorite": "Marcar favorito {{itemType}}", "Favorited": "Favorito", "Unfavorite": "Desmarcar favorito {{itemType}}", "Unfavorited": "No Favorito" }, "Infuse": "Infundir", "InfuseTitle": "Abrir el buscador de infusión de objetos", "IntrinsicBreaker": "Esta arma tiene intrínsicamente {{breaker}}.", "LoadingSockets": "Las ventajas y los detalles de estadísticas no han cargado todavía para este objeto.", "LockUnlock": { "AutoLock": "El estado de bloqueo se sincronizó con la etiqueta de este objeto", "Lock": "Bloquear {{itemType}}", "Locked": "Bloqueado", "Unlock": "Desbloquear {{itemType}}", "Unlocked": "Desbloqueado" }, "MissingSockets": "Los detalles de modificadores y ventajas no estarán disponibles mientras Bungie esté actualizando sus servicios. Volverán cuando estén listos, normalmente en unas pocas horas.", "Notes": "Notas:", "OpenOnStreamDeck": "Abrir en Stream Deck", "OverviewTab": "Resumen", "Owned": "Este objeto está en tu inventario.", "OwnedMod": "Este modificador está en el inventario de modificadores.", "PullItem": "Traer de {{bucket}} a {{store}}", "PullPostmaster": "Extraer de Administración", "ReadLore": "Leer historia en Ishtar Collective", "ReadLoreLink": "Leer historia", "Rewards": "Recompensas:", "SendToVault": "Enviar al Depósito", "Store": "Llevar a:", "StoreWithName": "Llevar a {{character}}", "Subtitle": { "QuestProgress": "Paso {{questStepNum}} de {{questStepsTotal}}", "Type": "{{classType}} {{typeName}}" }, "TabList": "Pestañas de detalles de objetos", "ToggleSidecar": "Expandir o colapsar acciones de objeto", "TrackUntrack": { "Track": "Seguir {{itemType}}", "Tracked": "Seguido", "Untrack": "No seguir {{itemType}}", "Untracked": "No seguido" }, "TriageTab": "Triaje", "UnreliablePerkOption": "Esta ventaja sólo aparece en la vista de colecciones. Podría no aparecer aleatoriamente como tirada en este objeto.", "Vault": "Depósito", "WeaponLevel": "Nivel de Arma {{level}}" }, "Notes": { "Error": "¡Error! Máximo 120 caracteres por notas.", "Help": "Añadir notas, #hashtags y :symbols:" }, "Notification": { "Cancel": "Cancelar", "OK": "Descartar" }, "Objectives": { "Complete": "Completo", "Incomplete": "Incompleto" }, "Organizer": { "BulkMove": "Mover A", "BulkMoveLoadoutName": "Seleccionado en el Organizador", "BulkTag": "Etiqueta", "Columns": { "Ammo": "Munición", "Archetype": "Arquetipo", "BaseStats": "Estadísticas Base", "Breaker": "Rompedor", "Crafted": "Fecha Formación", "CustomTotal": "Total Personalizado", "Damage": "Daño", "Energy": "Energía", "Event": "Evento", "Featured": "Equipamiento Nuevo", "Foundry": "Fundición", "Frame": "Armazón", "Harmonizable": "Armonizable", "Holofoil": "Holometalizadas", "Icon": "Icono", "ItemTier": "Nivel", "KillTracker": "Bajas", "Level": "Nivel", "Loadouts": "Equipaciones", "Location": "Ubicación", "Locked": "Bloqueado", "MasterworkStat": "Estadísticas OM", "MasterworkTier": "Nivel OM", "ModSlot": "Ranura para Modificador", "Mods": "Modificadores", "Name": "Nombre", "New": "Nuevo", "Notes": "Notas", "OriginTraits": "Rasgo Original", "OtherPerks": "Componentes de Arma", "PercentComplete": "% Completo", "Perks": "Ventajas", "PerksGrid": "Cuadrícula de Ventajas", "Power": "Poder", "Quality": "% Calidad", "Recency": "Novedad", "Season": "Temporada", "Shaders": "Cosméticos", "Source": "Fuente", "StatQuality": "Calidad de las Estadísticas", "StatQualityStat": "{{stat}}%", "Stats": "Estadísticas", "Tag": "Etiqueta", "TertiaryStat": "3ª Estadística", "Tier": "Rareza", "Traits": "Rasgos de Armas", "TuningStat": "Afinador", "WishList": "Lista de Deseos", "WishListNotes": "Notas de la Lista de Deseos", "Year": "Año" }, "EnabledColumns": "Columnas Habilitadas", "Lock": "Bloquear", "NoItems": "No hay objetos que coincidan con los filtros. Si tienes una consulta de búsqueda, intenta limpiarla.", "NoMobile": "Gire su teléfono en horizontal para usar el Organizador.", "Note": "Establecer Notas", "OpenIn": "Mostrar en el Organizador", "Organizer": "Organizador", "SelectAll": "Seleccionar Todo", "SelectItem": "Seleccionar o deseleccionar {{name}}", "ShiftTip": "Consejo: Mantén la tecla SHIFT y haz clic en una celda para filtrar objetos", "Stats": { "Aim": "Puntería", "Airborne": "Aéreo", "AmmoGeneration": "Munición generada", "Power": "Poder", "RPM": "DPM", "Recoil": "Retroceso", "Reload": "Recarga" }, "Unlock": "Desbloquear" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "¡La Administración está casi llena! ({{number}}/{{postmasterSize}})", "PostmasterFull": "¡Administración está lleno! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "Contratos", "CatalystSource": "Fuente: {{source}}", "CrucibleRank": "NIveles", "Items": "Objetos de Aventuras", "Milestones": "Hazañas & Desafíos", "NoEventChallenges": "Has completado todos los desafíos del evento", "NoTrackedTriumph": "No tienes triunfos seguidos. Sigue tantos como quieras en DIM.", "PaleHeartPathfinder": "Pionero Débil Corazón", "PercentMax": "{{pct}}% hacia el máximo", "PercentPrestige": "{{pct}}% hacia el reinicio", "PointsUsed_one": "1 punto usado", "PointsUsed_other": "{{count}} puntos usados", "PowerBonusHeader": "+{{powerBonus}} Recompensas De Poder", "PowerBonusHeaderUndefined": "Otras Recompensas", "Progress": "Progreso", "QueryFilteredTrackedTriumphs": "Ninguno de tus triunfos seguidos coincidió con la búsqueda", "QuestExpired": "Caducado", "QuestExpires": "Expira en ", "Quests": "Aventuras", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}pts", "Resets_one": "1 reinicio", "Resets_other": "{{count}} reinicios", "RewardPassEndsIn": "Las recompensas del Pase acaban en ", "RewardPassPrestigeRank": "Rango de Prestigio {{rank}}", "SeasonalHub": "Centro de Operaciones de la Temporada", "StatTrackers": "Analizadores Estadísticos", "TrackedTriumphs": "Triunfos Seguidos" }, "RecordBooks": { "HideCompleted": "Ocultar hazañas completadas", "RecordBooks": "Libros de Hazañas" }, "Records": { "Title": "Hazañas", "UniversalOrnamentSetOther": "Otro" }, "SearchHistory": { "Date": "Último Usado", "DeleteAll": "Borrar todas las búsquedas sin marcar", "Description": "Estas son todas tus búsquedas anteriores y guardadas. Puedes borrarlas desde aquí.", "Item": "Búsquedas de Objetos", "Link": "Ver y editar el historial de búsqueda", "Loadout": "Búsquedas de Equipaciones", "Query": "Búsqueda", "Title": "Historial de Búsqueda", "UsageCount": "# Usado" }, "Settings": { "Appearance": "Apariencia", "ArmorArchetypeModslot": "Arquetipo de Armadura / Ranura para Mdificador", "AutoLockTagged": "Sincronizar el estado de bloqueo del objeto con la etiqueta", "AutoLockTaggedExplanation": "DIM automáticamente bloqueará y desbloqueará objetos que coincidan con su etiqueta. Los objetos fabricados se mantendrán desbloqueados para permitir reformarse. Cuando esta configuración esté habilitada, el icono de bloqueo no se mostrará en el mosaico para los objetos etiquetados.", "BadgePostmaster": "Muestra el número de objetos en Administración para el personaje actual en el icono de la app", "BadgePostmasterExplanation": "Para que esto funcione necesitas instalar DIM como app y tu sistema operativo debe soportar mostrar insignias", "BothDescriptions": "Ambas Descripciones", "BungieDescriptionOnly": "Descripciones de Bungie", "CharacterOrder": "Ordenar personajes por", "CharacterOrderFixed": "Edad del personaje (falla en PC)", "CharacterOrderRecent": "Personaje más reciente", "CharacterOrderReversed": "Personaje más reciente (al revés)", "ColumnSize": "{{num}} objetos", "ColumnSizeAuto": "Automático", "CommunityData": "Información de la Comunidad de las Ventajas", "CommunityDescriptionOnly": "Descripciones de la Comunidad", "CsvImport": "Importar CSV", "CustomErrorLabel": "Un nombre de estadística debe contener caracteres de palabra y deben ser diferentes de otros nombres de estadística para esta clase de Guardián.", "CustomErrorValues": "La ponderación de estadísticas deben ser números positivos.\nAl menos una ponderación de 2 estadísticas deben estar por encima de cero.", "CustomStatChooseName": "Elige un nombre de Estadística Personalizada", "CustomStatCreate": "Crear una nueva estadística personalizada", "CustomStatDelete": "Borrar esta Estadística Personalizada", "CustomStatDeleteConfirm": "¿Borrar esta Estadística Personalizada?", "CustomStatDesc1": "Elige estadísticas de armadura deseadas para crear una estadística total personalizada.", "CustomStatDesc3": "Las estadísticas personalizadas aparecerán en la Ventana Emergente, el Organizador y Comparar.", "CustomStatTitle": "Total de Estadística Personalizada", "Data": "Hojas de cálculo", "DefaultItemSizeNote": "Un objeto de 50px de tamaño parecerá lo más nítido, sin desdibujar la imagen o el texto del objeto.", "DontForgetDupes": "No te olvides de que puedes buscar is:dupe para buscar rápidamente objetos duplicados, y que puedes usar la herramienta de comparación o el organizador para evaluar objetos relacionados.", "EnableAdvancedStats": "Mostrar la valoración de la calidad de las estadísticas en la armadura (D1)", "ExpandSingleCharacter": "Mostrar todos los personajes", "ExportLoadoutSS": "Hojas de cálculo de equipamiento", "ExportLoadoutSSHelp": "Descargar una lista CSV de tus Equipaciones de DIM para que puedan ser fácilmente vistas en una aplicación de hojas de cálculo de tu elección.", "ExportProfile": "Exportar respuesta de perfil de la API", "ExportSS": "Hojas de cálculo de inventario", "ExportSSHelp": "Descargar una lista CSV de tus objetos que pueden ser vistos fácilmente en una aplicación de hojas de cálculo de tu preferencia.", "HidePullFromPostmaster": "Ocultar el botón \"$t(Loadouts.PullFromPostmaster)\"", "Inventory": "Visualización del Inventario", "InventoryColumns": "Anchura del inventario del personaje", "InventoryColumnsMobile": "Anchura del inventario del personaje en el móvil en modo retrato", "InventoryColumnsMobileLine2": "Los objetos serán redimensionados para acomodarse a la nueva configuración", "InventoryNumberOfSpacesToClear": "Número de espacios vacíos a hacer cuando se usa el Modo Recolección", "Items": "Visualización de Objetos", "Language": "Idioma", "LogOut": "Cerrar sesión", "Masterworked": "Obras Maestras Completas", "MaxParallelCores": "Núcleos máximos para tareas paralelas", "MaxParallelCoresExplanation": "Controla cuántos núcleos de la CPU puede usar DIM para tareas intensivas como el Optimizador de Equipaciones y el Analizador de Equipaciones. Valores más algo podrían mejorar el rendimiento pero usar más recursos del sistema.", "OrnamentDisplay": "Mostrar Diseños en las miniaturas de los objetos", "OrnamentDisplayExplanationDisabled": "Los objetos nunca mostrarán sus diseños", "OrnamentDisplayExplanationEnabled": "Pasar el cursor o mantener pulsado un largo tiempo una armadura ocultará su diseño", "OrnamentDisplayExplanationHide": "Pasar el cursor o mantener pulsado un largo tiempo un objeto mostrará este diseño", "OrnamentDisplayExplanationShow": "Pasar el cursor o mantener pulsado un largo tiempo un objeto ocultará este diseño", "ResetToDefault": "Reiniciar", "RestoreVaultSide": "Muestra objetos en el depósito en su propia columna", "ReverseSort": "Alternar orden normal/invertido", "SetSort": "Ordenar objetos por:", "SetVaultWeaponGrouping": "Agrupar armas del depósito por:", "Settings": "Configuración", "ShowNewItems": "Mostrar un punto rojo en objetos nuevos", "SingleCharacter": "Vista de Personaje Único", "SingleCharacterExplanation": "DIM sólo mostrará el personaje jugado más reciente.\nLos objetos en posesión por personajes ocultos aparecerán en el depósito, si pueden ser usados por el personaje actual.\nLos objetos específicos para otras clases estarán completamente ocultos.", "SizeItem": "Tamaño de objeto", "SortByAmmoType": "Tipo de Munición", "SortByAmount": "Tamaño del Montón", "SortByClassType": "Clase Requerida", "SortByCrafted": "Fabricadas (D2)", "SortByDeepsight": "Visión Profunda (D2)", "SortByFeatured": "Equipamiento Nuevo / Destacado (D2)", "SortByPrimary": "Nivel de Poder", "SortByRarity": "Rareza", "SortByRating": "Calidad de la Armadura (D1)", "SortByRecent": "Recientemente Adquiridos (D2)", "SortBySeason": "Temporada (D2)", "SortByTag": "Etiqueta ({{taglist}})", "SortByTier": "Nivel (D2)", "SortByType": "Tipo", "SortByWeaponElement": "Tipo de Daño", "SortCustom": "Orden Personalizado", "SortName": "Nombre", "SpacesSize_one": "{{count}} espacio", "SpacesSize_other": "{{count}} espacios", "Theme": "Tema", "Troubleshooting": "Resolución de problemas", "VaultArmorGroupingStyle": "Separa armadura por clase en diferentes líneas", "VaultGroupingNone": "Ninguna", "VaultUnder": "Muestra objetos en el depósito debajo de objetos equipados", "VaultWeaponGroupingStyle": "Separa grupos de armas en diferentes líneas", "WeaponFrame": "Armazón de Arma", "WishlistRefreshNotificationBody": "Si no ves ninguna actualización, ¡asegúrate de que la fuente (como GitHub) lo refleje!", "WishlistRefreshNotificationTitle": "Lista de Deseos Recargada" }, "Sockets": { "ApplyPerks": "Aplicar Ventajas", "GridStyle": "Mostrar ventajas como cuadrícula", "Insert": { "Ability": "Equipar Habilidad", "Aspect": "Insertar Aspecto", "Fragment": "Insertar Fragmento", "Mod": "Insertar Modificador", "Ornament": "Aplicar Diseño", "Projection": "Aplicar Proyección de Espectro", "Shader": "Aplicar Shader", "Super": "Equipar Súper", "Transmat": "Aplicar Efecto de Teletransporte" }, "ListStyle": "Mostrar ventajas como lista", "Search": "Buscar nombres o descripciones", "Select": { "Ability": "Previsualizar Habilidad", "Aspect": "Previsualizar Aspecto", "Fragment": "Previsualizar Fragmento", "Mod": "Previsualizar Modificador", "Ornament": "Previsualizar Diseño", "Projection": "Previsualizar Proyección de Espectro", "Shader": "Previsualizar Shader", "Super": "Previsualizar Súper", "Transmat": "Previsualizar Efecto de Teletransporte" }, "SelectWishlistPerks": "Previsualizar Ventajas de Lista de Deseos" }, "Stats": { "CrouchingSpeed": "Agachado", "Custom": "Total Personalizado", "CustomDesc": "Total personalizado de estadísticas base seleccionadas, ignorando modificadores u obras maestras. Visita Ajustes para configurar cuales estadísticas están incluidas.", "DamageResistance": "Resistencia a Daño JcE", "Discipline": "Disciplina", "DropLevel": "Poder de la Cuenta", "DropLevelExplanation1": "El Poder de la Cuenta es el nivel del poder base cuando se calcula el nivel incrementado de las recompensas.", "DropLevelExplanation2": "El Poder de la Cuenta usa el nivel más alto en cada ranura, independientemente de la Clase o de la regla del \"Exótico Único\".", "EquippableGear": "Equipo Equipable", "FlinchResistance": "Resistencia a Estremecimiento", "HP": "PV", "Intellect": "Intelecto", "MaxGearPower": "Poder Máximo del equipamiento equipable", "MaxGearPowerAll": "Poder Máximo de todo el equipamiento", "MaxGearPowerOneExoticRule": "Poder Máximo de equipo equipable\n(sólo una pieza de armadura Exótica equipada)", "MaxTotalPower": "Poder total máximo", "MetersPerSecond": "m/s", "Milliseconds": "ms", "NoBonus": "Sin Bonificación", "NotApplicable": "No Aplicable", "OfMaxRoll": "{{range}} del máximo posible de tiradas", "PercentHelp": "Haz clic para más información de lo que es la Calidad de Estadísticas.", "Percentage": "%", "PowerModifier": "Poder otorgado por la progresión de experiencia de temporada", "Prestige": "Nivel de Prestigio: {{level}}\n{{exp}}xp para 5 motas de luz.", "Quality": "Calidad de las estadísticas", "ShieldHP": "PV Escudo", "StrafingSpeed": "Movimiento lateral", "Strength": "Fuerza", "TierProgress": "N{{tier}} {{statName}} ({{progress}}/60 para N{{nextTier}})\n", "TierProgress_Max": "N{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "Tiempo hasta PV Completos", "Total": "Total", "TotalHP": "PV Totales", "WalkingSpeed": "Caminando", "WeaponPart": "Partes de Armas" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM ahora puede almacenar tus etiquetas, equipaciones y configuraciones en nuestros propios servidores y sincronizar esa información entre diferentes versiones de DIM, sin inicio de sesión separado. Puedes importar tu información existente desde la página de Configuración si no has habilitado Sincronización DIM antes. ¡Esto fue hecho posible por el apoyo de nuestros patrocinadores de OpenCollective!", "No": "Ahora no", "Title": "¿Habilitar Sincronización DIM?", "Yes": "Habilitar Sincronización" }, "AutoBackup": "Hemos hecho una copia de seguridad de tus datos en un archivo en tu carpeta de descargas llamado dim-data.json, sólo por si acaso.", "BackUpFirst": "DEBES hacer una copia de tu información primero, antes de borrarlo todo. Sólo por si acaso.", "BrowserMayClearData": "El navegador puede borrar esta información si te quedas sin espacio o no visitas DIM frecuentemente.", "DataIsLocal": "La información de las notas y las etiquetas es solo local", "DeleteAllData": "Borrar TODA la Información de los Servidores de Sincronización DIM", "DeleteAllDataConfirm": "¿Estás seguro de que quieres borrar TODA tu información, para todas las cuentas, de Sincronización DIM? No puedes deshacer esto.", "Details": { "IndexedDBStorage": "El almacenamiento local sólo guardará información en este navegador. Limpiando sus datos de navegación borrará esta información." }, "DimApiFinePrint": "DIM guardará todas tus etiquetas, equipaciones y configuraciones a los servidores de DIM y se sincronizarán entre diferentes versiones de DIM.", "DimSyncDown": "Sincronización DIM no está conectada debido a un problema comunicándose con el servidor.", "DimSyncEnabled": "Sincronización DIM Habilitada", "DimSyncNotEnabled": "Sincronización DIM no está habilitada, así que tus configuraciones, etiquetas, equipaciones y búsquedas sólo son almacenadas localmente y se perderán si limpias el almacenamiento del navegador. Habilita Sincronización DIM en Configuración para hacer una copia de tu información automáticamente, o hacer una copia de tu información manualmente.", "EnableDimApi": "Habilitar Sincronización DIM (recomendado)", "Export": "Descargar Información de la Copia de Seguridad", "ExportError": "Error al descargar la copia de seguridad desde Sincronización DIM", "ExportErrorBody": "Sincronización DIM puede estar caído, o puede que estés teniendo problemas con tu conexión. Descargaremos una copia de tus datos guardados localmente en su lugar.", "Import": "Importar Información de la Copia de Seguridad", "ImportConfirmDimApi": "¿Estás seguro de que quieres sobreescribir tus etiquetas actuales, equipaciones y configuraciones con esta versión? Reemplazará completamente todo lo que tenías.", "ImportExport": "Copia de Seguridad e Importación", "ImportFailed": "¡Importación Fallida! {{error}}", "ImportNoFile": "¡Ningún archivo ha sido seleccionado!", "ImportNotification": { "FailedBody": "No se pudieron importar datos. {{error}}", "FailedTitle": "Importación Fallida", "NoData": "No se encontraron equipaciones o etiquetas en la copia de seguridad", "SuccessBodyForced": "Importadas configuraciones, {{loadouts}} equipaciones y {{tags}} objetos etiquetas desde tu copia de seguridad a Sincronización DIM, reemplazando lo que ya estaba allí.", "SuccessBodyLocal": "Configuraciones, {{loadouts}} equipaciones y {{tags}} objetos etiquetados importados desde tu copia de seguridad al almacenamiento local, reemplazando lo que ya había ahí. No podemos garantizar que el almacenamiento local será perdido (considera habilitar Sincronización DIM).", "SuccessTitle": "Importación Exitosa" }, "ImportTooManyFiles": "Por favor, selecciona sólo un archivo para importar.", "ImportWrongFileType": "El archivo no es un archivo JSON. Podría no ser una copia de DIM.", "IndexedDBStorage": "Almacenamiento Local del Navegador", "LearnMore": "Aprender más sobre Sincronización DIM", "MenuTitle": "Sincronización y Copias de Seguridad", "ProfileErrorBody": "Tuvimos un problema comunicándonos con Sincronización DIM. Tu última configuración, etiquetas, equipaciones y búsquedas podrían no mostrarse. Tu información todavía está en nuestros servidores, y cualquier actualización que hagas localmente será guardada cuando podamos reconectar. Seguiremos intentándolo mientras DIM esté abierto.", "ProfileErrorTitle": "Error de Descarga de Sincronización DIM", "RefreshDimSync": "Recargar información remota desde Sincronización DIM", "UpdateErrorBody": "Tuvimos un problema guardando tu información a Sincronización DIM. Seguiremos intentándolo mientras DIM esté abierto.", "UpdateErrorTitle": "Error de Guardado de Sincronización DIM", "UpdateInvalid": "Fallo al guardar datos con Sincronización DIM", "UpdateInvalidBody": "La información enviada a Sincronización DIM fue inválida y no será guardada.", "UpdateInvalidBodyLoadout": "La equipación \"{{name}}\" es inválida y no será guardada. Si la importaste desde otro sitio, por favor háganles saber que están importando equipaciones inválidas.", "UpdateQueueLength_one": "{{count}} cambio nuevo será guardado cuando podamos reconectar.", "UpdateQueueLength_other": "{{count}} cambios nuevos serán guardados cuando podamos reconectar.", "Usage": "DIM está usando {{usage, humanBytes}} de {{quota, humanBytes}} disponibles para ello en este dispositivo. Esto incluye las bases de datos de objetos de Destiny descargados de Bungie.net." }, "StreamDeck": { "Authorize": "Conectar aplicación", "Enable": "Complemento Stream Deck", "Error": { "Body": "Hubo un error enviando los datos al complemento de Stream Deck. Por favor contacta con el desarrollador del plugin. {{error}}", "Title": "Error del Complemento de Stream Deck" }, "FinePrint": "Habilita la conexión con el complemento de DIM Stream Deck. Este complemento es un proyecto separado que no está escrito ni respaldado por el equipo de DIM.", "Install": "Instalar complemento", "MissingAuthorization": "Debes autorizar la aplicación Stream Deck para conectar con DIM. Ve a configuración y haz clic en \"Conectar aplicación\".", "Tooltip": { "Application": "Aplicación Stream Deck", "AuthRequired": "Haga clic en este botón o ve a configuración y haz clic en \"Conectar aplicación\".", "Error": "Tu complemento Stream Deck ya no tiene soporte. Por favor actualiza a la última versión. Este complemento requiere al menos:", "ErrorConnection": "Si ya estás usando la última versión, comprueba si alguna extensión de navegador está bloqueando la conexión.", "ExtensionIssue": "Problema con las Extensiones", "Plugin": "Complemento", "Title": "Complemento DIM para Stream Deck", "Version": "Versión:" } }, "StripSockets": { "Action": "Despejar Ranuras", "ArmorMods": "{{count}}x Modificadores de Armadura", "Button": "Despejar {{numSockets}} Ranuras", "Cancel": "Cancelar", "Choose": "Elige Ranuras para despejar", "DiscountedMods": "{{count}}x Modificadores con descuento", "Done": "Ranuras Despejadas", "NoSockets": "Sin Ranuras para despejar", "Ok": "Ok", "Ornaments": "{{count}}x Diseños", "Others": "{{count}}x Proyecciones de Espectro", "Running": "Despejando Ranuras", "Shaders": "{{count}}x Shaders", "Subclass": "{{count}}x Opciones de Subclase", "WeaponMods": "{{count}}x Modificadores de Arma" }, "Tags": { "Archive": "Archivar", "ClearTag": "Limpiar Etiqueta", "Favorite": "Favorito", "Infuse": "Infundir", "Junk": "Basura", "Keep": "Guardar", "LockAll": "Bloquear Objetos", "TagItem": "Etiqueta", "UnlockAll": "Desbloquear Objetos" }, "Triage": { "AccountsForArtifice": "Esto prueba si una pieza de armadura Artificiosa pudiera ser mejor, si un modificador de estadística +3 fuera usado.", "BetterArmor": "Estrictamente Mejor Armadura", "BetterArtificeArmor": "Mejor Armadura Artificiosa", "BetterStatArmor": "Armadura de Mejores Estadísticas", "BetterStatArtificeArmor": "Armadura Artificiosa de Mejores Estadísticas", "BetterWorseArmor": "Mejor/Peor Armadura", "BetterWorseIncludes": "Identifica piezas de armadura con:", "HighStats": "Estadísticas Altas", "InLoadouts": "En Equipaciones", "OwnedCount": "# En Propiedad", "PerkBetterArmorDesc": "Las mismas, o más, ventajas intrínsecas o espacios especiales de modificador.", "PerkWorseArmorDesc": "La misma ventaja intrínseca, o ninguna.", "SimilarItems": "Objetos Similares", "StatBetterArmorDesc": "Todas las estadísticas al menos tan altas, y al menos una estadística mejor.", "StatNotPerkArmorDesc": "Esto solo prueba estadísticas. Una pieza más baja todavía puede tener espacios de modificador especiales o ventajas intrínsecas.", "StatWorseArmorDesc": "Sin mejores estadísticas, y al menos una estadística peor.", "ThisItem": "Este objeto", "WorseArmor": "Estrictamente Peor Armadura", "WorseArtificeArmor": "Peor Armadura No-Artificiosa", "WorseStatArmor": "Armadura de Peores Estadísticas", "WorseStatArtificeArmor": "Armadura No-Artificiosa de Peores Estadísticas", "YourBestItem": "Tu mejor objeto" }, "Triumphs": { "GildingTriumph": "Triunfo para Dorado", "HideCompleted": "Ocultar triunfos completados", "RevealRedacted": "Revelar triunfos clasificados", "SortRecords": "Ordenar triunfos por compleción" }, "Vendors": { "Collections": "Colecciones", "Engram": "Nivel", "FilterToUnacquired": "Mostrar sólo objetos no coleccionados", "HideSilverItems": "Ocultar objetos de Plata", "NoItems": "Este Comerciante no está ofreciendo ningún artículo actualmente.", "RefreshTime": "El inventario se actualiza en:", "Vendors": "Comerciantes" }, "Views": { "About": { "APIHistory": "Ver el historial de todas las acciones hechas por DIM (y otras apps de Destiny)", "BungieCopyright": "Todas las imágenes y el contenido son propiedad de Bungie.", "CommunityInsight": "Información de la Comunidad para las Ventajas y las Estadísticas de los Personajes cortesía de {{clarityLink}}. Si observas imprecisiones o tienes preguntas, únete al {{clarityDiscordLink}}.", "Discord": "Discord", "DiscordHelp": "Haz preguntas, da tu opinión y recibe soporte en nuestros canales de Discord.", "FAQ": "Preguntas Más Frecuentes", "FAQAccess": "¿Cómo accede DIM a mis datos de Destiny?", "FAQAccessAnswer": "Utilizamos la autenticación de la aplicación de Bungie para permitir a DIM ver y mover tus objetos. DIM nunca ve tu usuario o contraseña. Funciona del mismo modo que la aplicación Acompañante.", "FAQKeyboard": "¿DIM soporta atajos de teclado?", "FAQKeyboardAnswer": "¡Sí! Presiona la tecla \"?\" para ver una lista de los atajos disponibles.", "FAQLogout": "¿Cómo puedo cerrar sesión en DIM?", "FAQLogoutAnswer": "Abra el menú desde el icono superior izquierdo y elija \"Cerrar Sesión\"", "FAQLostItem": "¡Perdí mi objeto usando tu herramienta!", "FAQLostItemAnswer": "Bungie no permite apps que borren objetos (¡ni siquiera en su propia app!). Lo más probable es que la transferencia haya fallado, dejando tu objeto en el depósito o en otro personaje. Podrías buscar el objeto. Si eso no lo arregla, recarga la página. Comprueba {{link}} o en el juego para ver si tu objeto aún existe. Estamos seguros de que aún sigue ahí.", "FAQMobile": "¿DIM tendrá soporte móvil? ¿Habrá una app?", "FAQMobileAnswer": "La página web de DIM puede ser cargada en teléfonos y tabletas hoy, y puedes añadirla tu pantalla de inicio para una experiencia similar a la aplicación.", "GitHub": "GitHub", "GitHubHelp": "Si estás interesado en contribuir al proyecto, visita la página del proyecto en {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM es gratis, una aplicación de código abierto construida por desarrolladores de la comunidad bajo los mismos servicios usado por Bungie.net y la Aplicación del Acompañante de Destiny.", "Schedule": { "beta": "Esta versión beta de DIM está actualizada todo el tiempo que cambiamos su codificación - obtiene las últimas características y arreglos, ¡pero también los últimos errores!", "release": "Esta versión de DIM está actualizada una vez a la semana, los Sábados de medianoche aproximadamente, hora del pacífico de los Estados Unidos." }, "Translation": "¡Únete al Equipo de Traducción!", "TranslationText": "Utilizamos {{link}} para facilitar la traducción. Si quieres mejorar alguna de las traducciones de DIM, únete al equipo.", "Version": "Versión {{version}} ({{flavor}}), construida el {{date}}", "Wiki": "Guía de Usuario de DIM", "WikiHelp": "Aprende cómo usar las características de DIM." }, "Login": { "Auth": "Autorizar con Bungie.net", "EnableDimSyncWarning": "Ya habías deshabilitado previamente Sincronización DIM y sólo estaba usando almacenamiento de datos local. Habilitando Sincronización DIM reemplazará cualquier información local con la información de Sincronización DIM. Deberías hacer una copia de seguridad de tus datos antes de habilitar Sincronización DIM. Puedes reestablecerla desde esa copia de seguridad en Configuración.", "Explanation": "Permite a DIM ver y modificar tus personajes de Destiny, depósito y progreso.", "LearnMore": "Aprenda más sobre cuentas e inicio de sesión", "NewAccount": "Inicia sesión con una cuenta diferente de Bungie.net", "Permission": "Necesitamos tu permiso..." }, "Support": { "BackersDetail": "Apóyanos con una donación única o mensual y ayuda a que podamos continuar nuestro desarrollo activo.", "FreeToDownload": "DIM es un producto libre de descarga y uso. El código fuente para DIM es código abierto y libre para que cualquiera pueda mejorarlo. Nunca verás un anuncio en DIM. Ese es nuestro compromiso.", "OpenCollective": "Estamos usando {{link}} como servicio para compensar a nuestros desarrolladores por su dedicación y tiempo dedicado a este proyecto.", "Store": "Tenemos mercancía con nuestro logo y otros diseños en venta en {{link}}", "Support": "Apoyar DIM" } }, "WishListRoll": { "BestRatedTip_one": "Esta ventaja coincide exactamente con una tirada de arma en tu lista de deseos.", "BestRatedTip_other": "Estas ventajas coinciden exactamente con una tirada de arma de tu lista de deseos.", "Clear": "Limpiar Lista de Deseos", "CopiedLine": "Tirada de la Lista de Deseos copiada al portapapeles", "CopyLine": "Copiar las Ventajas Seleccionadas como Tirada de la Lista de Deseos", "DupeRolls": " (+{{num, number}} duplicados ignroados)", "ExternalSource": "Añadir otra lista de deseos", "ExternalSourcePlaceholder": "Pega enlace de lista de deseos aquí", "Header": "Lista de Deseos", "Import": "Cargar Tiradas de la Lista de Deseos", "ImportError": "Error cargando lista de deseos desde \"{{url}}\": {{error}}", "ImportFailed": "Ninguna de tus listas de deseos contenían alguna tirada válida.", "ImportNoFile": "No hay archivo seleccionado.", "InvalidExternalSource": "Por favor, introduzca una URL válida para su lista de deseos externa. La URL debe empezar con algunos de los siguientes:", "JustAnotherTeam": "Solo Otro Equipo", "LastUpdated": "Última actualización: {{lastUpdatedDate}} a las {{lastUpdatedTime}}", "Num": "{{num, number}} tiradas en tu lista de deseos", "NumRolls": "{{num, number}} tiradas", "Refresh": "Actualizar Lista de Deseos", "SourceAlreadyAdded": "Lista de Deseos ya añadida", "UpdateExternalSource": "Añadir Lista de Deseos", "Voltron": "voltron (por defecto)", "WishListNotes": "Notas de la Lista de Deseos:", "WorstRatedTip_one": "Esta ventaja coincide exactamente con una tirada de arma en tu lista de basura.", "WorstRatedTip_other": "Estas ventajas coinciden exactamente con una tirada de arma en tu lista de basura." }, "no-space": "sin-espacio", "wrong-level": "nivel-incorrecto" } ================================================ FILE: src/locale/esMX.json ================================================ { "AWA": { "ConfirmDescription": "Por favor, usa la App Acompañante de Destiny 2 para aprobar que DIM modifique tus objetos.", "ConfirmTitle": "Confirmar Acción", "Error": "Error cambiando mods o perks", "ErrorMessage": "No pudimos equipar {{plug}} en {{item}}.\n\n{{error}}", "FailedToken": "No se pudo obtener permiso para cambiar el objeto", "IrreversiblePlugging": "No tienes ningún {{plug}}, así que no lo sobrescribiremos." }, "Accounts": { "Choose": "Perfiles para {{bungieName}}", "ErrorLoadInventory": "No se pueden cargar tus personajes e inventario de Destiny {{version}}", "ErrorLoadManifest": "La información de la base de datos de Destiny desde Bungie no se pudo cargar", "ErrorLoading": "No se pudieron cargar cuentas de Destiny desde Bungie.net", "MissingAccountWarning": "Si no ves tu cuenta aquí, es posible que no hayas iniciado sesión con la cuenta de Bungie.net correcta, o es posible que Bungie.net no funcione por mantenimiento.", "MissingDescription": "La cuenta que estás intentando ver no es una cuenta enlazada a tu perfil de Bungie.net. Selecciona una de tus cuentas debajo.", "MissingTitle": "Cuenta no encontrada", "NoCharacters": "No tienes personajes de Destiny asociados con esta cuenta de Bungie.net. Inicia sesión con una cuenta diferente.", "NoCharactersTitle": "No Se Encontraron Personajes", "SwitchAccounts": "Puedes cambiar de cuentas más tarde desde el menú en el encabezado.", "Title": "Cuentas" }, "Activities": { "Activities": "Actividades", "Hard": "Difícil", "Nightfall": "Asalto de Ocaso", "Normal": "Normal", "WeeklyHeroic": "Asalto heroico semanal" }, "Armory": { "AlternateItems": "Versiones alternativas", "Armory": "Armería", "DifferentSeason": "Reemitir desde una temporada diferente", "NoNotes": "Sin Notas", "OpenInArmory": "ver en la Armería", "Season": "Temporada {{season}}, Año {{year}}", "TrashlistedRolls_one": "Tirada en lista de basura", "TrashlistedRolls_other": "{{count, number}} Tiradas en Lista de Basura", "Unknown": "Objeto Desconocido", "UnknownPerkHash": "El hash del perk {{hash}} ({{perkName}}) no aparece en este objeto, así que esta tirada de la lista de deseos es inválida. Por favor contacta con el autor de la lista de deseos para corregir esto. Toma en cuenta que la lista de deseos siempre deberían especificar las versiones no mejoradas de los perks.", "WishlistedRolls_one": "Tirada en lista de deseos", "WishlistedRolls_other": "{{count, number}} Tiradas en Lista de Deseos", "YourItems": "Tus Objetos" }, "Browsercheck": { "Samsung": "El Internet de Samsung puede hacer que los sitios parezcan demasiado oscuros cuando el modo oscuro está activo. Activa Opciones > Laboratorios > Usar tema oscuro o cambia a otro navegador.", "Steam": "El navegador de Steam es muy antiguo y podría hacer que algunas o todas las características de DIM no funcionen. No podemos proveer de soporte para ello.", "Unsupported": "El equipo de DIM no soporta usar este navegador. Algunas o todas las características de DIM podrían no funcionar." }, "Bucket": { "Armor": "Armadura", "Class": "Subclase", "General": "General", "Ghost": "Espectro", "Inventory": "Inventario", "Postmaster": "Administración", "Progress": "Progreso", "Reputation": "Reputación", "Unknown": "Desconocido", "Vault": "Depósito", "Weapons": "Armas" }, "BulkNote": { "Append": "Adjuntar a notas / añadir #hashtags", "Confirm": "Actualizar Notas", "Remove": "Eliminar de notas / eliminar #hashtags", "Replace": "Reemplazar notas", "Title_one": "Cambiar notas para 1 objeto", "Title_other": "Cambiar notas para {{count}} objetos" }, "BungieAlert": { "Title": "Un mensaje de Bungie:" }, "BungieService": { "AppNotPermitted": "DIM no tiene permiso para realizar esta acción.", "DestinyCannotPerformActionAtThisLocation": "No puedes equipar objetos o cambiar mods mientras estés en una actividad. Intenta dirigirte a órbita o a una área social. Esto es una limitación de la API de Bungie.net, no de DIM.", "DestinyItemUnequippable": "No puedes equipar este objeto. Si la última actividad del personaje bloqueó tu equipamiento, intenta iniciar sesión con el personaje de nuevo.", "DestinyLegacyPlatform": "Los servicios de Bungie tienen actualmente un error que impide que DIM pueda cargar tu perfil de Destiny 2 si has jugado Destiny 1 en una consola de la generación anterior. Bungie arreglará esto pronto, mientras tanto una solución es jugar Destiny 1 en una consola de la generación actual para poder acceder a tu perfil.", "DevVersion": "¿Estás usando una versión en desarrollo de DIM? Debes registrar tu extensión de Chrome con Bungie.net.", "Difficulties": "En estos momentos Bungie.net está experiementando dificultades.", "ErrorTitle": "Error de Bungie.net", "ItemUniquenessExplanation": "Un personaje sólo puede tener un '{{name}}' en él.", "Maintenance": "Los servidores de Bungie.net se encuentran fuera de servicio por mantenimiento.", "MissingInventory": "Bungie.net no devolvió tu inventario, posiblemente porque tus ajustes de privacidad lo impiden. Intenta cerrar sesión y volver a iniciar sesión.", "NetworkError": "Error de red - {{status}} {{statusText}}", "NoAccount": "No se encontró ninguna cuenta de Destiny. ¿Tienes seleccionada la plataforma correcta?", "NoAccountForPlatform": "No encontramos ninguna cuenta de Destiny en {{platform}}.", "NotConnected": "Es posible que no tengas conexión a Internet.", "NotConnectedOrBlocked": "Podrías no estar conectado a internet, o una extensión de bloqueo de anuncios o de privacidad puede estar bloqueando Bungie.net.", "NotLoggedIn": "Por favor autoriza a DIM para poder usar esta app.", "Slow": "Bungie.net está lento en estos momentos", "SlowDetails": "Bungie.net está tomando mucho tiempo regresar la información. Esto puede pasar cuando muchos jugadores están en el juego a la vez o si Bungie.net está teniendo problemas. También podrías estar teniendo un problema de conexión de internet. Estaremos esperando por una respuesta.", "SlowResponse": "Bungie.net fue demasiado lento en responder.", "Throttled": "Bungie.net está limitando cuántas solicitudes DIM puede hacer.", "Twitter": "Obtén actualizaciones de estado en:", "UnknownError": "Mensaje de Bungie.net: {{message}}", "VendorNotFound": "La información de los vendedores no está disponible." }, "Compare": { "Archetype": "Arquetipo", "AssumeMasterworked": "Asumir Obra Maestra Completa", "AssumeMasterworkedDescription": "Estadísticas si es Obra Maestra Completa, sin los Mods actuales", "BaseStatsDescription": "Estadísticas base, sin Obras Maestras o Mods", "Button": "Comparar", "ButtonHelp": "Comparar Objetos", "CompareBaseStats": "Mostrar Estadísticas Base", "CurrentStats": "Estadísticas Actuales", "CurrentStatsDescription": "Estadísticas actuales, incluyendo Mods y nivel de Obra Maestra", "Error": { "Invalid": "No hay objetos válidos para la comparación.", "Unmatched": "Este artículo no coincide con el tipo de objetos que estás comparando." }, "InitialItem": "Este es el objeto desde el que se inició la herramienta Comparar", "IsVendorItem": "Este objeto no está en tu inventario, pero {{vendorName}} lo vende.", "NoModArmor": "Pre-mods" }, "Cooldown": { "Grenade": "Tiempo de recarga de Granada: {{cooldown}}", "Melee": "Tiempo de recarga de Ataque Cuerpo a Cuerpo: {{cooldown}}", "Super": "Tiempo de recarga de Súper: {{cooldown}}" }, "Countdown": { "Days_compact_one": "{{count}}d", "Days_compact_other": "{{count}}d", "Days_one": "1 Día", "Days_other": "{{count}} Días" }, "Csv": { "EmptyFile": "No había líneas en el archivo.", "ImportConfirm": "¿Estás seguro de que quieres importar las etiquetas/notas del CSV? Esto sobreescribirá las etiquetas/notas para todos los objetos contenidos en tu hoja de cálculo.", "ImportFailed": "Falló al importar etiquetas/notas del CSV: {{error}}", "ImportSuccess_one": "Etiquetas/notas cargadas para un objeto.", "ImportSuccess_other": "Etiquetas/notas cargadas para {{count}} objetos.", "ImportWrongFileType": "El archivo no es un archivo CSV.", "WrongFields": "El CSV debe tener las columnas 'Id', 'Notes', 'Tag' y 'Hash'." }, "Dialog": { "Cancel": "Cancelar", "OK": "OK" }, "EnergyMeter": { "Energy": "Energía", "Unused": "Sin usar", "UpgradeNeeded": "La capacidad de energía del objeto actual es {{energyCapacity}}. Para encajar los mods seleccionados, su capacidad de energía debe ser {{energyUsed}}.", "Used": "Usado" }, "ErrorBoundary": { "Title": "Algo salió mal" }, "ErrorPanel": { "BrowserTooOld": "Tu navegador es demasiado antiguo para usar DIM. Por favor actualiza tu navegador a la última versión.", "BrowserTooOldTitle": "Navegador Incompatible", "Description": "Intenta cargar tu inventario en la Aplicación del Acompañante de Destiny 2 para ver si Bungie está funcionando.", "ReadTheGuide": "Lea nuestra Guía de Usuario (enlazada desde el menú) para pasos de solución de problemas.", "SystemDown": "Esto afecta a todas las aplicaciones de Destiny, y el equipo de DIM no puede arreglarlo ni evitarlo.", "Troubleshooting": "Guía de Solución de Problemas" }, "FarmingMode": { "D2Desc_female_one": "DIM está impidiendo que los objetos vayan a la Administración asegurándose de que siempre haya un espacio vacío por tipo de objeto en {{store}}.", "D2Desc_female_other": "DIM está impidiendo que los objetos vayan a la Administración asegurándose de que siempre haya {{count}} espacios vacíos por tipo de objeto en {{store}}.", "D2Desc_male_one": "DIM está impidiendo que los objetos vayan a la Administración asegurándose de que siempre haya un espacio vacío por tipo de objeto en {{store}}.", "D2Desc_male_other": "DIM está impidiendo que los objetos vayan a la Administración asegurándose de que siempre haya {{count}} espacios vacíos por tipo de objeto en {{store}}.", "D2Desc_one": "DIM está impidiendo que los objetos vayan a laAdministración asegurándose de que siempre haya un espacio vacío por tipo de objeto en {{store}}.", "D2Desc_other": "DIM está impidiendo que los objetos vayan a la Administración asegurándose de que siempre haya {{count}} espacios vacíos por tipo de objeto en {{store}}.", "Desc_female_one": "DIM está moviendo Engramas y Lumen desde {{store}} al Depósito y mantiene un espacio vacío abierto por tipo de objeto para impedir que cualquier cosa se vaya a la Administración.", "Desc_female_other": "DIM está moviendo Engramas y Lumen desde {{store}} al Depósito y mantiene {{count}} espacios abiertos por tipo de objeto para impedir que cualquier cosa se vaya a Administración.", "Desc_male_one": "DIM está moviendo Engramas y Lumen desde {{store}} al Depósito y mantiene un espacio vacío abierto por tipo de objeto para impedir que cualquier cosa se vaya a la Administración.", "Desc_male_other": "DIM está moviendo Engramas y Lumen desde {{store}} al Depósito y mantiene {{count}} espacios abiertos por tipo de objeto para impedir que cualquier cosa se vaya a la Administración.", "Desc_one": "DIM está moviendo Engramas y Lumen desde {{store}} al Depósito y mantiene un espacio vacío abierto por tipo de objeto para impedir que cualquier cosa se vaya a la Administración.", "Desc_other": "DIM está moviendo Engramas y Lumen desde {{store}} al depósito y mantiene {{count}} espacios abiertos por tipo de objeto para impedir que cualquier cosa se vaya a la Administración.", "FarmingMode": "Modo Farmeo", "FarmingModeNote": "(mantiene espacio para objetos soltados)", "MakeRoom": { "Desc": "DIM esta moviendo solo Engramas y Lúmen desde {{store}} al Depósito o a otros personajes para prevenir que se vayan a la Administración.", "Desc_female": "DIM esta moviendo solo Engramas y Lúmen desde {{store}} al Depósito o a otros personajes para prevenir que se vayan a la Administración.", "Desc_male": "DIM esta moviendo solo Engramas y Lúmen desde {{store}} al Depósito o a otros personajes para prevenir que se vayan a la Administración.", "MakeRoom": "Haz espacio para recolectar objetos moviendo el equipamiento", "Tooltip": "Si esta marcado, DIM moverá armas y armadura para hacer espacio en el Depósito para engramas." }, "OutOfRoom": "Estás sin espacio para mover objetos fuera de {{character}}. ¡Es hora de tirar la basura!", "OutOfRoomTitle": "Sin espacio", "Stop": "Detener", "Vault": "Moverá objetos al Depósito para hacer espacio." }, "FashionDrawer": { "Accept": "Guardar cosméticos", "CannotFitOrnament": "Este objeto no tiene ranura para diseño o no tienes diseños para él.", "CannotFitShader": "A este objeto no le encaja ningún shader", "ClearOrnaments": "Limpiar Diseños", "ClearOrnamentsTitle": "Reiniciar todos los diseños a \"sin preferencia\"", "ClearShaders": "Limpiar Shaders", "ClearShadersTitle": "Reiniciar todos los shaders a \"sin preferencia\"", "NoPreference": "No hay preferencia - este espacio no se cambiará", "Reset": "Limpiar cosméticos", "Sync": "Sincronizar", "SyncOrnaments": "Sincronizar Diseños", "SyncOrnamentsTitle": "Usar diseños del mismo conjunto en todos los objetos, si están desbloqueados", "SyncShaders": "Sincronizar Shaders", "SyncShadersTitle": "Usar el mismo shader en todos los objetos", "Title": "Elegir shaders y diseños", "UseEquipped": "Usar cosméticos equipados" }, "FileUpload": { "Instructions": "Haz click o arrastra archivos" }, "Filter": { "Adept": "\\(Adept\\)", "AmmoType": "Mostrar objetos basados en su tipo de munición.", "Armor": "Muestra objetos que son armaduras.", "Armor3": "Muestra objetos que utilizan el sistema de estadística de Armadura 3.0 introducidos en Los Confines del Destino.", "ArmorCategory": "Muestra armaduras basadas en su categoría.", "ArmorIntrinsic": "Muestra armadura legendaria que tenga un perk intrínseco, como Armadura Artificiosa.", "Artifice": "Mostrar armadura Artificiosa.", "Ascended": "Muestra objetos que tengan un nodo ascendente en el que haya sido ascendido.", "Breaker": "Filtrar por tipo de rompedor o tipo de campeón correspondiente. breaker:intrinsic muestra objetos con habilidad de rompedor intrínseca.", "BulkClear_one": "Etiqueta eliminada de 1 objeto.", "BulkClear_other": "Etiquetas eliminadas de {{count}} objetos.", "BulkRevert_one": "Etiqueta revertida en 1 objeto.", "BulkRevert_other": "Etiquetas revertidas en {{count}} objetos.", "BulkTag_one": "Etiquetado objeto seleccionado como {{tag}}.", "BulkTag_other": "Etiquetados {{count}} objetos seleccionados como {{tag}}.", "Catalyst": "Muestra catalizadores basados en su estado. catalyst:complete muestra catalizadores que hayas completado y aplicado, catalyst:incomplete muestra catalizadores que hayas desbloqueado pero puede que no has completado el objetivo o aplicado el catalizador, y catalyst:missing muestra objetos que pueden tener un catalizador pero no los has encontrado todavía.", "Class": "Muestra objetos basados en su afinidad de clase.", "Combine": "Los filtros pueden ser combinados o agrupados con paréntesis, \"or\" y \"and\" para limitar tu búsqueda, por ejemplo \"{{example}}\".", "ContributePower": "Muestra los objetos que tienen poder y que contribuyen a subir tu nivel de poder.", "Cosmetic": "Muestra objetos que son cosméticos o estilo.", "Craftable": "Muestra objetos que son fabricables.", "CraftedDupe": "Muestras armas duplicadas donde al menos una de las duplicadas está fabricada.", "Curated": "Muestra objetos que tengan una tirada de curador.", "CurrentClass": "Muestra objetos que sean equipables en el personaje conectado en estos momentos.", "CustomStatLower": "Muestra armadura cuyas estadísticas son estrictamente más bajas que cualquiera otra del mismo tipo de armadura, solo tomando en cuenta estadísticas que están en cualquiera de esa lista personalizada de estadísticas totales de clase.", "DamageType": "Muestra los objetos basado en su tipo de daño.", "Deepsight": "Muestra armas con Resonancia de Visión Profunda, que pueden tener su patrón extraído, o que puedan tener Resonancia de Visión Profunda habilitada usando un Armonizador de Visión Profunda.", "Deprecated": "Este filtro ya no es compatible.", "Description": "Descripción", "DescriptionFilter": "Muestra los elementos cuya descripción tiene una coincidencia parcial con el texto del filtro. Busca frases completas usando comillas.", "DisabledModSlot": "Muestra objetos con un mod deshabilitado.", "Dupe": "Muestra objetos duplicados, incluyendo reincidencias", "DupeArchetype": "Agrupa armadura con la misma estadística de Arquetipo.", "DupeCount": "Objetos que tienen el número especificado de duplicados.", "DupeLower": "Objetos duplicados, incluyendo reincidencias, que no son el poder más alto del duplicado. Sólo un duplicado es elegido como el más alto, y el resto son considerados más bajos.", "DupePerks": "Muestra objetos cuyas ventajas ya sean duplicadas de, o un subconjunto de, otro objeto del mismo tipo.", "DupeSetBonus": "Agrupa armadura con la misma bonificación de conjunto.", "DupeStats": "Muestra armadura con estadísticas base idénticas, y coincidencias en mods de ajuste de estadísticas como Artificiosa o de Ajuste.", "DupeTertiary": "Agrupa armadura con la misma estadística terciaria.", "DupeTraits": "Armas cuyos rasgos ya sean un duplicado de, o un subconjunto de otra arma del mismo tipo.", "DupeTunedStat": "Agrupa armadura con la misma estadística Ajuste.", "DupeUntunedStats": "Agrupa armadura con las mismas estadísticas idénticas, ignorando mods de ajuste de estadísticas.", "DupeZeroStats": "Agrupa armadura con las 3 mismas estadísticas base que no sean cero.", "Energy": "Muestra objetos que utilizan el sistema de mod de Armadura 2.0 introducidos en Bastión de Sombras.", "EnergyCapacity": "Muestra objetos basados en su capacidad de energía actual.", "Engrams": "Muestra engramas.", "Enhanceable": "Muestra armas que pueden ser mejoradas.", "Enhanced": "Muestra armas basadas en su nivel de mejora.", "EnhancedPerk": "Muestra armas que tienen un número especificado de columnas de perks mejoradas.", "EnhancementReady": "Muestra armas que han alcanzado niveles umbrales para mejora de perks.", "Equipment": "Objetos que pueden ser equipados.", "Equipped": "Objetos que actualmente están equipados en un personaje.", "Event": "Muestra objetos en que evento de Destiny 2 aparecieron.", "ExtraPerk": "Muestra armas legendarias con rolls aleatorios con una perk adicional seleccionable.", "Featured": "Objetos que cuenten como uno de los objetos \"Nuevo Equipamiento\" u \"Objetos Destacados\" en la temporada actual.", "Filter": "Filtro", "FilterWith": "Filtrar con:", "Focusable": "Muestra objetos que pueden ser enfocados en un comerciante", "Foundry": "Muestra objetos basados en la forja que los creó.", "Glimmer": "Muestra objetos que son consumibles que están relacionados con la ganancia de lumen.", "Harrowed": "\\(Saqueado\\)", "HasNotes": "Muestra objetos que contengan notas.", "HasOrnament": "Muestra objetos que tengan diseño aplicado.", "HasShader": "Mostrar objetos que tengan shader aplicado.", "Holofoil": "Muestra armas holometalizadas.", "InDimLoadout": "is:indimloadout muestra objetos que están incluidos en cualquier equipación en DIM.", "InInGameLoadout": "is:iningameloadout muestra objetos que están incluidos en cualquier equipación en el juego.", "InInventory": "Muestra objetos en los que tengas al menos una copia en tu inventario. Solo es realmente útil en las pantallas de los Vendedores y Hazañas.", "InLoadout": "is:inloadout muestra objetos que estén incluidos en cualquier equipación. Buscando con inloadout: muestra objetos que estén incluidos en equipaciones con títulos coincidentes. Cuando es usado con un hashtag, inloadout: muestra objetos cuyas equipaciones tengan el hashtag en el título o en las notas. Cuando se usa con un rango, muestra objetos que están en esa cantidad de equipaciones.", "Infusable": "Muestra objetos que pueden ser infundidos.", "InfusionFodder": "Muestra objetos que podrían ser infundidos en versiones más bajas de poder del mismo objeto sólo por lumen.", "IsAdept": "Muestra armas compatibles con mods Adepto.", "IsCrafted": "Muestra armas que han sido fabricadas.", "ItemHash": "Muestra los objetos con un hash de objeto de inventario obtenido. Para usuarios avanzados.", "ItemId": "Muestra el objeto con un ID de inventario obtenido. Para usuarios avanzados.", "Leveling": { "Complete": "{{term}} - muestra objetos que están completamente mejorados - todas las mejoras desbloqueadas.", "Incomplete": "{{term}} - muestra objetos que no están completados - aún queda al menos una mejora por desbloquear.", "NeedsXP": "{{term}} - muestra objetos que aún se les puede aumentar el XP.", "Upgraded": "{{term}} - muestra objetos que tienen suficiente XP para desbloquear todos sus nodos, pero no todos sus nodos han sido desbloqueados.", "XPComplete": "{{term}} - muestra objetos que no se les puede poner más XP (sin importar si sus mejoras han sido desbloqueadas o no)." }, "Location": "Muestra objetos basados en su localización dentro de la app. \"Left/middle/right\" son la localización visual del personaje, y mientras \"inleftchar\" siempre funcionará, los otros dos están basados en cuántos personajes tengas. \"Current\" es tu último/actual personaje iniciado (que está marcado con un triángulo amarillo).", "LockAllFailed": "Error al bloquear objetos", "LockAllSuccess": "{{num}} objetos bloqueados", "Locked": "Muestra objetos basado en su estado de bloqueo.", "Masterwork": "Muestra objetos basados en su nivel de estadística de obra maestra o nivel de obra maestra.", "MasterworkKills": "Muestra los objetos basados en su registro de contador de bajas de obra maestra.", "MaxPower": "Muestra los objetos en el nivel más alto de poder por ranura.", "MaxPowerLoadout": "Muestra los objetos en el equipamiento que podrían maximizar tu Poder de Luz para cada clase de personaje.", "Memento": "Muestra armas que tengan una ranura para recuerdo.", "ModSlot": "Muestra armadura con un tipo de mod específico.", "Mods": { "Y3": "Muestra objetos nuevos sin mods aplicados." }, "Name": "Muestra objetos cuyo nombre coincida exactamente (exactname:) o parcialmente (name:) con el texto filtrado. Busca frases enteras usando comillas.", "NamedStat": "Muestra armadura que tiene puntos en el atributo mencionado.", "Negate": "Para negar una búsqueda, prefija el término de búsqueda con el signo menos o la palabra \"not\", como por ejemplo \"{{notexample}}\" o \"{{notexample2}}\".", "NewItems": "Muestra objetos nuevos.", "Notes": "Buscar objetos que tengas etiquetados con notas personalizadas.", "OriginTrait": "Muestra armas que tienen un perk de rasgo original.", "Ornament": "Muestra objetos con diseños y filtros para su estado.", "PartialMatch": "Muestra objetos donde su nombre, descripción, cualquier perk o cualquier mod tenga una coincidencia parcial al texto filtrado. Busca frases enteras usando comillas.", "PatternUnlocked": "Muestra objetos que tienen un patrón de fabricación desbloqueado, incluso si el objeto no está fabricado.", "Perk": "Muestra objetos donde una de sus ventajas o modificadores tiene una coincidencia parcial al texto filtrado en su nombre o descripción. Busca por frases enteras usando comillas.", "PerkName": "Muestra objetos con un perk o mod cuyo nombre coincida exactamente (exactperk:) o parcialmente (perkname:) con el texto filtrado. Busca frases completas usando comillas.", "PinnacleReward": "Muestra aventuras que produzcan una recompensa hito.", "Postmaster": "Objetos que están actualmente en la administración.", "PowerKeywords": "Usa la palabra clave \"pinnaclecap\" o \"softcap\" en vez de un número para referirte a los límites de poder de la temporada actual.", "PowerLevel": "Muestra objetos basados en su nivel de poder. $t(Filter.PowerKeywords)", "PowerfulReward": "Muestra aventuras que tengan una recompensa poderosa.", "PrismaticDamageType": "Muestra objetos basados en si su tipo de daño son de Luz u Obscuridad. Los tipos de Luz son arco, solar y vacío. Los tipos de Obscuridad son estasis y atadura.", "Quality": "Muestra objetos basado en el porcentaje de calidad de su estadística total. '{{percentage}}' es un sobrenombre de '{{quality}}'.", "RandomRoll": "Muestra objetos que sueltan rolls aleatorios.", "RarityTier": "Muestra objetos basado en su nivel de rareza.", "Reforgeable": "Muestra objetos que pueden ser reforjados con el Armero.", "Release": "Muestra objetos disponibles de un lanzamiento o evento específico.", "RequiredLevel": "Muestra objetos basados en su nivel requerido.", "RetiredPerk": "Muestra armas con perks que ya no se pueden obtener.", "SearchPrompt": "Buscar filtros de comandos disponibles", "Season": "Muestra objetos por temporada de Destiny 2 en que aparecieron.", "StackFull": "Muestra objetos que están al límite de capacidad para la pila (Núcleos de Mejora, Monedas Extrañas, Materiales de Armero, etc)", "StackLevel": "Muestra objetos basados en la cantidad de objetos apilados.", "Stackable": "Muestra objetos que pueden ser apilados (munición sintética, monedas extrañas, etc)", "StatLower": "Muestra armadura cuyas estadísticas son estrictamente más bajas que otras del mismo tipo de armadura.", "Stats": "Muestra objetos basados en un valor de estadística específico. $t(Filter.StatsExtras)", "StatsBase": "Filtra la armadura basada en su valor de estadística base, sin incluir mods aplicados u obras maestras. $t(Filter.StatsExtras)", "StatsExtras": "Apoya la adición de estadísticas conectando múltiples nombres de estadísticas con los símbolos + o &. También hay palabras clave especiales (highest, secondhighest, thirdhighest, etc.) que coinciden con estadísticas basadas en su nivel dentro de las estadísticas de un objeto. Cada estadística personalizada también tiene su propio término de búsqueda, mostrado en la configuración de Estadísticas Personalizadas.", "StatsLoadout": "Encuentra sets de objetos para equipar con el valor máximo total de una estadística específica.", "StatsMax": "Encuentra armadura con el número más alto para una estadística específica. Incluye todos los objetos con la estadística más alta.", "StatsOrdinal": "Encuentra armadura 3.0 con un enfoque de estadísticas especificadas.", "Tags": { "Tag": "Muestra objetos que tengan una etiqueta específica.", "Tagged": "Muestra objetos que tengan cualquier etiqueta." }, "Tier": "Muestra objetos basados en su nivel del 0-5.", "Timelost": "\\(tiempo perdido\\)", "Tracked": "Muestra aventuras/contratos basados en su estado de seguimiento.", "Transferable": "Objetos que pueden moverse entre personajes.", "Trashlist": "Muestra objetos que coincidan con la lista basura de la lista de deseos.", "TunedStat": "Muestra objetos con mods de afinación para la estadística especificada.", "Unascended": "Muestra objetos que tengan un nodo ascendente en el que no haya sido ascendido.", "Undo": "Deshacer", "UnlockAllFailed": "Error al desbloquear objetos", "UnlockAllSuccess": "{{num}} objetos desbloqueados", "Vendor": "El objeto está disponible desde un vendedor específico.", "VendorItem": "El objeto es de un comerciante, no está en tu inventario. Útil para excluir los objetos de un comerciante desde el Optimizador de Equipaciones.", "Weapon": "Muestra objetos que sean armas.", "WeaponLevel": "Muestra armas basadas en su Nivel de Arma.", "WeaponType": "Muestra armas basadas en su tipo de arma.", "Wishlist": "Muestra objetos que coincidan con tu lista de deseos.", "WishlistDupe": "Muestra objetos duplicados donde al menos uno de los duplicados esté en tu lista de deseos.", "WishlistEnabled": "Muestra los elementos que son elegibles para tener rolls de lista de deseos.", "WishlistNotes": "Muestra objetos de la lista de deseos cuyas notas coincidan con la búsqueda.", "WishlistUnknown": "Muestra objetos sin recomendaciones de rolls en las listas de deseos cargadas.", "Year": "Muestra los objetos según el año de aparición en Destiny." }, "General": { "ClickForDetails": "Haz click para detalles", "Close": "Cerrar", "Confirm": "¿Confirmar?", "UserGuideLink": "Manual del usuario" }, "Glyphs": { "Axe": "Hacha", "DarkAbility": "Habilidad de Obscuridad", "Gilded": "Dorado", "Harmonic": "Armónico", "HiveSword": "Espada de la Colmena", "LightAbility": "Habilidad de Luz", "LightLevel": "Nivel de Luz", "Misadventure": "Accidente", "Missing": "Perdido", "OpenSymbolsPicker": "Abrir el Selector de Símbolos", "Prismatic": "Prismática", "Quickfall": "Caída Rápida", "RespawnRestricted": "Reaparición Limitada", "ScorchCannon": "Cañón Calcinante", "SearchSymbols": "Buscar Símbolos...", "Smoke": "Humo" }, "Header": { "About": "Acerca de DIM", "AutoRefresh": "DIM se recargará automáticamente todo el tiempo que estés jugando.", "BulkTag": "Etiquetar los objetos en general", "BungieNetAlert": "Alerta de Bungie", "Clear": "Limpiar filtro de búsqueda", "CompareMatching": "Comparar Objetos", "DeleteSearch": "Borrar Búsqueda", "FilterHelp": "Buscar objeto/perk, {{example}}, y más", "FilterHelpBrief": "Buscar objetos", "FilterHelpLoadouts": "Buscar nombres y notas de equipamientos", "FilterHelpMenuItem": "Ayuda de Filtros...", "FilterHelpOptimizer": "Filtro de Armadura incluida en construcciones, por ejemplo: {{example}}", "FilterHelpProgress": "Buscar hazañas y contratos", "FilterHelpRecords": "Buscar triunfos y colecciones", "FilterMatchCount_one": "1 objeto", "FilterMatchCount_other": "{{count}} objetos", "Filters": "Filtros", "InstallDIM": "Instalar como aplicación", "InstallDIMBanner": "Instalar DIM como app en tu pantalla de inicio", "Inventory": "Inventario", "IosPwaPrompt": "En Safari Móvil, haz click en el botón de compartir (botón central en la parte inferior) y selecciona \"Añadir a Pantalla de Inicio\".", "KeyboardShortcuts": "Atajos de Teclado", "LaunchDIMAlone": "Separar Ventana", "MaterialCounts": "Contador de Materiales", "Menu": "Menú", "ProfileAge": "Los servidores de Destiny enviaron por última vez información hace {{age}}.\nActualizar desde DIM podría obtener nuevos datos, pero Bungie.net también podría repetir información en el caché.", "Refresh": "Actualizar Datos de Destiny [R]", "ReloadApp": "Recargar App", "ReportBug": "Informar de un Error", "SaveSearch": "Guardar Búsqueda", "SearchActions": "Abrir Acciones de Búsqueda", "SearchResults": "Mostrar Objetos", "Shop": "Tienda", "TagAs": "Etiquetar como '{{tag}}'", "UpgradeDIM": "Actualizar DIM", "WhatsNew": "Novedades" }, "Help": { "CannotMove": "No se puede mover ese objeto de este personaje.", "NoStorage": "DIM no puede almacenar información", "NoStorageMessage": "DIM no puede almacenar información en tu navegador. Esto puede ser causado por navegar en privado o en modo incógnito, o cuando tengas poco espacio en disco, o por un fallo del navegador. ¡Intenta reiniciar tu ordenador! No podrás ser capaz de iniciar sesión o usar DIM hasta que no arregles esto." }, "Hotkey": { "Armory": "Mostrar la Armería para un objeto", "CheatSheetTitle": "Atajos del Teclado:", "ClearDialog": "Descartar diálogo", "ClearNewItems": "Despejar los objetos nuevos", "Enter": "ENTER", "ItemPopupTab": "Cambiar a pestaña de detalles de objeto", "LockUnlock": "Bloquear o desbloquear un objeto", "MarkItemAs": "Marcar objeto como '{{tag}}'", "Menu": "Alternar menú", "Note": "Escribir notas", "Pull": "Traer objeto al personaje activo", "RefreshInventory": "Actualizar inventario", "ShowHotkeys": "Mostrar atajos del teclado", "StartSearch": "Empezar una búsqueda", "StartSearchClear": "Empezar una nueva búsqueda", "Tab": "TAB", "Vault": "Enviar objeto al depósito" }, "InGameLoadout": { "ClearSlot": "Limpiar Ranura {{index}}", "Create": "Crear equipamiento", "CreateTitle": "Crear Equipamiento en el juego desde el equipo actual", "CurrentlyEquipped": "Equipado en estos momentos", "DeleteFailed": "Error al borrar equipamiento", "Deleted": "Equipamiento Eliminado", "DeletedBody": "Se ha limpiado el equipamiento del juego en la ranura {{index}}", "EditFailed": "Error al actualizar equipamiento", "EditIdentifiers": "Editar Identificadores", "EditTitle": "Editar Nombre e Icono del Equipamiento", "EquipNotReady": "Equipar En El Juego No está Listo", "EquipReady": "Equipar En El Juego Listo", "LoadoutDetails": "Detalles del Equipamiento", "MatchingLoadouts": "Equipaciones Coincidentes:", "PrepareEquip": "Preparar para Equipar", "Replace": "Reemplazar equipamiento {{index}}", "Save": "Actualizar Equipamiento", "SaveIdentifiers": "Actualizar Identificadores", "SnapshotFailed": "Error al capturar equipamiento equipado" }, "Infusion": { "Filter": "Filtrar objetos", "InfuseSource": "Selecciona el objeto con el que vas a infundir {{name}}", "InfuseTarget": "Selecciona el objeto al que {{name}} se va a infundir", "InfusionMaterials": "Materiales para Infundir", "NoItems": "No hay objetos disponibles para infundir.", "NoTransfer": "El material para infundir\n{{target}} no se puede mover.", "SwitchDirection": "Cambiar", "TransferItems": "Transferir" }, "Inventory": { "ClickToExpand": "(Haz clic para expandir)", "MissingSilver": "Tu balance de Plata solo está disponible mientras estás jugando al juego." }, "Item": { "SetBonus": { "NPiece_one": "{{count}} Pieza", "NPiece_other": "{{count}} Piezas" }, "ThumbsDown": "Pulgar Abajo", "ThumbsUp": "Pulgar Arriba" }, "ItemFeed": { "ClearFeed": "Limpiar Recientes", "Description": "Objetos Recientes", "HideTagged": "Ocultar etiquetados", "NoNewItems": "No hay nuevos objetos", "ShowOlderItems": "Mostrar objetos más antiguos" }, "ItemMove": { "Consolidate": "Consolidado {{name}}", "Distributed": "{{name}} distribuido.\nAhora {{name}} se encuentra distribuido de manera equitativa entre los personajes.", "MovingItem": "Transferir al depósito", "MovingItem_female": "Transferir a {{target}}", "MovingItem_male": "Transferir a {{target}}", "ToStore": "Ahora todos los {{name}} están en tu {{store}}.", "ToVault": "Ahora todos los {{name}} están en tu depósito." }, "ItemPicker": { "ChooseItem": "Elige un objeto:", "SearchPlaceholder": "Buscar objetos" }, "ItemService": { "BucketFull": { "Guardian": "Hay muchos objetos de tipo '{{itemtype}}' en tu {{store}}.", "Guardian_female": "Hay muchos objetos de tipo '{{itemtype}}' en tu {{store}}.", "Guardian_male": "Hay muchos objetos de tipo '{{itemtype}}' en tu {{store}}.", "Vault": "Hay muchos objetos de tipo '{{itemtype}}' en la {{store}}." }, "Classified": "Este objeto es \"clasificado\" y no puede transferirse en este momento.", "Classified2": "Objeto Clasificado. Bungie no aporta ningún tipo de información respecto a este objeto. Añade notas a este objeto y usa el filtro de busqueda \"notes:\" para encontrarlo.", "Deequip": "No se pudo encontrar otro objeto para equipar con el fin de desequipar {{itemname}}", "ExoticError": "'{{itemname}}' no puede ser equipado porque el objeto excepcional en el espacio {{slot}} no puede ser desequipado. ({{error}})", "NotEnoughRoom": "No hay nada que podamos sacar fuera de {{store}} para hacerle espacio a {{itemname}}", "NotEnoughRoomGeneral": "No hay suficiente espacio para mover este objeto.", "OnlyEquippedClassLevel": "Esto sólo puede equiparse en un {{class}} de nivel igual o superior a {{level}}.", "OnlyEquippedLevel": "Esto solo puede equiparse en personajes de nivel igual o superior a {{level}}.", "PostmasterAlmostFull": "¡Casi lleno!", "PostmasterFull": "¡Lleno!", "PreviewVendor": "Previsualizar {{type}} contenidos", "StackFull": "Ya tienes una pila completa de {{name}}", "StoreName": "{{genderRace}}{{className}}" }, "KillType": { "ClassAbilities": "Habilidad de Clase", "Finisher": "Remate", "Grenade": "Granada", "Melee": "Cuerpo a cuerpo", "Precision": "Precisión", "Super": "Súper" }, "LB": { "AddStack": "Añadir otra copia de este mod", "AdvancedOptions": "Opciones avanzadas", "ChooseAMod": "Elige tus mods", "ChooseASetBonus": "Elige tu set de bonificacion", "ChooseAnExotic": "Elige tu excepcional", "ClearLocked": "Quitar bloqueo", "ContainsVendorItems": "Este equipamiento contiene objetos de vendedores", "Current": "Actual", "Equip": "Equipar en {{character}}", "Exclude": "Objetos Excluidos", "ExcludeHelp": "Shift + click en un objeto (o arrástralo y suéltalo en este espacio) para crear un conjunto sin equipo específico.", "ExistingBuildStats": "Estadísticas de Equipación Existentes", "ExistingBuildStatsNote": "Mostrando solo equipaciones con estadísticas estríctamente más altas.", "FilterSets": "Filtro de conjuntos", "Help": { "And": "Armadura con todos estos perks será usada (\"y\")", "ChangeNodes": "Cambia los nodos de Intelecto, Disciplina y Fuerza en el juego a lo que se muestra para crear cada equipamiento.", "Discipline": "La Disciplina acelera la recarga de tiempo de la Granada", "DragAndDrop": "Arrastra y suelta objetos en las cajas fijas para crear conjuntos con ese equipo solamente", "Help": "¿Necesitas ayuda?", "HigherTiers": "Los Niveles Altos son mejores", "Intellect": "El Intelecto acelera la recarga de tiempo de la Súper", "Lock": "Fija un conjunto de perks dando click al botón de fijar y seleccionando los perks", "MultiPerk": "Para usar una armadura con múltiples perks juntos, presiona shift+click en los perks deseadas", "NoPerk": "Si un perk no aparece, quiere decir que no tienes alguna armadura que cuente con ese perk", "Or": "Armadura con cualquiera de estos perks será usada (\"o\")", "ShiftClick": "Haz click Shift en un objeto para crear equpaciones sin ese equipo", "StatsIncrease": "Mientras el nivel de defensa de los objetos incremente, las estadísticas en ese objeto (int/dis/fuer) también incrementan.", "Strength": "La Fuerza acelera la recarga de tiempo del ataque Cuerpo a Cuerpo", "Synergy": "Intenta encontrar armadura que tenga el perk de incremento de munición para el tipo de armas que usas.", "Tier11Example": "4/5/2 (equipo Nivel 11) es 4 de Intelecto, 5 de Disciplina y 2 de Fuerza (4+5+2 = Nivel 11)" }, "HideAllConfigs": "Ocultar todas las configuraciones", "HideConfigs": "Ocultar configuraciones", "IncompatibleWithOptimizer": "Este objeto es incompatible con el Optimizador. Por favor, readquiere una versión nueva desde Colecciones.", "LB": "Optimizador de Equimiento", "LightMode": { "HelpCurrent": "Calcula los equipamientos con los niveles de defensa actuales.", "HelpScaled": "Calcula los equipamientos como si todos los objetos tuvieran 350 de defensa.", "LightMode": "Modo claro" }, "Loading": "Cargando las mejores equipaciones", "LockEquipped": "Fijar equipado", "LockPerk": "Fijar perk", "Locked": "Objetos Bloqueados", "LockedHelp": "Arrastra y suelta cualquier objeto en su espacio para crear un conjunto con ese equipamiento específico. Shift + click para excluir objetos.", "Missing2": "¡Faltan piezas raras, legendarias o excepcionales para crear un conjunto completo!", "ProcessingMode": { "Fast": "Rápido", "Full": "Completo", "HelpFast": "Solo busca tu mejor equipamiento.", "HelpFull": "Busca más equipamiento, pero tarda más.", "ProcessingMode": "Modo de proceso" }, "RemoveStack": "Eliminar una copia de este mod", "Scaled": "Escalado", "SearchAMod": "Buscar un mod o descripción", "SearchASetBonus": "", "SearchAnExotic": "Busca un excepcional o descripción", "SelectExotic": "Seleccionar excepcional", "SelectMods": "Seleccionar Mods", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Mods de Actividad", "SelectSetBonus": "Elige set de Bonificación", "SelectSubclassOptions": "Personalizar subclase", "ShowAllConfigs": "Mostrar todas las configuraciones", "ShowConfigs": "Mostrar configuraciones", "ShowGear": "Armadura de {{class}}", "Vendor": "Incluir objetos de vendedores" }, "Loading": { "Accounts": "Cargando cuentas de Destiny...", "Code": "Cargando código de DIM...", "FilterHelp": "Cargando ayuda de búsqueda...", "Profile": "Cargando perfil de Destiny...", "Vendors": "Cargando vendedores de Destiny..." }, "LoadoutAnalysis": { "Analyzed": "{{numLoadouts}} Equipaciones Analizadas", "Analyzing": "Analizando {{numAnalyzed}}/{{numLoadouts}} Equipaciones", "BetterStatsAvailable": { "Description": "Eligiendo armaduras o mods diferentes para esta equipación permitirá alcanzar estadísticas más altas. Elige \"$t(Loadouts.OpenInOptimizer)\" para ver mejores equipaciones.", "Name": "Mejores Estadísticas Disponibles" }, "BetterStatsAvailableFontNote": "Nota: Esta Equipación usa los mods \"Manantial de ...\" que causan que una estadística exceda 200. DIM podría identificar mejores estadísticas reduciendo la cantidad de exceso de estadísticas. Si no se deseas esto, desactiva \"$t(Loadouts.IncludeRuntimeStatBenefits)\" en la Equipación.", "DoesNotRespectExotic": { "Description": "Esta configuración de equipación del Optimizador de Equipaciones especifica la elección de un excepcional, pero la equipación no coincide con ese excepcional.", "Name": "Excepcional Incorrecto" }, "DoesNotSatisfyStatConstraints": { "Description": "La configuración del Optimizador de Equipaciones para esta Equipación especifica unos mínimos de estadísticas, pero la Equipación no las alcanza.", "Name": "Mínimos de Estadísticas Incorrectos" }, "EmptyFragmentSlots": { "Description": "Hay ranuras de fragmentos vacías en esta subclase.", "Name": "Ranuras de Fragmentos Vacías" }, "InvalidMods": { "Description": "Algunos mods de esta equipación están obsoletos o no encajan de otra manera en cualquiera de tus piezas de armadura.", "Name": "Mods Obsoletos" }, "InvalidSearchQuery": { "Description": "Este equipamiento fue creado con una consulta de búsqueda en Optimizador de Equipamiento que no es válido.", "Name": "Consulta Inválida" }, "ItemsDoNotMatchSearchQuery": { "Description": "Este equipamiento se creó con una consulta de búsqueda en el Optimizador de Equipamiento, y esa consulta de búsqueda excluye al menos uno de los elementos del equipamiento.", "Name": "Búsqueda excluye elementos" }, "MissingItems": { "Description": "Algunos de los objetos en esta equipación ya no están en tu inventario.", "Name": "Objetos Faltantes" }, "ModsDontFit": { "Description": "La armadura en esta equipación no se puede acomodar a todos los mods de la equipación, incluso si la armadura fuera mejorada.", "Name": "Mods Sin Asignar" }, "NeedsArmorUpgrades": { "Description": "La armadura en esta equipación necesita ser mejorada para acomodar todos los mods o alcanzar estadísticas especificadas.", "Name": "Necesita Mejoras de Armadura" }, "NotAFullArmorSet": { "Description": "Esta equipación no pudo ser analizada a fondo porque no incluía un conjunto completo de armadura.", "Name": "No Es Un Set De Armadura Completo" }, "TooManyFragments": { "Description": "Hay más fragmentos configurados en la subclase que los otorgados por los aspectos.", "Name": "Demasiados Fragmentos" }, "UsesSeasonalMods": { "Description": "Esta equipación se basa en mods que sólo están disponibles en algunas temporadas. Cuando la temporada termina, algunos mods no estarán disponibles o excederán la capacidad de energía de la armadura.", "Name": "Usa Mods de Temporada" } }, "LoadoutBuilder": { "All": "Todos", "AlwaysAutoMods": "Los mods Artificiosos y de Ajuste siempre serán elegidos automáticamente.", "AnyExotic": "Cualquier Excepcional", "AnyExoticDescription": "Los conjuntos deben contener un excepcional, pero cualquier excepcional sirve.", "Artifice": "Artificiosa", "AssumeMasterwork": "Asumir Obra Maestra", "AssumeMasterworkOptions": { "All": "Todas las Armaduras: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "Todas las Armaduras: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExóticos de Armadura 2.0: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Mejorada para aceptar modificadores de estadísticas Artificiosas.", "Current": "Estadísticas actuales, asumiendo nivel de energía de al menos {{minLoItemEnergy}}.", "Legendary": "Legendarias: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExóticas: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Bonificación de estadísticas de Obra Maestra Completa, asumiendo nivel de energía de al menos 10.", "None": "Todas las armaduras: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "Añadir automáticamente mods de estadísticas", "AutomaticallyPicked": "Este mod fue añadido automáticamente para mejorar las estadísticas de la equipación.", "CompareLoadout": "Comparar Equipamiento", "ConfirmOverwrite": "¿Estás seguro de que quieres reemplazar la armadura en el equipamiento \"{{name}}\" con este nuevo set de armadura?", "DecreaseStatPriority": "Reducir prioridad a la estadística", "DisabledByAutoStatMods": "Los mods de estadísticas están siendo elegidos automáticamente por el Optimizador de Equipaciones.", "DisabledDueToMaintenance": "El Optimizador de Equipamiento está actualmente desactivado debido al mantenimiento de la API de Bungie.", "EquipItems": "Equipa", "ExcludeItem": "Excluir objeto", "ExcludeVendors": "Busca \"not:vendor\" para excluir objetos de comerciantes del Optimizador de Equipaciones.", "ExcludedItems": "Objetos excluidos", "ExistingLoadout": "Equipamiento Existente", "Exotic": "Armadura Exótica", "ExoticClassItemPerks": "Si quieres perks específicas, usa búsquedas como exactperk:\"spirit of verity\". Haz clic en las ventajas de los resultados del Optimizador para añadirlas o eliminarlas del filtro de objetos.", "ExoticSpecialCategory": "Especial", "FOTLWildcardWarning": "Este conjunto contiene una máscara de La Fiesta de las Almas Perdidas. Aplica manualmente el modificador correcto para activar los bonificadores de conjunto.", "Filter": "Configuración", "IgnoreStat": "Si no está marcado, el Optimizador de Equipaciones pretenderá que esta estadística no existe cuando se creen equipaciones", "IncreaseStatPriority": "Incrementar prioridad a la estadística", "Legendary": "Legendarias", "LimitToNewFeaturedGear": "Limitar al equipo nuevo/destacado", "LockItem": "Anclar objeto", "MissingClass": "El equipamiento es para: {{className}}", "MissingClassDescription": "El equipamiento que estás intentando visualizar es para una clase de personaje que no tienes.", "MwExotic": "Exótico", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "Una consulta de búsqueda activa está restringiendo los objetos que DIM puede incluir en los equipamientos", "AllowAutoStatMods": "Permitir que DIM incluya automáticamente mods adicionales de estadística", "AlwaysInvalidMods": "Estos mods no encajan en ninguno de los objetos que tienes:", "AssumeMasterworked": "Permitir que DIM recomiende convertir armadura en obra maestra", "AssumptionsRestricted": "DIM no puede recomendar cambios de energía de armadura:", "BadSlot": "En la ranura {{bucketName}} ninguno de los objetos permitidos pudieron acomodar estos mods:", "ExoticDoesNotExist": "No tienes ninguna de las armaduras exóticas seleccionadas en tu inventario.", "Header": "No se encontraron equipamientos. Estas son las posibles razones por las que DIM no pudo encontrar equipamientos:", "LowerBoundsFailed": "Muchas equipaciones no cumplieron los requisitos de estadísticas mínimas", "MaybeAllowMoreItems": "Considera permitir otros objetos:", "MaybeDecreaseLowerBounds": "Considera reducir los requisitos de estadísticas mínimas", "MaybeRemoveMods": "Considera eliminar ciertos mods:", "MaybeRemoveSearchQuery": "Considera limpiar o cambiar el filtro en la barra de búsqueda", "ModAssignmentFailed": "Muchas equipaciones no pudieron acomodar todos los mods solicitados", "RemoveMods": "Elimina estos mods", "RemoveSetBonuses": "Considera eliminar algunas bonificaciones de conjunto", "SetBonuses": "Has elegido algunos bonificadores de conjunto, quizás no tengas los objetos correctos para usarlos." }, "NoExotic": "Sin Exótico", "NoExoticDescription": "Equivalente a buscar \"not:exotic\" en la barra de búsqueda - los conjuntos no usarán ninguna armadura exótica.", "NoExoticPreference": "Sin Exóticos Seleccionados", "NoExoticPreferenceDescription": "La armadura exótica será usada si maximizas las estadísticas.", "NoLoadoutsToCompare": "No hay equipamientos para comparar", "None": "Ninguno", "OptimizerExplanationGuide": "Lee la Guía de Usuario para más información y un vídeo tutorial.", "OptimizerExplanationMods": "Elige un exótico, mods y una subclase. Esto contribuirá a las estadísticas de la equipación, mientras que los mods que ya estaban en la armadura serán ignorados.", "OptimizerExplanationSearch": "Utilice la barra de búsqueda para limitar la armadura a considerar, por ejemplo, {{example}}. Si ninguna armadura en una ranura coincide con la búsqueda, todos los objetos se considerarán para esa ranura.", "OptimizerExplanationStats": "Arrastra las estadísticas más importantes arriba y desmarca las que no quieras maximizar.", "OptimizerSet": "Set Optimizador", "PinnedItems": "Objetos Fijados", "PinnedItemsFinePrint": "Los filtros de búsqueda están guardados con la configuración del Optimizador de Equipaciones, pero los objetos marcados y excluidos no. Los marcados y las exclusiones serán ignoradas cuando DIM compruebe Equipaciones existentes para mejores estadísticas de equipaciones.", "ProcessingSets": "Encontrando los conjuntos de estadísticas más altas...", "SaveAs": "Guardar como", "SetBonus": "Set de Bonificación", "SpeedReport": "Evaluados {{combos, number}} combinaciones en {{time}} segundos usando {{cpus}} núcleos de CPU.", "StatConstraints": "Prioridades & Rangos de Estadísticas", "StatMax": "Máx", "StatMin": "Min", "StatRangeTooltip": "Con la configuración actual de min/max, existen equipaciones que tienen de {{min}} a {{max}} puntos en esta estadística. Haz doble clic para configurar el min a {{max}}.", "StatTotal": "Total: {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "No se pueden agregar todos los mods.", "UnableToAddAllModsBody": "No había suficientes espacios disponibles para que cupiera el mod de {{mods}}.", "UnlockItem": "Desanclar objeto" }, "LoadoutFilter": { "Contains": "Muestra equipaciones las cuales tengan un objeto o un mod que coincidan con el texto filtrado. Busca por objetos con espacios en su nombre usando comillas.", "FashionOnly": "Muestra equipaciones que contengan solo cosméticos (shaders o diseños).", "LoadoutLight": "Muestra equipaciones basadas en su nivel de luz calculado. Usa la palabra clave \"pinnaclecap\" o \"softcap\" en vez de un número para referirse a límites de poder de la temporada actual.", "ModsOnly": "Muestra equipaciones que solo contengan mods de armadura.", "Name": "Muestra equipaciones cuyo nombre coincida exactamente (exactname:) o parcialmente (name:) con el texto filtrado. Busca frases enteras usando comillas.", "Notes": "Busca equipaciones por el campo de sus notas.", "PartialMatch": "Muestra equipaciones donde su nombre o notas tengan una coincidencia parcial con el texto filtrado. Busca por frases enteras usando comillas.", "Season": "Muestra equipaciones según la temporada de Destiny 2 en la que fueron modificados por última vez.", "Subclass": "Muestra equipaciones cuyo nombre de subclase o tipo de daño coincida parcialmente con el texto filtrado." }, "Loadouts": { "Abilities": "Habilidades", "Actions": "Acciones para {{title}}", "AddEquippedItems": "Añadir Equipados", "AddNotes": "Agregar nota", "AddUnequippedItems": "Añadir Desequipados", "Any": "Cualquier clase", "Apply": "Aplicar", "ApplyInGameLoadoutInGame": "Tu equipamiento está listo, pero ya que estás en una actividad necesitas equiparla en el juego.", "ApplyMods": "Aplicando mods", "ApplySearch": "Transferir búsqueda \"{{query}}\"", "ArmorStats": "Estadísticas de Armadura", "ArtifactUnlocks": "Desbloqueos del Artefacto", "ArtifactUnlocksDesc": "Debido a limitaciones de Bungie.net, DIM no puede configurar automáticamente tu artefacto. Necesitas realizar estos desbloqueos en el juego antes de aplicar el Equipamiento.", "ArtifactUnlocksWithSeason": "Desbloqueos del Artefacto - T{{seasonNumber}}", "BadLoadoutShare": "No se pudo cargar el equipamiento compartido", "BadLoadoutShareBody": "El equipamiento que estás intentando cargar es inválido: {{error}}", "Before": "Antes '{{name}}'", "CancelEditing": "Cancelar Edición", "CannotCustomizeSubclass": "Esta subclase no puede ser configurada", "ChooseItem": "Añadir {{name}}", "ClassType": "Equipación para cualquier clase", "ClassTypeMismatch": "Un objeto de {{className}} no puede ser añadido a este equipamiento", "ClassTypeMissing": "No tienes un {{className}} para el cual crear un equipamiento", "ClassType_female": "{{className}} equipación", "ClassType_male": "{{className}} equipación", "Classified": "Algunos de tus objetos son clasificados y no pueden ser incluidos en el cálculo de Poder máximo.", "ClearLoadoutParameters": "Eliminar configuración del Optimizador de Equipamientos", "ClearSection": "Quitar todo", "ClearSpace": "Quitar objetos no usados", "ClearSpaceArmor": "Quitar armaduras no usadas", "ClearSpaceWeapons": "Quitar armas no usadas", "ClearUnsetMods": "Quitar otros mods", "ClearingSpace": "Moviendo otros objetos", "CopyAndEdit": "Editar Copia", "Create": "Crear equipamiento", "CurrentlyEquipped": "Equipado en estos momentos", "Deequip": "Desequipando objetos de otros personajes", "Delete": "Borrar", "DimLoadouts": "Equipamientos DIM", "Edit": "Editar equipamiento", "EditBrief": "Editar", "EquipInGameLoadout": "Equipando equipamiento en el juego", "EquipItems": "Equipando objetos", "EquippableDifferent1": "Múltiples objetos Excepcionalas fueron usados para calcular tu Poder Máximo, por lo que el número mostrado podría no alcanzarse cuando equipes tus objetos dentro del juego.", "EquippableDifferent2": "El Poder Máximo no está limitado por la regla del \"Excepcional Único\" cuando se determina el Poder de tus recompensas de botines/poderosos/hito.", "Failed": "El equipamiento falló por completo al aplicar", "Fashion": "Elegir cosméticos", "FashionOnly": "Solo moda", "FillFromEquipped": "Rellenar usando equipados", "FillFromInventory": "Rellenar usando no equipados", "FilteredItems": "Objetos filtrados", "FindAnother": "Encontrar otro {{name}}", "FromEquipped": "Equipado", "Generated": "{{statTotal}} Puntos de Estadísticas de Equipación", "HashtagTip": "Tip: Usa #hashtags en los nombres o notas de tu equipamiento y se mostrarán aquí.", "Import": { "BadURL": "No es un enlace válido para compartir el equipamiento.", "Error": "Error obteniendo equipamiento:", "Error404": "Este equipamiento no existe.", "PasteHere": "Pega un enlace de equipamiento para abrir el equipamiento." }, "ImportLoadout": "Importar Equipamiento", "InGameActions": "Acciones para Equipamientos En El Juego", "InGameLoadouts": "Equipamientos En El Juego", "IncludeRuntimeStatBenefits": "Incluir estadísticas de modificador Fontana", "IncludeRuntimeStatBenefitsDesc": "Los mods de armadura de \"Fontana de ...\" proveen una mejora lineal a las estadísticas del personaje mientras tengas Cargas de Armadura.\n\nCon esta configuración, DIM considera estos modificadores activos y añade estos beneficios a las estadísticas de esta Equipación en cálculos y optimizaciones.", "ItemErrorSummary_one": "1 error de objeto:", "ItemErrorSummary_other": "{{count}} errores de objetos:", "ItemLeveling": "Nivelación de objeto", "LoadoutName": "Nombre del equipamiento", "LoadoutParameters": "Configuración del Optimizador de Equipamientos", "LoadoutParametersExotic": "El equipamiento debe incluir este excótico: {{exoticName}}", "LoadoutParametersQuery": "Los objetos deben coincidir con este filtro de búsqueda", "LoadoutParametersStats": "Prioridad de estadísticas y rangos de estadísticas mínimos/máximos", "Loadouts": "Equipamientos", "MakeRoom": "Hacer espacio en Administración", "MakeRoomDone_female_one": "Se terminó de hacer espacio para 1 objeto de la Administración al mover 1 objeto fuera de {{store}}.", "MakeRoomDone_female_other": "Se terminó de hacer espacio para {{count}} objetos de la Administración al mover {{movedNum}} objetos fuera de {{store}}.", "MakeRoomDone_male_one": "Se terminó de hacer espacio para 1 objeto de la Administración al mover 1 objeto fuera de {{store}}.", "MakeRoomDone_male_other": "Se terminó de hacer espacio para {{count}} objetos de la Administración al mover {{movedNum}} objetos fuera de {{store}}.", "MakeRoomDone_one": "Se terminó de hacer espacio para 1 objeto de la Administración al mover 1 objeto fuera de {{store}}.", "MakeRoomDone_other": "Se terminó de hacer espacio para {{count}} objetos de la Administración al mover {{movedNum}} objetos fuera de {{store}}.", "MakeRoomError": "Imposible hacer espacio para todos los objetos en la Administración: {{error}}.", "ManageLoadouts": "Administrar Equipamiento", "MaxSlots": "Solo puedes tener {{slots}} {{bucketName}} en un equipamiento.", "MaximizeLight": "Luz Máxima", "MaximizePower": "Poder máximo", "MaximizeStat": "Maximizar Estadística", "MissingItemsWarning": "Algunos de los objetos en esta equipación ya no están en tu inventario.", "ModErrorSummary_one": "1 Error de mod:", "ModErrorSummary_other": "{{count}} errores de mods:", "ModPlacement": { "InvalidMods": "Mods no válidos", "InvalidModsDesc_one": "1 mod no pudo encajar en ninguna pieza de armadura.", "InvalidModsDesc_other": "{{count}} mods no pudieron encajar en ninguna pieza de armadura.", "ModPlacement": "Colocación de Mods", "StackableMod": "Apilable", "UnassignedMods": "Mods Sin Asignar", "UnassignedModsDesc_one": "1 modificador no encajó debido a energía insuficiente o ranuras para modificadores. Las mejoras de energía para la armadura seleccionada no arreglarán este problema.", "UnassignedModsDesc_other": "{{count}} modificadores no encajaron debido a energía insuficiente o ranuras para modificadores. Las mejoras de energía para la armadura seleccionada no arreglarán este problema.", "UnstackableMod": "No Apilable", "UpgradeCosts": "Costos de Mejoras", "UpgradeCostsDesc": "Algunas armaduras necesitan mejoras de capacidad de energía para encajar los mods solicitados. En total, estas mejoras cuestan:" }, "Mods": "Mods", "ModsOnly": "Solo Mods", "MoveItems": "Moviendo objetos", "NoSpace": "Estas sin espacio en el depósito y en cualquier otro personaje.", "NoneMatch": "Ninguno de tus equipamiento coincidió con los filtros.", "NotStarted": "Esperando a que otras acciones se completen, o a que se actualice el inventario para terminar de cargar", "NotesPlaceholder": "Escribe algunas notas sobre este equipamiento, o usa #hashtags para categorizarla", "NotificationTitle": "Equipamiento: {{name}}", "OnWrongCharacterAdvice": "Haz clic aquí para encontrar los objetos de mayor potencia de este personaje.", "OnWrongCharacterWarning": "La armadura más poderosa de este personaje está en otro personaje. Para que te cuente y te suelten recompensas de Poder, poderosas e hito, la armadura debe estar en este personaje o en el Depósito.", "OnlyItems": "Solo objetos equipables, materiales, y consumibles pueden añadirse a un equipamiento.", "OpenInOptimizer": "Optimizar Armadura", "OpenOnStreamDeck": "Abrir en Stream Deck", "PickArmor": "Escoger Armadura", "PickMods": "Añadir mods de armadura", "Prismatic": { "Aspect": "Aspecto Prismático", "Grenade": "Granada Prismática", "Melee": "Cuerpo a cuerpo Prismático", "Super": "Super Habilidad" }, "PullFromPostmaster": "Recoger de la Administración", "PullFromPostmasterError": "Imposible retirar de la Administración: {{error}}.", "PullFromPostmasterGeneralError": "No se pudo extraer todos los elementos de la Administración.", "PullFromPostmasterNotification_female_one": "Extrayendo 1 objeto de Administración a {{store}}.", "PullFromPostmasterNotification_female_other": "Extrayendo {{count}} objetos de Administración a {{store}}.", "PullFromPostmasterNotification_male_one": "Extrayendo 1 objeto de Administración a {{store}}.", "PullFromPostmasterNotification_male_other": "Extrayendo {{count}} objetos de Administración a {{store}}.", "PullFromPostmasterNotification_one": "Extrayendo 1 objeto de Administración a {{store}}.", "PullFromPostmasterNotification_other": "Extrayendo {{count}} objetos de Administración a {{store}}.", "PullFromPostmasterPopupTitle": "Extraer de la Administración", "Random": "Aleatorio", "Randomize": "Aleatorizar Equipamiento", "RandomizeButton": "Aleatorizar", "RandomizeNew": "Crear aleatoria", "RandomizeQueryHint": "Consejo: Busca objetos primero para restringir qué objetos pueden ser elegidos aleatoriamente.", "RandomizeSearch": "Aleatorizar desde la Búsqueda", "RandomizeSearchPrompt": "Aleatorizar tus objetos equipados desde la búsqueda \"{{query}}\"?", "Redo": "Rehacer", "RestoreAllItems": "Todos los objetos", "SalvationsEdgeMods": "Modificadores de El Filo de la Salvación", "Save": "Guardar", "SaveAsDIM": "Guardar como Equipamiento DIM", "SaveAsNew": "Guardar como Nuevo", "SaveAsNewTooltip": "Mantener el equipamiento original y guardar esta como una nueva", "SaveDisabled": { "AlreadyExists": "Elige un nuevo nombre para el equipamiento.", "Empty": "El equipamiento está vacío.", "NoName": "Este equipamiento necesita un nombre." }, "SaveLoadout": "Guardar Equipamiento", "Season": "Temporada {{season}}", "SetBonusesDesc": "Set de Bonificación requerida", "Share": { "Copied": "Enlace copiado del equipamiento al portapapeles", "CopyButton": "Copiar Enlace", "Error": "Error obteniendo enlace para compartir", "Fashion": "Moda (shaders y diseños)", "LoadoutOptimizer": "Configuración del Optimizador de Equipamientos", "NativeShare": "Compartir Enlace", "Notes": "Notas", "NumItems_one": "{{count}} objeto - el recipiente será preguntado para seleccionar a un objeto comparable de su inventario", "NumItems_other": "{{count}} objetos - los recipientes serán preguntados para seleccionar objetos comparables en su inventario", "NumMods_one": "{{count}} mod", "NumMods_other": "{{count}} mods", "Placeholder": "Cargando enlace compartido", "Subclass": "Personalización de subclase", "Summary": "Compartir este equipamiento que contiene:", "Title": "Compartir \"{{name}}\"" }, "ShareLoadout": "Compartir", "ShowModPlacement": "Mostrar Colocación de Mods", "Snapshot": "Guardar como Equipamiento En El Juego", "SocketOverrides": "Cambiando opciones de subclase", "SortByEditTime": "Ordenar por última edición", "SortByName": "Ordenar por nombre", "SubclassOptions": "Opciones de {{subclass}}", "SubclassOptionsSearch": "Buscar opciones de {{subclass}}", "Succeeded": "Equipamiento exitoso", "SyncFromEquipped": "Sincronizar desde equipados", "TooManyRequested": "Tienes {{total}}{{itemname}} pero tu equipamiento pide por {{requested}}. Transferimos todo lo que tenías.", "TuningMods": "Mods de Ajuste", "UnassignedModError": "El mod no encajó en tu armadura actual", "Undo": "Deshacer", "Update": "Guardar Cambios", "UpdateLoadout": "Actualizar Equipamiento", "VendorsCannotEquip": "No tienes estos objetos. Pulsa para escoger otro reemplazo o haz click en la X para quitar:" }, "Manifest": { "Download": "Descargando la última información de la base de datos desde Bungie...", "Error": "Error cargando la información de la base de datos de Destiny:\n{{error}}\nRecarga para reintentar.", "Load": "Cargando información de la base de datos de Destiny..." }, "Milestone": { "Daily": "Desafío Diario", "OneTime": "Desafío Único", "SeasonalRank": "Rango {{rank}} de Temporada", "Special": "Desafío de Evento Especial", "Tutorial": "Tutorial de Desafíos", "Unknown": "Desafío", "Weekly": "Desafío Semanal" }, "Mods": { "HarmonicModDescription": "El efecto de este mod tiene un costo reducido y cambia el elemento dependiendo de la subclase equipada." }, "MoveAmount": { "Amount": "Cantidad:" }, "MovePopup": { "Acquired": "Este objeto está desbloqueado en colecciones.", "AcquiredMod": "Este mod está desbloqueado en colecciones.", "AddNote": "Añadir notas", "AddToLoadout": "Equipamiento", "AddToLoadoutTitle": "Añadir esto al equipamiento", "All": "Todos", "ArtifactBreaker": "Esta arma tiene {{breaker}} debido a una perk del artefacto desbloqueada.", "CannotCurrentlyRoll": "Este perk no se puede obtener en la versión actual de este objeto.", "CantPullFromPostmaster": "Debes visitar la Administración en el juego para recuperar este objeto.", "CatalystProgress": "Progreso del Catalizador", "CommunityData": "Perspectiva de la Comunidad", "Consolidate": "Consolidar", "DistributeEvenly": "Distribuir Equitativamente", "EnhancementTier": "Nivel {{tier}}", "Equip": "Equipar en:", "EquipWithName": "Equipar al {{character}}", "FavoriteUnFavorite": { "Favorite": "{{itemType}} Favorita", "Favorited": "Favoritos", "Unfavorite": "{{itemType}} Menospreciada", "Unfavorited": "Quitar de Favoritos" }, "Infuse": "Infundir", "InfuseTitle": "Abrir el buscador de objetos de infusión", "IntrinsicBreaker": "Esta arma tiene intrínsicamente {{breaker}}.", "LoadingSockets": "Los perks y los detalles de estadísticas no se han cargado todavía para este objeto.", "LockUnlock": { "AutoLock": "El estado de bloqueo se sincronizó con la etiqueta de este objeto", "Lock": "Bloquear {{itemType}}", "Locked": "Bloqueado", "Unlock": "Desbloquear {{itemType}}", "Unlocked": "Desbloqueado" }, "MissingSockets": "Los detalles de modificadores y perks no estarán disponibles mientras Bungie esté actualizando sus servicios. Volverán cuando estén listos, normalmente en unas pocas horas.", "Notes": "Notas:", "OpenOnStreamDeck": "Abrir en Stream Deck", "OverviewTab": "Resumen", "Owned": "Este objeto está en tu inventario.", "OwnedMod": "Este mod está en el inventario de modificadores.", "PullItem": "Mover de {{bucket}} a {{store}}", "PullPostmaster": "Retirar de la Administración", "ReadLore": "Lee lore en Ishtar Collective", "ReadLoreLink": "Leer lore", "Rewards": "Recompensas:", "SendToVault": "Enviar al depósito", "Store": "Mandar a:", "StoreWithName": "Mandar a {{character}}", "Subtitle": { "QuestProgress": "Paso {{questStepNum}} de {{questStepsTotal}}", "Type": "{{classType}} {{typeName}}" }, "TabList": "Pestañas de detalles de objetos", "ToggleSidecar": "Expandir o colapsar acciones de objeto", "TrackUntrack": { "Track": "Seguir {{itemType}}", "Tracked": "Con Seguimiento", "Untrack": "No seguir {{itemType}}", "Untracked": "Sin Seguimiento" }, "TriageTab": "Triaje", "UnreliablePerkOption": "Esta perk sólo aparece en la vista de colecciones. Podría no aparecer aleatoriamente como roll en este objeto.", "Vault": "Depósito", "WeaponLevel": "Nivel de arma {{level}}" }, "Notes": { "Error": "¡Error! Máximo 120 caracteres por notas.", "Help": "Añadir notas, #hashtags y :symbols:" }, "Notification": { "Cancel": "Cancelar", "OK": "Descartar" }, "Objectives": { "Complete": "Completo", "Incomplete": "Incompleto" }, "Organizer": { "BulkMove": "Mover a", "BulkMoveLoadoutName": "Seleccionado en el Organizador", "BulkTag": "Etiqueta", "Columns": { "Ammo": "Munición", "Archetype": "Arquetipo", "BaseStats": "Estadísticas Base", "Breaker": "Triturador", "Crafted": "Fecha de Creación", "CustomTotal": "Total Personalizado", "Damage": "Daño", "Energy": "Energía", "Event": "Evento", "Featured": "Equipamiento Nuevo", "Foundry": "Forja", "Frame": "Armazón", "Harmonizable": "Armonizable", "Holofoil": "Holometalizadas", "Icon": "Icono", "ItemTier": "Nivel", "KillTracker": "Bajas", "Level": "Nivel", "Loadouts": "Equipamientos", "Location": "Ubicación", "Locked": "Bloqueado", "MasterworkStat": "Estadísticas OM", "MasterworkTier": "Nivel OM", "ModSlot": "Espacio para Mod", "Mods": "Mods", "Name": "Nombre", "New": "Nuevo", "Notes": "Notas", "OriginTraits": "Rasgo Original", "OtherPerks": "Componentes de Arma", "PercentComplete": "% Completo", "Perks": "Perks", "PerksGrid": "Cuadrícula de Perks", "Power": "Poder", "Quality": "% Calidad", "Recency": "Recientemente", "Season": "Temporada", "Shaders": "Diseños", "Source": "Fuente", "StatQuality": "Calidad de las Estadísticas", "StatQualityStat": "{{stat}}%", "Stats": "Estadísticas", "Tag": "Etiqueta", "TertiaryStat": "3ª Estadística", "Tier": "Rareza", "Traits": "Rasgos de Armas", "TuningStat": "Afinador", "WishList": "Lista de Deseos", "WishListNotes": "Notas de la Lista de Deseos", "Year": "Año" }, "EnabledColumns": "Columnas Habilitadas", "Lock": "Bloquear", "NoItems": "No hay objetos que coincidan con los filtros. Si tienes una consulta de búsqueda, intenta limpiarla.", "NoMobile": "Gira tu teléfono en forma horizontal para usar el Organizador.", "Note": "Establecer Notas", "OpenIn": "Mostrar en el Organizador", "Organizer": "Organizador", "SelectAll": "Seleccionar Todo", "SelectItem": "Seleccionar o deseleccionar {{name}}", "ShiftTip": "Consejo: Mantén la tecla SHIFT apretada y haz click en una celda para filtrar objetos", "Stats": { "Aim": "Puntería", "Airborne": "Efec. Aérea", "AmmoGeneration": "Munición generada", "Power": "Poder", "RPM": "DPM", "Recoil": "Retroceso", "Reload": "Recarga" }, "Unlock": "Desbloquear" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "¡La Administración está casi llena! ({{number}}/{{postmasterSize}})", "PostmasterFull": "¡La Administración está llena! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "Contratos", "CatalystSource": "Fuente: {{source}}", "CrucibleRank": "Rango", "Items": "Objetos de aventuras", "Milestones": "Hazañas & Desafíos", "NoEventChallenges": "Has completado todos los desafíos del evento", "NoTrackedTriumph": "No tienes triunfos para seguir. Sigue tantos como quieras en DIM.", "PaleHeartPathfinder": "Rastreador del corazón pálido", "PercentMax": "{{pct}}% para el máximo", "PercentPrestige": "{{pct}}% progreso para el reinicio", "PointsUsed_one": "1 punto usado", "PointsUsed_other": "{{count}} puntos usados", "PowerBonusHeader": "Recompensas De Poder +{{powerBonus}}", "PowerBonusHeaderUndefined": "Otras Recompensas", "Progress": "Progreso", "QueryFilteredTrackedTriumphs": "Ninguno de tus triunfos ha seguir coincidió con la búsqueda", "QuestExpired": "Caducado", "QuestExpires": "Caduca en ", "Quests": "Aventuras", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}pts", "Resets_one": "1 reinicio", "Resets_other": "{{count}} reinicios", "RewardPassEndsIn": "El Pase de Recompensas termina en ", "RewardPassPrestigeRank": "Rango de Prestigio {{rank}}", "SeasonalHub": "Centro de Operaciones de la Temporada", "StatTrackers": "Analizadores Estadísticos", "TrackedTriumphs": "Triunfos seguidos" }, "RecordBooks": { "HideCompleted": "Ocultar hazañas completadas", "RecordBooks": "Libros de Hazañas" }, "Records": { "Title": "Hazañas", "UniversalOrnamentSetOther": "Otro" }, "SearchHistory": { "Date": "Último uso", "DeleteAll": "Borrar todas las búsquedas sin marcar", "Description": "Estas son todas tus búsquedas anteriores y guardadas. Puedes eliminarlas desde aquí.", "Item": "Búsquedas de Objetos", "Link": "Ver y editar el historial de búsqueda", "Loadout": "Búsquedas de Equipaciones", "Query": "Buscar", "Title": "Historial de búsqueda", "UsageCount": "# Usado" }, "Settings": { "Appearance": "Apariencia", "ArmorArchetypeModslot": "Arquetipo de Armadura / Ranura para Mods", "AutoLockTagged": "Sincronizar el estado de bloqueo del objeto con la etiqueta", "AutoLockTaggedExplanation": "DIM automáticamente bloqueará y desbloqueará objetos que coincidan con su etiqueta. Los objetos fabricados se mantendrán desbloqueados para permitir reformarse. Cuando esta configuración esté habilitada, el icono de bloqueo no se mostrará en el mosaico para los objetos etiquetados.", "BadgePostmaster": "Muestra el número de objetos en la Administración para el personaje actual en el icono de la app", "BadgePostmasterExplanation": "Para que esto funcione necesitas instalar DIM como app y tu sistema operativo debe soportar mostrar insignias", "BothDescriptions": "Ambas Descripciones", "BungieDescriptionOnly": "Descripciones de Bungie", "CharacterOrder": "Ordenar personajes por", "CharacterOrderFixed": "Edad del personaje (falla en PC)", "CharacterOrderRecent": "Personaje más reciente", "CharacterOrderReversed": "Personaje más reciente (al revés)", "ColumnSize": "{{num}} objetos", "ColumnSizeAuto": "Automático", "CommunityData": "Perspectiva de la Comunidad sobre los Perks", "CommunityDescriptionOnly": "Descripciones de la Comunidad", "CsvImport": "Importar CSV", "CustomErrorLabel": "Un nombre de estadística debe contener caracteres de palabra y deben ser diferentes de otros nombres de estadística para esta clase de Guardián.", "CustomErrorValues": "La consideración de estadísticas debe ser números positivos.\nAl menos una consideración de 2 estadísticas debe estar por encima de cero.", "CustomStatChooseName": "Elige un nombre de Estadística Personalizada", "CustomStatCreate": "Crear una nueva estadística personalizada", "CustomStatDelete": "Borrar esta Estadística Personalizada", "CustomStatDeleteConfirm": "¿Borrar esta Estadística Personalizada?", "CustomStatDesc1": "Elige estadísticas de armadura deseadas para crear una estadística total personalizada.", "CustomStatDesc3": "La estadísticas personalizadas aparecerán en la Ventana Emergente, el Organizador y Comparar.", "CustomStatTitle": "Total de Estadística Personalizada", "Data": "Hojas de cálculo", "DefaultItemSizeNote": "Un objeto de 50px de tamaño será más nítido, sin hacer borrosa la imagen o el texto del objeto.", "DontForgetDupes": "No olvides de que puedes usar is:dupe para buscar rápidamente objetos duplicados, y que puedes usar la herramienta de comparación o el organizador para evaluar objetos relacionados.", "EnableAdvancedStats": "Mostrar la calificación de la calidad en la armadura (D1)", "ExpandSingleCharacter": "Mostrar todos los personajes", "ExportLoadoutSS": "Hojas de cálculo de equipamiento", "ExportLoadoutSSHelp": "Descargar una lista CSV de tus Equipaciones de DIM para que puedan ser fácilmente vistas en una aplicación de hojas de cálculo de tu elección.", "ExportProfile": "Exportar perfil de respuesta de la API", "ExportSS": "Hojas de cálculo de inventario", "ExportSSHelp": "Descargar una lista CSV de tus objetos que puede ser vista fácilmente en la aplicación de hojas de cálculo de tu preferencia.", "HidePullFromPostmaster": "Ocultar el botón \"$t(Loadouts.PullFromPostmaster)\"", "Inventory": "Visualización del Inventario", "InventoryColumns": "Ancho de inventario de personaje", "InventoryColumnsMobile": "Ancho del Inventario del personaje en el celular modo retrato", "InventoryColumnsMobileLine2": "Los objetos serán redimensionados para acomodarse a la nueva configuración", "InventoryNumberOfSpacesToClear": "Número de espacios vacíos a tener cuando uses el Modo Farmeo", "Items": "Visualización de Objetos", "Language": "Idioma", "LogOut": "Cerrar sesión", "Masterworked": "Obra Maestra Completa", "MaxParallelCores": "Núcleos máximos para tareas paralelas", "MaxParallelCoresExplanation": "Controla cuántos núcleos de la CPU puede usar DIM para tareas intensivas como el Optimizador de Equipaciones y el Analizador de Equipaciones. Valores más altos podrían mejorar el rendimiento pero usar más recursos del sistema.", "OrnamentDisplay": "Mostrar Diseños en las miniaturas de los objetos", "OrnamentDisplayExplanationDisabled": "Los objetos nunca mostrarán sus diseños", "OrnamentDisplayExplanationEnabled": "Pasar el cursor o mantener pulsado sobre una armadura ocultará su diseño", "OrnamentDisplayExplanationHide": "Pasar el cursor o mantener pulsado sobre un objeto mostrará su diseño", "OrnamentDisplayExplanationShow": "Pasar el cursor o mantener pulsado sobre un objeto ocultará su diseño", "ResetToDefault": "Reiniciar", "RestoreVaultSide": "Muestra objetos en el depósito en su propia columna", "ReverseSort": "Alternar orden normal/inverso", "SetSort": "Ordenar objetos por:", "SetVaultWeaponGrouping": "Agrupar armas del depósito por:", "Settings": "Configuración", "ShowNewItems": "Mostrar un punto rojo en objetos nuevos", "SingleCharacter": "Vista de Personaje Único", "SingleCharacterExplanation": "DIM sólo mostrará el personaje más reciente usado.\nLos objetos en posesión por personajes ocultos aparecerán en el depósito si pueden ser usados por el personaje actual.\nLos objetos específicos para otras clases estarán completamente ocultos.", "SizeItem": "Tamaño de objeto", "SortByAmmoType": "Tipo de Munición", "SortByAmount": "Tamaño de la Pila", "SortByClassType": "Clase Requerida", "SortByCrafted": "Creadas (D2)", "SortByDeepsight": "Visión Profunda (D2)", "SortByFeatured": "Equipamiento Nuevo / Destacado (D2)", "SortByPrimary": "Nivel de Poder", "SortByRarity": "Rareza", "SortByRating": "Calidad de la armadura (D1)", "SortByRecent": "Conseguidos Recientemente (D2)", "SortBySeason": "Temporada (D2)", "SortByTag": "Etiqueta ({{taglist}})", "SortByTier": "Nivel (D2)", "SortByType": "Tipo", "SortByWeaponElement": "Tipo de Daño", "SortCustom": "Orden Personalizado", "SortName": "Nombre", "SpacesSize_one": "{{count}} espacio", "SpacesSize_other": "{{count}} espacios", "Theme": "Tema", "Troubleshooting": "Resolución de problemas", "VaultArmorGroupingStyle": "Separa armadura por clase en diferentes líneas", "VaultGroupingNone": "Ninguno", "VaultUnder": "Muestra objetos en el depósito debajo de objetos equipados", "VaultWeaponGroupingStyle": "Separa grupos de armas en diferentes líneas", "WeaponFrame": "Armazón de Arma", "WishlistRefreshNotificationBody": "Si no ves ninguna actualización, ¡asegúrate de que la fuente (como GitHub) lo refleje!", "WishlistRefreshNotificationTitle": "Lista de Deseos Recargada" }, "Sockets": { "ApplyPerks": "Aplicar Perks", "GridStyle": "Mostrar perks como una cuadrícula", "Insert": { "Ability": "Equipar Habilidad", "Aspect": "Insertar Aspecto", "Fragment": "Insertar Fragmento", "Mod": "Insertar Mod", "Ornament": "Aplicar Diseño", "Projection": "Aplicar Proyección de Espectro", "Shader": "Aplicar Shader", "Super": "Equipar Súper", "Transmat": "Aplicar Efecto de Teletransporte" }, "ListStyle": "Mostrar perks en forma de lista", "Search": "Buscar nombres o descripciones", "Select": { "Ability": "Previsualizar Habilidad", "Aspect": "Previsualizar Aspecto", "Fragment": "Previsualizar Fragmento", "Mod": "Previsualizar Mod", "Ornament": "Previsualizar Diseño", "Projection": "Previsualizar Proyección de Espectro", "Shader": "Previsualizar Shader", "Super": "Previsualizar Súper", "Transmat": "Previsualizar Efecto de Teletransporte" }, "SelectWishlistPerks": "Vista previa de la lista de deseos de Perks" }, "Stats": { "CrouchingSpeed": "Agachado", "Custom": "Total Personalizado", "CustomDesc": "Estadísticas totales personalizadas de base seleccionadas, ignorando modificadores u obras maestras. Visita Ajustes para configurar cuales estadísticas están incluidas.", "DamageResistance": "Resistencia a Daño PvE", "Discipline": "Disciplina", "DropLevel": "Poder de la Cuenta", "DropLevelExplanation1": "El Poder de la Cuenta es el nivel del poder base cuando se calcula el nivel incrementado de las recompensas.", "DropLevelExplanation2": "El Poder de la Cuenta usa el nivel más alto en cada ranura, independientemente de la Clase o de la regla del \"Exótico Único\".", "EquippableGear": "Equipo Equipable", "FlinchResistance": "Resistencia a Estremecimiento", "HP": "HP (Puntos de salud)", "Intellect": "Intelecto", "MaxGearPower": "Poder Máximo del equipamiento equipable", "MaxGearPowerAll": "Poder Máximo de todo el equipamiento", "MaxGearPowerOneExoticRule": "Poder Máximo de equipo equipable\n(sólo una pieza de armadura Exótica equipada)", "MaxTotalPower": "Poder total máximo", "MetersPerSecond": "m/s", "Milliseconds": "ms", "NoBonus": "Sin bono", "NotApplicable": "N/A", "OfMaxRoll": "{{range}} del máximo posible de tiradas", "PercentHelp": "Haz click para más información acerca de su calidad estadística.", "Percentage": "%", "PowerModifier": "Poder otorgado por la progresión de experiencia de temporada", "Prestige": "Nivel de Prestigio: {{level}}\n{{exp}}xp para 5 motas de luz.", "Quality": "Calidad de las Estadísticas", "ShieldHP": "HP de Escudo", "StrafingSpeed": "Movimiento lateral", "Strength": "Fuerza", "TierProgress": "T{{tier}} {{statName}} ({{progress}}/60 para N{{nextTier}})\n", "TierProgress_Max": "N{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "Tiempo hasta HP Completos", "Total": "Total", "TotalHP": "HP Totales", "WalkingSpeed": "Caminando", "WeaponPart": "Partes del Arma" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM ahora puede almacenar tus etiquetas, equipamientos y configuraciones en nuestros propios servidores y sincronizar esa información entre diferentes versiones de DIM, sin inicio de sesión separadas. Puedes importar tu información existente desde la página de Configuración si no has habilitado Sincronización DIM antes. ¡Esto fue hecho posible por el apoyo de nuestros patrocinadores de OpenCollective!", "No": "Ahora no", "Title": "¿Habilitar Sincronización DIM?", "Yes": "Habilitar Sincronización" }, "AutoBackup": "Hemos hecho una copia de seguridad de tus datos en un archivo en tu carpeta de descargas llamado dim-data.json, sólo por si acaso.", "BackUpFirst": "DEBES hacer una copia de tu información primero, antes de borrarlo todo. Sólo por si acaso.", "BrowserMayClearData": "El navegador puede borrar esta información si te quedas sin espacio o no visitas DIM frecuentemente.", "DataIsLocal": "La información de las notas y las etiquetas es solo local", "DeleteAllData": "Borrar TODA la Información de los Servidores de Sincronización DIM", "DeleteAllDataConfirm": "¿Estás seguro que quieres borrar TODA tu información, para todas las cuentas, de Sincronización DIM? No puedes deshacer esto.", "Details": { "IndexedDBStorage": "El almacenamiento local sólo guardará información en este navegador. El limpiar tus datos de navegación borrará esta información." }, "DimApiFinePrint": "DIM guardará todas tus etiquetas, equipamientos y configuraciones a los servidores de DIM y se sincronizarán entre diferentes versiones de DIM.", "DimSyncDown": "La Sincronización DIM no está conectada debido a un problema de comunicación con el servidor.", "DimSyncEnabled": "Sincronización DIM habilitada", "DimSyncNotEnabled": "La Sincronización DIM no está activada, así que tus configuraciones, etiquetas, equipamientos y búsquedas sólo son almacenadas localmente y se perderán si limpias el almacenamiento del navegador. Activa Sincronización DIM en Configuración para hacer una copia de tu información automáticamente, o hacer una copia de tu información manualmente.", "EnableDimApi": "Habilitar sincronización DIM (recomendado)", "Export": "Descargar Información para Respaldo", "ExportError": "Error al descargar la copia de seguridad desde Sincronización DIM", "ExportErrorBody": "La Sincronización DIM puede estar caída, o puede que estés teniendo problemas con tu conexión. Descargaremos una copia de tus datos guardados localmente en su lugar.", "Import": "Importar Información de Respaldo", "ImportConfirmDimApi": "¿Estás seguro que quieres sobreescribir tus etiquetas actuales, equipamientos y configuraciones con esta versión? Reemplazará completamente todo lo que tenías.", "ImportExport": "Copia de Seguridad e Importación", "ImportFailed": "¡Falló la Importación! {{error}}", "ImportNoFile": "¡Ningún archivo ha sido seleccionado!", "ImportNotification": { "FailedBody": "No se pudieron importar datos. {{error}}", "FailedTitle": "Importación Fallida", "NoData": "No se encontraron equipamientos o etiquetas en la copia de seguridad", "SuccessBodyForced": "Configuraciones importadas, {{loadouts}} equipaciomientos y {{tags}} objetos etiquetas desde tu copia de seguridad a Sincronización DIM, reemplazando lo que ya estaba allí.", "SuccessBodyLocal": "Configuraciones, {{loadouts}} equipamientos y {{tags}} objetos etiquetados importados desde tu copia de seguridad al almacenamiento local, reemplazará lo que ya había ahí. No podemos garantizar que el almacenamiento local será perdido - considera habilitar Sincronización DIM.", "SuccessTitle": "Importación Exitosa" }, "ImportTooManyFiles": "Por favor, selecciona sólo un archivo a importar.", "ImportWrongFileType": "El archivo no es un archivo JSON. Podría no ser una copia de DIM.", "IndexedDBStorage": "Almacenamiento local del Navegador", "LearnMore": "Aprender más sobre Sincronización DIM", "MenuTitle": "Sincronización y Copias de Seguridad", "ProfileErrorBody": "Tuvimos un problema comunicándonos con Sincronización DIM. Tu última configuración, etiquetas, equipamientos y búsquedas podrían no mostrarse. Tu información todavía está en nuestros servidores, y cualquier actualización que hagas localmente será guardada cuando podamos reconectar. Seguiremos intentándolo mientras DIM esté abierto.", "ProfileErrorTitle": "Error de Descarga de Sincronización DIM", "RefreshDimSync": "Recargar información remota desde Sincronización DIM", "UpdateErrorBody": "Tuvimos un problema guardando tu información a Sincronización DIM. Seguiremos intentándolo mientras DIM esté abierto.", "UpdateErrorTitle": "Error de Guardado de Sincronización DIM", "UpdateInvalid": "Error al guardar los datos en DIM Sync", "UpdateInvalidBody": "Los datos enviados a la sincronización de DIM no eran válidos y no se guardarán.", "UpdateInvalidBodyLoadout": "El equipamiento \"{{name}}\" no es válido y no se guardará. Si lo importaste desde otro sitio, por favor hazles saber que están exportando equipos no válidos.", "UpdateQueueLength_one": "{{count}} cambio nuevo será guardado cuando podamos reconectar.", "UpdateQueueLength_other": "{{count}} cambios nuevos serán guardados cuando podamos reconectar.", "Usage": "DIM está usando {{usage, humanBytes}} de {{quota, humanBytes}} disponibles en este dispositivo. Esto incluye las bases de datos de objetos de Destiny descargados de Bungie.net." }, "StreamDeck": { "Authorize": "Conectar aplicación", "Enable": "Plugin de Stream Deck", "Error": { "Body": "Hubo un error enviando los datos al complemento de Stream Deck. Por favor contacta con el desarrollador del plugin. {{error}}", "Title": "Error del Complemento de Stream Deck" }, "FinePrint": "Activa la conexión con el complemento de DIM Stream Deck. Este complemento es un proyecto separado que no está escrito ni respaldado por el equipo de DIM.", "Install": "Instalar plugin", "MissingAuthorization": "Debes autorizar la aplicación de Stream Deck para conectar con DIM. Ve a ajustes y haz clic en \"Conectar aplicación\".", "Tooltip": { "Application": "Aplicación de Stream Deck", "AuthRequired": "Haz clic en este botón o ve a ajustes y haz clic en \"Conectar aplicación\".", "Error": "Tu complemento Stream Deck ya no tiene soporte. Por favor actualiza a la última versión. Este complemento requiere al menos:", "ErrorConnection": "si ya estás usando la última versión, comprueba si alguna extensión de navegador está bloqueando la conexión.", "ExtensionIssue": "Problema con las Extensiones", "Plugin": "Complemento", "Title": "Complemento DIM para Stream Deck", "Version": "Versión:" } }, "StripSockets": { "Action": "Desocupar Ranuras", "ArmorMods": "{{count}}x Mods de Armadura", "Button": "Desocupar {{numSockets}} Ranuras", "Cancel": "Cancelar", "Choose": "Elige Ranuras a desocupar", "DiscountedMods": "{{count}}x Mods con descuento", "Done": "Ranuras Desocupadas", "NoSockets": "No hay Ranuras para limpiar", "Ok": "Ok", "Ornaments": "{{count}}x Diseño", "Others": "{{count}}x Proyecciónes de Espectro", "Running": "Desocupando Ranuras", "Shaders": "{{count}}x Shader", "Subclass": "{{count}}x Opción de Subclase", "WeaponMods": "{{count}}x Mods de Armas" }, "Tags": { "Archive": "Archivar", "ClearTag": "Limpiar Etiqueta", "Favorite": "Favorito", "Infuse": "Infundir", "Junk": "Basura", "Keep": "Mantener", "LockAll": "Bloquear objetos", "TagItem": "Etiquetar", "UnlockAll": "Desbloquear objetos" }, "Triage": { "AccountsForArtifice": "Esto prueba si una pieza de armadura Artificio pudiera ser mejor, si un modificador de estadística +3 fuera usado.", "BetterArmor": "Estrictamente Mejor Armadura", "BetterArtificeArmor": "Mejor Armadura Artificio", "BetterStatArmor": "Armadura de Mejores Estadísticas", "BetterStatArtificeArmor": "Armadura Artificiosa de Mejores Estadísticas", "BetterWorseArmor": "Mejor/Peor Armadura", "BetterWorseIncludes": "Identifica piezas de armadura con:", "HighStats": "Estadísticas Altas", "InLoadouts": "En Equipamientos", "OwnedCount": "# Poseídos", "PerkBetterArmorDesc": "Las mismas, o más, perks intrínsicas o espacios especiales de mods.", "PerkWorseArmorDesc": "La misma perk intrínsica, o ninguna.", "SimilarItems": "Objetos similares", "StatBetterArmorDesc": "Todas las estadísticas al menos tan altas, y por lo menos una estadística mejor.", "StatNotPerkArmorDesc": "Esto solo prueba estadísticas. Una pieza más baja todavía puede tener espacios de mods especiales o perks intrínsicas.", "StatWorseArmorDesc": "Sin mejores estadísticas, y por lo menos una estadística peor.", "ThisItem": "Este objeto", "WorseArmor": "Estrictamente Peor Armadura", "WorseArtificeArmor": "Peor Armadura No-Artificio", "WorseStatArmor": "Armadura de Peores Estadísticas", "WorseStatArtificeArmor": "Armadura No-Artificiosa de Peores Estadísticas", "YourBestItem": "Tu mejor objeto" }, "Triumphs": { "GildingTriumph": "Triunfo Dorado", "HideCompleted": "Ocultar triunfos completados", "RevealRedacted": "Revelar triunfos clasificados", "SortRecords": "Ordenar triunfos por compleción" }, "Vendors": { "Collections": "Colecciones", "Engram": "Rango", "FilterToUnacquired": "Mostrar sólo objetos no coleccionados", "HideSilverItems": "Ocultar objetos de Compra con Plata", "NoItems": "Este Vendedor no está ofreciendo ningún artículo en este momento.", "RefreshTime": "El inventario se actualiza en:", "Vendors": "Vendedores" }, "Views": { "About": { "APIHistory": "Ver el historial de todas las acciones hechas por DIM (y otras apps de Destiny)", "BungieCopyright": "Todas las imágenes y el contenido son propiedad de Bungie.", "CommunityInsight": "Información de la Comunidad para los Perks y las Estadísticas de los Personajes cortesía de {{clarityLink}}. Si observas inexactitudes o tienes preguntas, únete al {{clarityDiscordLink}}.", "Discord": "Discord", "DiscordHelp": "Haz preguntas, da tu opinión y recibe soporte en nuestros canales de Discord.", "FAQ": "Preguntas Frecuentes", "FAQAccess": "¿Cómo accede DIM a mis datos de Destiny?", "FAQAccessAnswer": "Utilizamos la autenticación de la app de Bungie para permitir el acceso a que DIM vea y mueva tus objetos. DIM nunca ve tu usuario o contraseña. Esta es la misma forma que funciona la App Acompañante.", "FAQKeyboard": "¿Soporta DIM atajos de teclado?", "FAQKeyboardAnswer": "¡Sí! Presiona la tecla \"?\" para ver una lista de los atajos disponibles.", "FAQLogout": "¿Cómo puedo cerrar sesión en DIM?", "FAQLogoutAnswer": "Abre el menú desde el icono superior izquierdo y elije \"Cerrar Sesión\"", "FAQLostItem": "¡Perdí mi objeto usando tu herramienta!", "FAQLostItemAnswer": "Bungie no permite borrar elementos a las aplicaciones (¡ni siquiera a su propia aplicación!). Lo más seguro es que la transferencia haya fallado y haya dejado tu objeto en el deposito o en otro personaje. Si incluso así no aparece, actualiza la pagina presionando F5. Asegúrate de revisar {{link}} o en el juego para verificar que tu objeto aparece de nuevo. Estamos seguros de que sigue ahí.", "FAQMobile": "¿Tendrá DIM soporte móvil? ¿Habrá una app?", "FAQMobileAnswer": "La página web de DIM puede ser cargada en teléfonos y tabletas hoy, y puedes añadirla a tu pantalla de inicio para una experiencia similar a la aplicación.", "GitHub": "GitHub", "GitHubHelp": "Si estas interesado en contribuir al proyecto, visita la página del proyecto en {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM es una app gratis de código abierto, construída por desarrolladores de la comunidad bajo los mismos servicios usados por Bungie.net y la Aplicación del Acompañante de Destiny.", "Schedule": { "beta": "Esta versión beta de DIM es actualizada cada vez que cambiamos su codificación - obtiene las últimas características y arreglos, ¡pero también los últimos bugs!", "release": "Esta versión de DIM se actualiza una vez a la semana, aproximadamente a la medianoche de los Sábados, hora del pacífico de los Estados Unidos." }, "Translation": "¡Únete al Equipo Traductor!", "TranslationText": "Utilizamos {{link}} para facilitar la traducción. Si quieres mejorar alguna de las traducciones de DIM, únete al equipo.", "Version": "Versión {{version}} ({{flavor}}), construida en {{date}}", "Wiki": "Guía de Usuario de DIM", "WikiHelp": "Aprende cómo usar las características de DIM." }, "Login": { "Auth": "Autorizar con Bungie.net", "EnableDimSyncWarning": "Anteriormente habías desactivado la sincronización DIM y sólo se estaba utilizando el almacenamiento de datos local. Habilitar sincronización DIM reemplazará cualquier dato local con los datos de DIM Sync. Deberías realizar una copia de seguridad de tus datos antes de activar la sincronización de DIM. Puedes restaurar desde esa copia de seguridad en Configuración.", "Explanation": "Permite a DIM ver y modificar tus personajes de Destiny, depósito y progreso.", "LearnMore": "Más información sobre cuentas e inicia sesión", "NewAccount": "Iniciar sesión con una cuenta diferente de Bungie.net", "Permission": "Necesitamos tu permiso..." }, "Support": { "BackersDetail": "Apóyanos con una donación única o mensual, y ayuda a que podamos continuar con nuestro desarrollo activo.", "FreeToDownload": "DIM es un producto libre de descarga y uso. El código fuente para DIM es de código abierto y libre para que cualquiera pueda mejorarlo. Nunca verás un anuncio en DIM. Ese es nuestro compromiso.", "OpenCollective": "Estamos usando {{link}} como servicio para compensar a los desarrolladores por su dedicación y tiempo dedicado a este proyecto.", "Store": "Tenemos mercancía con nuestro logo y otros diseños en venta en {{link}}", "Support": "Apoya a DIM" } }, "WishListRoll": { "BestRatedTip_one": "Esta perk coincide exactamente con una tirada de arma en tu lista de deseos.", "BestRatedTip_other": "Estas perks coinciden exactamente con una tirada de arma de tu lista de deseos.", "Clear": "Limpiar Lista de Deseos", "CopiedLine": "Lista de deseos de rolls copiada al portapapeles", "CopyLine": "Copiar los perks seleccionados como lista de deseos de rolls", "DupeRolls": " (+{{num, number}} duplicados ignorados)", "ExternalSource": "Añadir otra lista de deseos", "ExternalSourcePlaceholder": "Pega el enlace de la lista de deseos aquí", "Header": "Lista de Deseos", "Import": "Cargar Tiradas de la Lista de Deseos", "ImportError": "Error al cargar la lista de deseos de \"{{url}}\": {{error}}", "ImportFailed": "Ninguna de tus listas de deseos contenía algún roll válido.", "ImportNoFile": "No hay archivo seleccionado.", "InvalidExternalSource": "Por favor, introduce una URL válida para tu lista externa de deseos. La URL debe empezar con algunos de lo siguientes:", "JustAnotherTeam": "Solo Otro Equipo", "LastUpdated": "Última actualización: {{lastUpdatedDate}} a las {{lastUpdatedTime}}", "Num": "{{num, number}} tiradas en tu lista de deseos", "NumRolls": "{{num, number}} tiradas", "Refresh": "Actualizar Lista de Deseos", "SourceAlreadyAdded": "Lista de Deseos ya añadida", "UpdateExternalSource": "Añadir Lista de Deseos", "Voltron": "voltron (por defecto)", "WishListNotes": "Notas de la Lista de Deseos:", "WorstRatedTip_one": "Esta ventaja coincide exactamente con una tirada de arma en tu lista de basura.", "WorstRatedTip_other": "Estas perks coinciden exactamente con una tirada de arma en tu lista de basura." }, "no-space": "sin-espacio", "wrong-level": "nivel-incorrecto" } ================================================ FILE: src/locale/fr.json ================================================ { "AWA": { "ConfirmDescription": "Veuillez utiliser l'application Compagnon de Destiny 2 pour donner à DIM l'autorisation de modifier vos objets.", "ConfirmTitle": "Confirmer l'action", "Error": "Erreur lors du changement des mods ou des attributs", "ErrorMessage": "Nous n'avons pas pu équiper {{plug}} dans {{item}}.\n\n{{error}}", "FailedToken": "Impossible d'obtenir la permission de modifier l'objet", "IrreversiblePlugging": "Vous ne possédez pas {{plug}}, donc nous ne le remplacerons pas." }, "Accounts": { "Choose": "Profils pour {{bungieName}}", "ErrorLoadInventory": "Impossible de charger vos personnages et inventaire Destiny {{version}}", "ErrorLoadManifest": "Impossible de charger la base de données d'information de Destiny depuis Bungie", "ErrorLoading": "Impossible de charger les comptes Destiny depuis Bungie.net", "MissingAccountWarning": "Si vous ne voyez pas votre compte ici, vous ne vous êtes peut-être pas connecté au bon compte Bungie.net, ou Bungie.net est peut-être hors-ligne pour maintenance.", "MissingDescription": "Le compte que vous essayez de voir n'est pas un compte associé à votre profil Bungie.net. Sélectionnez un de vos comptes ci-dessous.", "MissingTitle": "Compte Introuvable", "NoCharacters": "Vous n'avez aucun personnage Destiny associé à ce compte Bungie.net. Essayez de vous connecter sur un compte différent.", "NoCharactersTitle": "Aucun personnage trouvé", "SwitchAccounts": "Vous pouvez changer de compte plus tard à partir du menu dans la barre de navigation.", "Title": "Comptes" }, "Activities": { "Activities": "Activités", "Hard": "Difficile", "Nightfall": "Assaut « Nuit noire »", "Normal": "Normal", "WeeklyHeroic": "Assaut « Épique » de la Semaine" }, "Armory": { "AlternateItems": "Versions alternatives", "Armory": "Armurerie", "DifferentSeason": "Réédition d'une autre saison", "NoNotes": "Aucune notes", "OpenInArmory": "voir dans l'armurerie", "Season": "Saison {{season}}, Année {{year}}", "TrashlistedRolls_one": "Arme indésirable", "TrashlistedRolls_other": "{{count, number}} Armes indésirables", "Unknown": "Object inconnu", "UnknownPerkHash": "The perk hash {{hash}} ({{perkName}}) does not appear on this item, so this wish list roll is invalid. Please contact the wish list author to correct this. Note that wish lists should always specify the non-enhanced version of perks.", "WishlistedRolls_one": "Arme souhaitée", "WishlistedRolls_other": "{{count, number}} Armes souhaitées", "YourItems": "Vos objets" }, "Browsercheck": { "Samsung": "Samsung Internet peut rendre les sites trop sombre quand le mode sombre est activé. Activez Paramètres > Labs > Utiliser le mode sombre du site web ou utilisez un autre navigateur.", "Steam": "Le navigateur de l'overlay Steam est très vieux et certaines des fonctionnalitées de DIM peuvent ne pas fonctionner. Nous ne pouvons pas fournir de support pour ce navigateur.", "Unsupported": "L'équipe DIM ne prend pas en charge ce navigateur. Certaines ou toutes les fonctionnalités de DIM peuvent ne pas fonctionner." }, "Bucket": { "Armor": "Armure", "Class": "Doctrine", "General": "Général", "Ghost": "Spectre", "Inventory": "Inventaire", "Postmaster": "Commis des postes", "Progress": "Avancement", "Reputation": "Estime", "Unknown": "Inconnu", "Vault": "Coffre", "Weapons": "Armes" }, "BulkNote": { "Append": "Ajouter aux notes / ajouter des #hashtags", "Confirm": "Modifier les notes", "Remove": "Retirer des notes / retirer des #hashtags", "Replace": "Remplacer les notes", "Title_one": "Changer les notes de 1 objet", "Title_other": "Changer les notes de {{count}} objets" }, "BungieAlert": { "Title": "Un message de Bungie:" }, "BungieService": { "AppNotPermitted": "DIM n'a pas la permission d'effectuer cette action.", "DestinyCannotPerformActionAtThisLocation": "Vous ne pouvez pas équiper des objets ou modifier des mods pendant une activité. Essayez d'aller en orbite ou dans un espace social. Ceci est une limitation de l'API Bungie.net, pas DIM.", "DestinyItemUnequippable": "Vous ne pouvez pas équiper cet objet. Si la dernière activité de ce personnage a verrouillé son équipement, essayez de vous reconnecter au personnage.", "DestinyLegacyPlatform": "Les services Bungie ont actuellement un bug empêchant DIM de charger les informations pour votre compte Destiny 2 si vous avez joué à Destiny 1 sur une console d'ancienne génération. Bungie corrigera bientôt ce bug, mais en attendant vous devez jouer à Destiny 1 sur une console de génération actuelle pour pouvoir accéder à vos informations.", "DevVersion": "Est-ce une version de développement de DIM ? Vous devez enregistrer votre extension chrome avec Bungie.net.", "Difficulties": "Bungie.net connaît actuellement des difficultés.", "ErrorTitle": "Erreur Bungie.net", "ItemUniquenessExplanation": "Un personnage ne peut avoir qu'un seul \"{{name}}\" sur lui.", "Maintenance": "Les serveurs Bungie.net sont indisponibles pour cause de maintenance.", "MissingInventory": "Bungie.net n'a pas renvoyé votre inventaire, vos paramètres de vie privée peuvent l'en empêcher. Essayez de vous déconnecter puis de vous reconnecter.", "NetworkError": "Erreur Réseau - {{status}} {{statusText}}", "NoAccount": "Aucun compte Destiny n'a été trouvé. Avez-vous sélectionné la bonne plateforme?", "NoAccountForPlatform": "Nous n'avons pas trouvé de compte Destiny pour vous sur {{platform}}.", "NotConnected": "Vous n'êtes peut-être pas connecté à internet.", "NotConnectedOrBlocked": "Vous n'êtes peut-être pas connecté à internet, ou un bloquer de pub est peut-être en train de bloquer Bungie.net.", "NotLoggedIn": "Veuillez autoriser DIM pour pouvoir utiliser cette application.", "Slow": "Bungie.net actuellement lent", "SlowDetails": "Bungie.net prend longtemps pour obtenir la donnée demandée. Cela peut arriver lorsque beaucoup d'utilisateurs sont dans le jeu en même temps, ou s'il y a des problèmes avec Bungie.net. Il est aussi possible que votre connection internet ait des problèmes. Nous allons continuer d'essayer d'obtenir la donnée.", "SlowResponse": "Bungie.net a été trop lent pour répondre.", "Throttled": "Bungie.net limite le nombre de requêtes que DIM peut faire.", "Twitter": "Tenez-vous informé sur:", "UnknownError": "Message de Bungie.net: {{message}}", "VendorNotFound": "Les données du vendeur sont indisponibles." }, "Compare": { "Archetype": "Archétype", "AssumeMasterworked": "Considérer pièce maîtresse", "AssumeMasterworkedDescription": "Stats if fully Masterworked, without current Mods", "BaseStatsDescription": "Base stats, without Masterwork or Mods", "Button": "Comparer", "ButtonHelp": "Comparer les objets", "CompareBaseStats": "Afficher les statistiques de base", "CurrentStats": "Current Stats", "CurrentStatsDescription": "Current stats, including Mods and Masterwork level", "Error": { "Invalid": "Il n'y a aucun objet valide à comparer.", "Unmatched": "Cet objet ne correspond pas au type d'objet comparé." }, "InitialItem": "C'est l'objet depuis lequel l'outil de comparaison a été lancé", "IsVendorItem": "Cet objet n'est pas dans votre inventaire, mais {{vendorName}} le vend.", "NoModArmor": "Avant les mods" }, "Cooldown": { "Grenade": "Délai de la Grenade: {{cooldown}}", "Melee": "Délai de la Mêlée: {{cooldown}}", "Super": "Délai du Super: {{cooldown}}" }, "Countdown": { "Days_compact_one": "{{count}}j", "Days_compact_other": "{{count}}j", "Days_one": "1 Jour", "Days_other": "{{count}} Jours" }, "Csv": { "EmptyFile": "Il n'y avait aucune lignes dans le fichier.", "ImportConfirm": "Êtes-vous sure de vouloir importer des étiquettes/notes depuis un CSV ? Cela écrasera les étiquettes/notes de tout les objets contenus dans votre tableur.", "ImportFailed": "Erreur lors de l'importation des étiquettes/notes depuis un CSV: {{error}}", "ImportSuccess_one": "Étiquettes/notes chargés pour un objet.", "ImportSuccess_other": "Étiquettes/notes chargés pour {{count}} objets.", "ImportWrongFileType": "Le fichier n'est pas un fichier CSV.", "WrongFields": "Le CSV doit avoir les colonnes \"Id\", \"Notes\", \"Tag\", et \"Hash\"." }, "Dialog": { "Cancel": "Annuler", "OK": "OK" }, "EnergyMeter": { "Energy": "Énergie", "Unused": "Libre", "UpgradeNeeded": "La capacité en énergie de cet objet est de {{energyCapacity}}. Pour placer les mods sélectionnés, sa capacité en énergie doit être de {{energyUsed}}.", "Used": "Utilisé" }, "ErrorBoundary": { "Title": "Une erreur s'est produite" }, "ErrorPanel": { "BrowserTooOld": "Votre navigateur est trop ancien pour utiliser DIM. Veuillez mettre à jour votre navigateur vers la dernière version.", "BrowserTooOldTitle": "Navigateur incompatible", "Description": "Essayez de charger votre inventaire dans l'application Compagnon de Destiny 2 pour voir si Bungie.net est hors-ligne.", "ReadTheGuide": "Lisez notre manuel d'utilisation (lien dans le menu) pour les étapes de dépannage.", "SystemDown": "Ceci affecte toutes les applications Destiny et l'équipe DIM ne peut pas le réparer ou le contourner.", "Troubleshooting": "Guide de Dépannage" }, "FarmingMode": { "D2Desc_female_one": "DIM empêche les objets d'aller au commis des postes en s'assurant qu'il y aura toujours un espace vide par type d'objet sur {{store}}.", "D2Desc_female_other": "DIM empêche les objets d'aller au commis des postes en s'assurant qu'il y aura toujours {{count}} espaces vides par type d'objet sur {{store}}.", "D2Desc_male_one": "DIM empêche les objets d'aller au commis des postes en s'assurant qu'il y aura toujours un espace vide par type d'objet sur {{store}}.", "D2Desc_male_other": "DIM empêche les objets d'aller au commis des postes en s'assurant qu'il y aura toujours {{count}} espaces vides par type d'objet sur {{store}}.", "D2Desc_one": "DIM empêche les objets d'aller au commis des postes en s'assurant qu'il y aura toujours un espace vide par type d'objet sur {{store}}.", "D2Desc_other": "DIM empêche les objets d'aller au commis des postes en s'assurant qu'il y aura toujours {{count}} espaces vides par type d'objet sur {{store}}.", "Desc_female_one": "DIM transfère des engrammes et objets de lumen du {{store}} au coffre et laisse un espace disponible pour chaque type d'objet afin que rien ne soit envoyé au commis des postes.", "Desc_female_other": "DIM transfère des engrammes et objets de lumen du {{store}} au coffre et laisse {{count}} espaces disponibles pour chaque type d'objet afin que rien ne soit envoyé au commis des postes.", "Desc_male_one": "DIM transfère des engrammes et objets de lumen du {{store}} au coffre et laisse un espace disponible pour chaque type d'objet afin que rien ne soit envoyé au commis des postes.", "Desc_male_other": "DIM transfère des engrammes et objets de lumen du {{store}} au coffre et laisse {{count}} espaces disponibles pour chaque type d'objet afin que rien ne soit envoyé au commis des postes.", "Desc_one": "DIM transfère des engrammes et objets de lumen du {{store}} au coffre et laisse un espace disponible pour chaque type d'objet afin que rien ne soit envoyé au commis des postes.", "Desc_other": "DIM transfère des engrammes et objets de lumen du {{store}} au coffre et laisse {{count}} espaces disponibles pour chaque type d'objet afin que rien ne soit envoyé au commis des postes.", "FarmingMode": "Mode farming", "FarmingModeNote": "(garde de la place)", "MakeRoom": { "Desc": "DIM est entrain de transférer uniquement les engrammes et objets de lumen du {{store}} au coffre ou autres personnages afin d'éviter que rien ne soit envoyé au commis des postes.", "Desc_female": "DIM est entrain de transférer uniquement les engrammes et objets de lumen du {{store}} au coffre ou autres personnages afin d'éviter que rien ne soit envoyé au commis des postes.", "Desc_male": "DIM est entrain de transférer uniquement les engrammes et objets de lumen du {{store}} au coffre ou autres personnages afin d'éviter que rien ne soit envoyé au commis des postes.", "MakeRoom": "Faire de la place pour collecter des objets en déplaçant de l'équipement", "Tooltip": "Si activé, DIM déplacera les armes et armures afin de faire de la place dans le coffre pour les engrammes." }, "OutOfRoom": "Vous n'avez plus d'espace libre pour déplacer des objets en dehors de {{character}}. Il serait temps de se débarrasser des indésirables !", "OutOfRoomTitle": "Plus de Place", "Stop": "Stop", "Vault": "Il déplacera des objets vers le coffre pour faire de la place." }, "FashionDrawer": { "Accept": "Sauvegarder le style", "CannotFitOrnament": "Cet objet n'a pas d'emplacement d'ornement, ou vous n'avez pas d'ornement pour lui.", "CannotFitShader": "Cet objet n'accepte pas un shader", "ClearOrnaments": "Retirer les ornements", "ClearOrnamentsTitle": "Réinitialiser tous les ornements à \"aucune préférence\"", "ClearShaders": "Retirer les revêtements", "ClearShadersTitle": "Réinitialiser tous les revêtements à \"aucune préférence\"", "NoPreference": "Aucune préférence - cet emplacement restera inchangé", "Reset": "Retirer", "Sync": "Sync", "SyncOrnaments": "Synchroniser les ornements", "SyncOrnamentsTitle": "Utiliser les ornements du même ensemble sur tous les objets, s'ils sont déverrouillés", "SyncShaders": "Synchroniser les revêtements", "SyncShadersTitle": "Utiliser le même revêtement sur tous les objets", "Title": "Choisir les revêtements et ornements", "UseEquipped": "Utiliser équipés" }, "FileUpload": { "Instructions": "Cliquer ou glisser des fichiers" }, "Filter": { "Adept": "\\(expert\\)", "AmmoType": "Affiche les objets en fonction de leur type de munition.", "Armor": "Affiche les objets qui sont des armures.", "Armor3": "Affiche les objets qui utilisent le système de stats d'armure 3.0 introduis dans Les Confins du Destin.", "ArmorCategory": "Affiche les armures selon leur catégorie.", "ArmorIntrinsic": "Affiche les armures qui ont un attribut intrinsèque, comme Armure d'Artifice par exemple.", "Artifice": "Shows Artifice armor.", "Ascended": "Affiche les objets qui ont eu une ascension.", "Breaker": "Filtrer par type d'anti-champion ou par type de champion. breaker:intrinsic affiche les objets qui ont un anti-champion intrinsèque.", "BulkClear_one": "Suppression de l'étiquette de 1 objet.", "BulkClear_other": "Suppression des étiquettes de {{count}} objets.", "BulkRevert_one": "Rajout de l'étiquette sur 1 objet.", "BulkRevert_other": "Rajout des étiquettes sur {{count}} objets.", "BulkTag_one": "Élément sélectionné étiqueté {{tag}}.", "BulkTag_other": "{{count}} Éléments sélectionnés étiquetés {{tag}}.", "Catalyst": "Affiche les catalyseurs selon leur statut. catalyst:complete affiche les catalyseurs que vous avez complétés et appliqués, catalyst:incomplete affiche les catalyseurs que vous avez débloqués mais que vous n'avez soi pas complété soi pas appliqué, et catalyst:missing affiche les objets qui peuvent avoir un catalyseur que vous n'avez pas encore trouvé.", "Class": "Affiche les objets en fonction de leur affinité de classe.", "Combine": "Les filtres peuvent être combinés ou groupés avec des parenthèses, \"or\" et \"and\" pour affiner votre recherche, par exemple \"{{example}}\".", "ContributePower": "Affiche les objets qui ont de la puissance et qui peuvent contribuer à votre niveau de puissance.", "Cosmetic": "Affiche les objets de style ou cosmétiques.", "Craftable": "Affiche les objets pouvant être façonné.", "CraftedDupe": "Affiche les armes en double quand au moins un des doublons est façonné.", "Curated": "Affiche les objets qui ont des attributs prédéterminés.", "CurrentClass": "Affiche les objets qui peuvent être équipés sur le personnage actuellement connecté.", "CustomStatLower": "Affiche les armures dont les statistiques sont strictement inférieures à celles d'une autre armure du même type, en ne prenant en compte que les statistiques présentes dans un des total de statistiques personnalisé de la classe.", "DamageType": "Affiche les objets selon leur type de dégât.", "Deepsight": "Montre les armes avec une résonance de souvenance, dont le modèle peut être extrait, ou qui peuvent avoir une résonance de souvenance activée à l'aide d'un harmonisateur de souvenance.", "Deprecated": "Ce filtre n'est plus pris en charge.", "Description": "Description", "DescriptionFilter": "Montre les objets dont la description correspond partiellement au filtre de recherche. Recherchez des phrases en utilisant des guillemets.", "DisabledModSlot": "Affiche les objets ayant un mod désactivé.", "Dupe": "Affiche les doublons, y compris les rééditions", "DupeArchetype": "Groups armor with the same stat Archetype.", "DupeCount": "Objets qui ont le nombre spécifié de doublons.", "DupeLower": "Doublons, rééditions incluses, qui ne sont pas le doublon avec la puissance la plus élevé. Un seul doublon est choisis comme étant celui avec la puissance la plus élevé, les autres sont considérés comme plus bas.", "DupePerks": "Affiche les objets dont les attributs sont soit les mêmes qu'un, ou un sous-ensemble d'un, autre objet du même type.", "DupeSetBonus": "Groups armor with the same set bonus.", "DupeStats": "Shows armor with identical base stats, and matching stat adjustment mods like Artifice or Tuners.", "DupeTertiary": "Groups armor with the same tertiary stat.", "DupeTraits": "Weapons whose traits are either a duplicate of, or a subset of, another weapon of the same type.", "DupeTunedStat": "Groups armor with the same Tuned stat.", "DupeUntunedStats": "Groups armor with identical base stats, ignoring stat adjustment mods.", "DupeZeroStats": "Groups armor with the same 3 non-zero base stats.", "Energy": "Affiche les objets qui utilisent le système de stats d'armure 2.0 introduis dans Bastion des Ombres.", "EnergyCapacity": "Affiche les objets selon leur capacité énergétique actuelle.", "Engrams": "Affiche les engrammes.", "Enhanceable": "Affiche les armes pouvant être améliorées.", "Enhanced": "Affiche les armes en fonction de leur niveau d'amélioration.", "EnhancedPerk": "Affiche les armes qui ont le nombre spécifié d'attributs améliorés.", "EnhancementReady": "Affiche les armes qui ont atteint le niveau requis pour l'amélioration de ses attributs.", "Equipment": "Objets qui peuvent être équipés.", "Equipped": "Objets qui sont actuellement équipés sur un personnage.", "Event": "Affiche les objets en fonction de l’événement de Destiny 2 pendant lequel ils sont apparu.", "ExtraPerk": "Affiche les armes légendaires obtenues avec des attributs aléatoires ayant un attribut sélectionnable supplémentaire.", "Featured": "Objets considérés comme \"Nouveaux équipements\" ou \"À la une\" dans la saison actuelle.", "Filter": "Filtre", "FilterWith": "Filtrer avec :", "Focusable": "Affiche les objets qui peuvent être concentrés auprès d'un vendeur", "Foundry": "Affiche les objets en fonction de la fonderie qui les a créés.", "Glimmer": "Affiche les objets consommables qui ont un rapport avec l'obtention de lumen.", "Harrowed": "\\(Tourmenté\\)", "HasNotes": "Afficher les objets ayant des notes.", "HasOrnament": "Affiche les objets ayant un ornement appliqué.", "HasShader": "Affiche les objets qui ont un revêtement appliqué.", "Holofoil": "Shows holofoil weapons.", "InDimLoadout": "is:indimloadout affiche les objets présents dans un équipement DIM.", "InInGameLoadout": "is:ingameloadout affiche les objets qui sont inclus dans l'un de vos équipement en jeu.", "InInventory": "Affiche les objets dont vous avez au moins une copie dans l'un de vos inventaires. Généralement utile dans les écrans Marchands et Archives.", "InLoadout": "is:inloadout affiche les objets inclus dans un équipement. Rechercher avec inloadout: affiche les objets inclus dans les équipements au titre correspondant. Lorsque utilisé avec un hashtag, inloadout: affiche les objets dont les équipements ont le hashtag dans leur titre ou notes. Lorsque utilisé avec un chiffre, cela affiche les objets qui sont dans ce nombre d'équipements.", "Infusable": "Affiche les objets qui peuvent être infusés.", "InfusionFodder": "Affiche les objets qui pourraient être infusés dans une version plus faible du même objet en utilisant uniquement des lumens.", "IsAdept": "Affiche les armes compatible avec les mods Expert.", "IsCrafted": "Affiche les armes qui ont été façonnées.", "ItemHash": "Affiche les objets avec le hash d'objet spécifié. Pour les utilisateurs avancés.", "ItemId": "Affiche les objets avec l'ID d'objet d'inventaire spécifié. Pour les utilisateurs avancés.", "Leveling": { "Complete": "{{term}} - Affiche les objets qui sont complétés - toutes les améliorations débloqués.", "Incomplete": "{{term}} - Affiche les objets qui ne sont pas complétés - il manque au moins une amélioration à débloquer.", "NeedsXP": "{{term}} - Affiche les objets qui peuvent toujours recevoir de l'xp.", "Upgraded": "{{term}} - Affiche les objets qui ont accès d'xp pour débloquer toutes leurs bulles, mais pas toutes les bulles ont été débloquées.", "XPComplete": "{{term}} - Affiche les objets qui ne peuvent plus recevoir d'xp (que leurs améliorations aient été débloqués ou non)." }, "Location": "Affiche les objets en fonction de leur position dans l'app. gauche/milieu/droite sont les emplacements visuels des personnages, et tandis que le personnage de gauche fonctionnera toujours, les deux autres sont basés sur le nombre de personnages que vous avez. actuellement c'est votre dernier personnage sur lequel vous vous êtes connecté (marqué avec un triangle jaune).", "LockAllFailed": "Verouillage des objets échoué", "LockAllSuccess": "{{num}} objets verouillés", "Locked": "Affiche les objets selon leur verrouillage.", "Masterwork": "Affiche les objets en fonction du statut ou du niveau de leur pièce maîtresse.", "MasterworkKills": "Affiche les objets en fonction du nombre de frags sur le compte-frags de la pièce maîtresse.", "MaxPower": "Affiche les objets avec la puissance la plus élevée pour chaque emplacement.", "MaxPowerLoadout": "Affiche les objets qui maximiseraient votre niveau de puissance pour chaque classes.", "Memento": "Affiche les armes qui ont un emplacement de memento.", "ModSlot": "Affiche les armures avec un type d'emplacement de mod spécifique.", "Mods": { "Y3": "Affiche les objets avec au moins un mod appliqué." }, "Name": "Montre les objets dont le nom correspond totalement (exactname:) ou partiellement (name:) au filtre de recherche. Recherchez des phrases en utilisant des guillemets.", "NamedStat": "Affiche les armures qui ont des points dans la statistique sélectionnée.", "Negate": "Pour inverser une recherche, préfixer le terme de recherche par un signe moins ou le mot \"not\", par exemple \"{{notexample}}\" ou \"{{notexample2}}\".", "NewItems": "Affiche les nouveaux objets.", "Notes": "Cherche les objets auxquels vous avez ajouté des notes personnalisées.", "OriginTrait": "Affiche les armes qui ont un attribut d'origine.", "Ornament": "Affiche les objets avec des ornements et filtre leur statut.", "PartialMatch": "Montre les objets dont le nom, la description, n'importe quel attribut, ou n'importe quel mod correspond partiellement au filtre de recherche. Recherchez des phrases en utilisant des guillemets.", "PatternUnlocked": "Affiche les objets qui ont un modèle de façonnage débloqué, même si l'objet lui-même n'a pas été façonné.", "Perk": "Affiche les objets dont la description ou le nom d'au moins un de ses mods et attributs contient une partie du texte filtré. Rechercher des phrases entières en utilisant des guillemets.", "PerkName": "Affiche les objets avec un attribut ou un nom correspondant totalement (exactperk:) ou partiellement (perkname:) au filtre. Recherchez des phrases entières en utilisant des guillemets.", "PinnacleReward": "Affiche les poursuites qui produisent une récompense de prestige.", "Postmaster": "Objets actuellement au commis des postes.", "PowerKeywords": "Utilisez les mots-clés pinnaclecap ou softcap à la place de nombres pour utiliser les limites de puissance de la saison actuelle.", "PowerLevel": "Affiche les objets en fonction de leur niveau de puissance. $t(Filter.PowerKeywords)", "PowerfulReward": "Affiche les poursuites qui produisent une récompense puissante.", "PrismaticDamageType": "Affiche les objets selon qu'ils fassent des dégâts de lumière ou de tenèbres. Les dégâts de lumières sont cryo-éléctrique, solaire et abyssal. Les dégâts de ténèbres sont stase et filobscur.", "Quality": "Affiche les objets selon leur pourcentage de statistiques total. '{{percentage}}' est un alias pour '{{quality}}'.", "RandomRoll": "Affiche les objets qui s'obtiennent avec des compétences aléatoires.", "RarityTier": "Affiche les objets selon leur rareté.", "Reforgeable": "Affiche les objets qui peuvent être reforgés à l'armurier.", "Release": "Shows items available from a specific release or event.", "RequiredLevel": "Affiche les objets selon leur niveau requis.", "RetiredPerk": "Affiche les armes avec des attributs qui ne sont plus obtenable.", "SearchPrompt": "Rechercher les commandes de filtre disponibles", "Season": "Affiche les objets en fonction de la saison de Destiny 2 pendant lequel ils sont apparu.", "StackFull": "Affiche les objets qui sont dans une pile pleine (Prismes d’amélioration, pièces étranges, matériaux des armuriers etc)", "StackLevel": "Affiche les objets en fonction de la quantité de ces mêmes objets.", "Stackable": "Affiche les objets qui peuvent être empilés (cartouches de munition, pièces étranges, etc)", "StatLower": "Affiche les armures dont les statistiques sont strictement plus basses que celles d'une autre armure du même type.", "Stats": "Affiche les objets en fonction de la valeur d'une statistique spécifique. $t(Filter.StatsExtras)", "StatsBase": "Filtre les armures en fonction de la valeur de base de leurs statistiques, sans inclure les mods attachés ou les pièces maîtresses. $t(Filter.StatsExtras)", "StatsExtras": "Permet l'addition de statistique en connectant plusieurs noms de statistiques avec le symbole + ou &. Il y a aussi des mots-clés spéciaux highest, secondhighest, thirdhighest, etc. qui cherchent les statistiques en fonction de leur rang au sein des statistiques d'un objet. Chaque statistique personnalisé a son propre terme de recherche, affiché dans les paramètres de statistiques personnalisées.", "StatsLoadout": "Recherche un ensemble d'objets à équiper pour obtenir la valeur totale maximale d'une statistique spécifique.", "StatsMax": "Recherche des armures avec le nombre le plus élevé pour une stat spécifique. Inclut tous les objets avec le nombre le plus élevé.", "StatsOrdinal": "Finds armor 3.0 with the specified stat focusing.", "Tags": { "Tag": "Affiche les objets portant une certaine étiquette.", "Tagged": "Affiche les objets portant une étiquette." }, "Tier": "Affiche les objets selon leur palier de 0 à 5.", "Timelost": "\\(temps perdu\\)", "Tracked": "Affiche les quêtes/contrats selon si elles sont suivis ou non.", "Transferable": "Objets qui peuvent êtres déplacés entre les personnages.", "Trashlist": "Affiche les objets présents dans la liste d'indésirables de votre liste de souhaits.", "TunedStat": "Shows items with tuning mods for the specified stat.", "Unascended": "Affiche les objets qui ont un attribut d’ascensions qui n'ont pas eu une ascension.", "Undo": "Annuler", "UnlockAllFailed": "Déverouillage des objets échoué", "UnlockAllSuccess": "{{num}} objets déverrouillés", "Vendor": "L'objet est disponible auprès d'un marchand spécifique.", "VendorItem": "L'objet provient d'un vendeur, pas de votre inventaire. Utilse pour exclure les objets de vendeur de l'optimiseur d'équipement.", "Weapon": "Affiche les objets étant des armes.", "WeaponLevel": "Affiche les armes en fonction de leur niveau d'arme.", "WeaponType": "Affiche les armes selon leur type d'arme.", "Wishlist": "Affiche les objets présents dans votre liste de souhaits.", "WishlistDupe": "Affiche les doublons quand au moins un des doublons est dans votre liste de souhaits.", "WishlistEnabled": "Affiche les objets pouvant avoir des jets de liste de souhaits.", "WishlistNotes": "Affiche les éléments de la liste de souhait dont les notes correspondent à la recherche.", "WishlistUnknown": "Affiche les objets sans aucune recommandation dans les listes de souhaits chargées.", "Year": "Affiche les objets selon l'année à laquelle ils sont apparus." }, "General": { "ClickForDetails": "Cliquer pour détails", "Close": "Fermer", "Confirm": "Confirmer ?", "UserGuideLink": "Mode d'emploi" }, "Glyphs": { "Axe": "Hache", "DarkAbility": "Capacité des ténèbres", "Gilded": "Doré", "Harmonic": "Harmonique", "HiveSword": "Épée de la ruche", "LightAbility": "Capacité de lumière", "LightLevel": "Niveau de lumière", "Misadventure": "Mésaventure", "Missing": "Manquant", "OpenSymbolsPicker": "Ouvrir le sélecteur de symboles", "Prismatic": "Prismatique", "Quickfall": "Chute rapide", "RespawnRestricted": "Réapparition restreinte", "ScorchCannon": "Canon brûleur", "SearchSymbols": "Rechercher des symboles...", "Smoke": "Fumigène" }, "Header": { "About": "À propos de DIM", "AutoRefresh": "DIM se rechargera automatiquement tant que vous jouez.", "BulkTag": "Étiquetage de masse des objets", "BungieNetAlert": "Alerte Bungie", "Clear": "Effacer le filtre de recherche", "CompareMatching": "Comparer les objets", "DeleteSearch": "Supprimer la recherche", "FilterHelp": "Rechercher objet/attribut, {{example}}, et plus", "FilterHelpBrief": "Éléments de recherche", "FilterHelpLoadouts": "Rechercher les noms et les notes d'équipement", "FilterHelpMenuItem": "Aide des filtres...", "FilterHelpOptimizer": "Filtrer les armures incluses dans les builds, ex : {{example}}", "FilterHelpProgress": "Rechercher des jalons et des contrats", "FilterHelpRecords": "Rechercher triomphes et collections", "FilterMatchCount_one": "1 objet", "FilterMatchCount_other": "{{count}} objets", "Filters": "Filtres", "InstallDIM": "Installer en tant qu'application", "InstallDIMBanner": "Installez DIM comme application sur votre écran d'accueil", "Inventory": "Inventaire", "IosPwaPrompt": "Dans Safari, cliquez sur l'icône de partage (bouton central en bas) et sélectionnez \"Ajouter à l'écran d'accueil\".", "KeyboardShortcuts": "Raccourcis Clavier", "LaunchDIMAlone": "Nouvelle fenêtre", "MaterialCounts": "Quantité de matériaux", "Menu": "Menu", "ProfileAge": "Les serveurs de Destiny ont envoyé des données mises à jour il y a {{age}}.\nActualiser DIM peut permettre d'obtenir des données plus récentes, mais Bungie.net pourrait renvoyer des informations mises en cache.", "Refresh": "Rafraîchir les données de Destiny [R]", "ReloadApp": "Recharger l'application", "ReportBug": "Signaler un bug", "SaveSearch": "Enregistrer la recherche", "SearchActions": "Ouvrir les actions de recherche", "SearchResults": "Afficher les Objets", "Shop": "Boutique", "TagAs": "Étiqueter comme '{{tag}}'", "UpgradeDIM": "Mettre à jour DIM", "WhatsNew": "Nouveautés" }, "Help": { "CannotMove": "Impossible de déplacer cet objet de ce personnage.", "NoStorage": "DIM ne peut pas stocker de données", "NoStorageMessage": "DIM ne peut pas stocker de données dans votre navigateur. Cela peut être causé par la navigation privée ou en mode incognito, un espace disque faible, ou un bug du navigateur. Essayez de redémarrer votre ordinateur ! Vous ne pourrez pas vous connecter à ou utiliser DIM tant que vous n'aurez pas corrigé cela." }, "Hotkey": { "Armory": "Afficher l'armurerie pour un objet", "CheatSheetTitle": "Raccourcis Clavier:", "ClearDialog": "Fermer ce dialogue", "ClearNewItems": "Vider la liste de nouveaux objets", "Enter": "ENTRER", "ItemPopupTab": "Changer d'onglet de détails de l'objet", "LockUnlock": "Verrouiller ou déverrouiller un objet", "MarkItemAs": "Marquer objet comme '{{tag}}'", "Menu": "Activer/Désactiver le menu", "Note": "Saisir des notes", "Pull": "Transférer vers le personnage actif", "RefreshInventory": "Actualiser l'inventaire", "ShowHotkeys": "Afficher les raccourcis clavier", "StartSearch": "Commencer une recherche", "StartSearchClear": "Commencer une nouvelle recherche", "Tab": "TAB", "Vault": "Envoyer un objet au coffre" }, "InGameLoadout": { "ClearSlot": "Vider l'emplacement {{index}}", "Create": "Créer un équipement", "CreateTitle": "Créer un équipement en jeu à partir de l'équipement actuel", "CurrentlyEquipped": "Actuellement équipé", "DeleteFailed": "Échec de la suppression de l'équipement", "Deleted": "Équipement supprimé", "DeletedBody": "L'équipement en jeu à l'emplacement {{index}} a été vidé", "EditFailed": "Échec de la modification de l'équipement", "EditIdentifiers": "Changer l'apparence", "EditTitle": "Modifier le nom et l'icône de l'équipement", "EquipNotReady": "Pas prêt pour l'équipement en jeu", "EquipReady": "Prêt pour l'équipement en jeu", "LoadoutDetails": "Détails de l'équipement", "MatchingLoadouts": "Équipements correspondants :", "PrepareEquip": "Préparer pour équipement", "Replace": "Remplacer l'équipement {{index}}", "Save": "Mettre à jour l'équipement", "SaveIdentifiers": "Mettre à jour les identifiants", "SnapshotFailed": "Échec de la sauvegarde de l'équipement actuel" }, "Infusion": { "Filter": "Filtrer les objets", "InfuseSource": "Sélectionnez l'objet dans lequel infuser {{name}}", "InfuseTarget": "Sélectionnez l'objet à infuser dans {{name}}", "InfusionMaterials": "Matériaux d'infusion", "NoItems": "Aucun objet à infuser disponible.", "NoTransfer": "Transférer matériel d'infusion\n {{target}} ne peut pas être déplacé.", "SwitchDirection": "Inverser", "TransferItems": "Transférer" }, "Inventory": { "ClickToExpand": "(Cliquez pour agrandir)", "MissingSilver": "Votre solde d'Argentum est uniquement disponible lorsque vous jouez au jeu." }, "Item": { "SetBonus": { "NPiece_one": "{{count}} Piece", "NPiece_other": "{{count}} Piece" }, "ThumbsDown": "Pouce baissé", "ThumbsUp": "Pousse levé" }, "ItemFeed": { "ClearFeed": "Vider la liste", "Description": "Objets récents", "HideTagged": "Masquer étiquetés", "NoNewItems": "Aucun nouveaux objets", "ShowOlderItems": "Afficher les objets plus anciens" }, "ItemMove": { "Consolidate": "{{name}} consolidé", "Distributed": "{{name}} a été distribué\n{{name}} est maintenant divisé équitablement entre les personnages.", "MovingItem": "Transférer vers le coffre", "MovingItem_female": "Transfert vers {{target}}", "MovingItem_male": "Transfert vers {{target}}", "ToStore": "Tout(e)(s) les {{name}} sont maintenant dans votre {{store}}.", "ToVault": "Tout(e)(s) les {{name}} sont maintenant dans votre coffre." }, "ItemPicker": { "ChooseItem": "Choisissez un objet :", "SearchPlaceholder": "Éléments de recherche" }, "ItemService": { "BucketFull": { "Guardian": "Il y a trop d'objet '{{itemtype}}' dans ton {{store}}.", "Guardian_female": "Il y a trop d'objet '{{itemtype}}' dans ton {{store}}.", "Guardian_male": "Il y a trop d'objet '{{itemtype}}' dans ton {{store}}.", "Vault": "Il y a trop d'objet '{{itemtype}}' dans le {{store}}." }, "Classified": "Cette objet est classifié et ne peut pas être transférer à se moment.", "Classified2": "Objet classifié. Bungie n'a pas encore fourni d'information sur cet objet. Ajoutez des notes à cet objet et utilisez le filtre de recherche \"notes:\" pour le trouver.", "Deequip": "Aucun autre objet à équiper n'a été trouvé pour pouvoir déséquiper {{itemname}}", "ExoticError": "'{{itemname}}' ne peut pas être équipé car l'exotique dans le {{slot}} ne peut pas être déséquipé. ({{error}})", "NotEnoughRoom": "Il n'y a rien qu'on puisse déplacer de {{store}} pour faire de la place pour {{itemname}}", "NotEnoughRoomGeneral": "Il n'y a pas assez de place pour déplacer cet objet.", "OnlyEquippedClassLevel": "Cela ne peut être équipé que par un {{class}} de niveau {{level}} ou plus.", "OnlyEquippedLevel": "Cela ne peut être équipé que par les personnages de niveau {{level}} ou plus.", "PostmasterAlmostFull": "Presque plein !", "PostmasterFull": "Plein !", "PreviewVendor": "Aperçu du contenu {{type}}", "StackFull": "Vous avez déjà une pile pleine de {{name}}", "StoreName": "{{className}} {{genderRace}}" }, "KillType": { "ClassAbilities": "Capacité de classe", "Finisher": "Coup de grâce", "Grenade": "Grenade", "Melee": "Mêlée", "Precision": "Précision", "Super": "Super" }, "LB": { "AddStack": "Ajouter un autre exemplaire de ce mod", "AdvancedOptions": "Options avancées", "ChooseAMod": "Choisissez vos mods", "ChooseASetBonus": "Choose your set bonuses", "ChooseAnExotic": "Choisissez votre exotique", "ClearLocked": "Enlever verrouillés", "ContainsVendorItems": "Cet équipement contient des objets de marchand", "Current": "Actuel", "Equip": "Équiper sur {{character}}", "Exclude": "Objets exclus", "ExcludeHelp": "Shift + cliquez un objet (ou glissez-déposez dans cet espace) pour construire des sets sans équipement spécifique.", "ExistingBuildStats": "Stats de l'équipement existant", "ExistingBuildStatsNote": "N'affiche que les builds avec des stats strictement supérieures.", "FilterSets": "Filtrer les sets", "Help": { "And": "L'armure avec tout ces attributs sera utilisé (\"et\")", "ChangeNodes": "Changer les attributs d'intelligence, de discipline ou de force dans le jeu à ce qui est affiché pour créer chaque équipement.", "Discipline": "La Discipline accélère le temps de recharge des Grenades", "DragAndDrop": "Faites glisser et déposez les objets dans les seaux verrouillés pour construire des ensembles avec cette équipement seulement", "Help": "Besoin d'aide?", "HigherTiers": "Les paliers supérieurs sont meilleurs", "Intellect": "L'intelligence accélère le temps de recharge du super chargé", "Lock": "Verrouiller un ensemble d'attributs en cliquant sur un verrou et en sélectionnant des attributs", "MultiPerk": "Pour utiliser une armure avec plusieurs attributs réunis faite shift+click sur les attributs désirés", "NoPerk": "Si un attribut n'apparaît pas, cela veut dire que vous n'avez pas d'armure avec cet attribut", "Or": "L'armure avec n'importe lequel de ces attributs sera utilisé (\"ou\")", "ShiftClick": "Shift click sur un élément pour construire des ensembles sans cet équipement", "StatsIncrease": "À mesure que le niveau de défense des objets augmente, les statistiques de cet objet (int/dis/for) augmentent également.", "Strength": "La Force accélère le temps de recharge de la mêlée", "Synergy": "Essayez de trouver une armure qui a des attributs augmentant le nombre de munitions pour les types d'armes que vous utilisez.", "Tier11Example": "4/5/2 (un ensemble Palier 11) est 4 Intelligence 5 Discipline, 2 Force (4+5+2 = Palier 11)" }, "HideAllConfigs": "Cacher toutes les configurations", "HideConfigs": "Cacher les configurations", "IncompatibleWithOptimizer": "Cet objet est incompatible avec l'optimiseur. Obtenez en une nouvelle version depuis les Collections.", "LB": "Optimiseur d'équipement", "LightMode": { "HelpCurrent": "Calcule les équipements aux niveaux de défense actuels.", "HelpScaled": "Calcule les équipement comme si tous les objets avaient 350 de défense.", "LightMode": "Mode lumière" }, "Loading": "Chargement des meilleurs sets", "LockEquipped": "Verrouiller équipé", "LockPerk": "Verrouiller l'attribut", "Locked": "Objets verrouillés", "LockedHelp": "Glissez et déplacez un objet dans cet espace pour construire votre set avec cet équipement. Shift + clique pour exclure les objets.", "Missing2": "Pièces rares, légendaires ou exotiques manquantes pour construire un set complet!", "ProcessingMode": { "Fast": "Rapide", "Full": "Complet", "HelpFast": "Ne se concentre que sur votre meilleur équipement.", "HelpFull": "Se concentre sur plus d'équipements, mais prend plus de temps.", "ProcessingMode": "Mode de processing" }, "RemoveStack": "Retirer un exemplaire de ce mod", "Scaled": "Échelonné", "SearchAMod": "Rechercher un nom ou une description de mod", "SearchASetBonus": "", "SearchAnExotic": "Rechercher un nom ou une description d'exotique", "SelectExotic": "Sélectionner exotique", "SelectMods": "Sélectionner les Mods", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Mods d'activité", "SelectSetBonus": "Select Set Bonuses", "SelectSubclassOptions": "Personnaliser la doctrine", "ShowAllConfigs": "Afficher toutes les configurations", "ShowConfigs": "Afficher les configurations", "ShowGear": "Armure de {{class}}", "Vendor": "Inclure objets de marchands" }, "Loading": { "Accounts": "Chargement des comptes Destiny...", "Code": "Chargement du code de DIM...", "FilterHelp": "Chargement du guide de recherche...", "Profile": "Chargement du profil Destiny...", "Vendors": "Chargement des marchands Destiny..." }, "LoadoutAnalysis": { "Analyzed": "{{numLoadouts}} équipements analysés", "Analyzing": "Analyse de {{numAnalyzed}}/{{numLoadouts}} équipements", "BetterStatsAvailable": { "Description": "Choisir différents mods ou armures pour cet équipement lui permettra d'atteindre des stats plus élevées. Choisissez \"$t(Loadouts.OpenInOptimizer)\" pour voir de meilleurs builds.", "Name": "Amélioration de stats possibles" }, "BetterStatsAvailableFontNote": "Note : Cet équipement utilise des mods \"Source de ...\" qui font dépasser 200 points à une stat. DIM peut identifier de meilleurs stats en réduisant la quantité de stats en trop. Si ce n'est pas ce que vous voulez, désactivez \"$t(Loadouts.IncludeRuntimeStatBenefits)\"dans l'équipement.", "DoesNotRespectExotic": { "Description": "Les paramètres d'optimiseur de cet équipement spécifient un choix d'exotique, mais cet équipement ne contient pas cet exotique.", "Name": "Mauvais exotique" }, "DoesNotSatisfyStatConstraints": { "Description": "Les paramètres d'optimiseur de cet équipement spécifient des minimums de stats, mais cet équipement ne les atteint pas.", "Name": "Mauvais minimum de stats" }, "EmptyFragmentSlots": { "Description": "Il y a des emplacements de fragment vides dans cette doctrine.", "Name": "Emplacements de fragment vides" }, "InvalidMods": { "Description": "Certains mods de cet équipement sont obsolètes ou ne sont pas adaptés à l'une de vos pièces d'armures.", "Name": "Mods obsolètes" }, "InvalidSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer that is not valid.", "Name": "Recherche invalide" }, "ItemsDoNotMatchSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer, and that search query excludes at least one of the items in the loadout.", "Name": "Search Excludes Items" }, "MissingItems": { "Description": "Certains objets de cet équipement ne sont plus dans votre inventaire.", "Name": "Objets manquants" }, "ModsDontFit": { "Description": "Les armures de cet équipement ne peuvent pas accueillir tous les mods de l'équipement, même si les armures étaient améliorées.", "Name": "Mods non assignés" }, "NeedsArmorUpgrades": { "Description": "Les armures de cet équipement doivent être améliorées pour pouvoir accueillir tous les mods ou atteindre les stats spécifiées.", "Name": "Amélioration d'armure nécessaire" }, "NotAFullArmorSet": { "Description": "Cet arsenal n'a pas pu être complétement analysé, car il n'inclut pas un ensemble complet d'armures.", "Name": "Pas un ensemble d'armure complet" }, "TooManyFragments": { "Description": "Il y a plus de fragments configurés pour cette doctrine qu'elle n'en a grâce aux aspects.", "Name": "Trop de fragments" }, "UsesSeasonalMods": { "Description": "Cet équipement dépend de mods qui ne sont disponibles que certaines saisons. Quand la saison se termine, certains mods seront indisponible ou dépasseront la capacité en énergie de l'armure.", "Name": "Utilise des mods saisonniers" } }, "LoadoutBuilder": { "All": "Tous", "AlwaysAutoMods": "Artifice and Tuning mods will always be chosen automatically.", "AnyExotic": "N'importe quel exotique", "AnyExoticDescription": "Le build doit contenir un exotique, mais n'importe quel exotique fera l'affaire.", "Artifice": "Artifice", "AssumeMasterwork": "Considérer pièce maîtresse", "AssumeMasterworkOptions": { "All": "Toutes les armures : $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "Toutes les armures : $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nArmures 2.0 Exotiques : $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Améliorée pour avoir un mod d'artifice.", "Current": "Stats actuelles, considère un niveau d'énergie d'au moins {{minLoItemEnergy}}.", "Legendary": "Légendaire : $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExotique : $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Full masterwork stat bonuses, assumed energy level at least 10.", "None": "Toutes les armures : $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "Ajouter automatiquement les mods de statistiques", "AutomaticallyPicked": "Ce mod a été ajouté automatiquement pour améliorer les statistiques de l'équipement.", "CompareLoadout": "Comparer l'équipement", "ConfirmOverwrite": "Êtes-vous sûr de vouloir remplacer l'armure dans l'équipement \"{{name}}\" par ce nouvel ensemble d'armure ?", "DecreaseStatPriority": "Diminuer la priorité de la stat", "DisabledByAutoStatMods": "Les mods de stat sont choisis automatiquement par l'optimisateur d'équipement.", "DisabledDueToMaintenance": "L'optimiseur d'équipement est actuellement désactivé en raison d'une maintenance sur l'API Bungie.", "EquipItems": "Équiper", "ExcludeItem": "Exclure un objet", "ExcludeVendors": "Search \"not:vendor\" to exclude vendor items from Loadout Optimizer.", "ExcludedItems": "Objets exclus", "ExistingLoadout": "Équipement existant", "Exotic": "Armure exotique", "ExoticClassItemPerks": "If you want specific perks, use searches like exactperk:\"spirit of verity\". Click perks in the Optimizer results to add or remove them from the item filter.", "ExoticSpecialCategory": "Spécial", "FOTLWildcardWarning": "This set contains a Festival of the Lost mask. Manually apply the correct mod to activate desired set bonuses.", "Filter": "Paramètres", "IgnoreStat": "Si décoché, l'optimiseur d'équipement prétendra que cette stat n'existe pas lors du calcul des ensembles", "IncreaseStatPriority": "Augmenter la priorité de la stat", "Legendary": "Légendaire", "LimitToNewFeaturedGear": "Limiter aux équipements nouveaux/à la une", "LockItem": "Inclure objet", "MissingClass": "Build pour: {{className}}", "MissingClassDescription": "Le build que vous essayez de voir est fait pour une classe que vous n'avez pas.", "MwExotic": "Exotique", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "Une recherche active restreint les éléments que DIM peut inclure dans les builds", "AllowAutoStatMods": "Autoriser DIM à inclure automatiquement des mods de stat supplémentaires", "AlwaysInvalidMods": "Ces mods ne rentrent dans aucun des objets que vous possédez :", "AssumeMasterworked": "Autoriser DIM à recommander le passage en pièce maîtresse d'une armure", "AssumptionsRestricted": "DIM ne peut pas recommander des changements d'élément d'armure :", "BadSlot": "Dans l'emplacement {{bucketName}}, aucun des objets autorisés ne peut être adapté à ces mods :", "ExoticDoesNotExist": "Vous n'avez pas l'armure exotique sélectionnée dans votre inventaire.", "Header": "Aucun build trouver. Voici une liste des raisons possibles pour lesquelles DIM n'a pas pu trouver de builds :", "LowerBoundsFailed": "Plusieurs ensembles n'ont pas atteint les minimums de stats requis", "MaybeAllowMoreItems": "Envisagez d'autoriser d'autres objets :", "MaybeDecreaseLowerBounds": "Envisagez de réduire les minimum de stats requis", "MaybeRemoveMods": "Envisagez de retirer certains mods :", "MaybeRemoveSearchQuery": "Envisagez d'effacer ou de changer le filtre dans la barre de recherche", "ModAssignmentFailed": "De nombreux ensembles n'étaient pas adaptés à l'ensemble des mods demandés", "RemoveMods": "Supprimer ces mods", "RemoveSetBonuses": "Consider removing some set bonuses", "SetBonuses": "You have chosen some set bonuses, maybe you don't have the right items to use them." }, "NoExotic": "Pas d'exotique", "NoExoticDescription": "Équivalent à rechrcher \"not:exotic\" dans la barre de recherche - les ensembles n'utiliseront aucune armure exotique.", "NoExoticPreference": "Aucun exotique sélectionné", "NoExoticPreferenceDescription": "Une armure exotique sera utilisée si elle maximise les stats.", "NoLoadoutsToCompare": "Aucun équipement à comparer", "None": "Aucun", "OptimizerExplanationGuide": "Lisez le guide utilisateur pour plus d'informations et un tutoriel vidéo.", "OptimizerExplanationMods": "Choisissez un exotique, des mods, et une doctrine. Ils contribueront aux stats de ce build, tandis que les mods déjà présent sur l'armure seront ignorés.", "OptimizerExplanationSearch": "Utilisez la barre de recherche pour affiner la sélection d'armure, ex : {{example}}. Si la recherche ne renvoie aucune armure pour un emplacement, tous les objets seront considérés pour cet emplacement.", "OptimizerExplanationStats": "Déplacez les stats les plus importantes vers le haut, et décochez les stats que vous ne voulez pas maximiser.", "OptimizerSet": "Ensemble de l'optimiseur", "PinnedItems": "Objets épinglés", "PinnedItemsFinePrint": "Search filters are saved with Loadout Optimizer settings, but pinned and excluded items are not. Pins and exclusions will be ignored when DIM checks existing Loadouts for better stat builds.", "ProcessingSets": "Recherche des ensembles aux stats les plus élevés...", "SaveAs": "Enregistrer sous", "SetBonus": "Set Bonuses", "SpeedReport": "Evaluated {{combos, number}} combinations in {{time}} seconds using {{cpus}} CPU cores.", "StatConstraints": "Priorités et fourchettes de stats", "StatMax": "Max", "StatMin": "Min", "StatRangeTooltip": "Avec les paramètres min/max actuels, des équipements existent avec {{min}} à {{max}} points dans cette stat. Double-cliquez pour définir min à {{max}}.", "StatTotal": "Total : {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "Impossible d'ajouter tous les mods.", "UnableToAddAllModsBody": "Il n'y avait pas assez d'emplacements de mod disponibles pour placer {{mods}}.", "UnlockItem": "Désépingler Objet" }, "LoadoutFilter": { "Contains": "Affiche les équipements ayant un objet ou un mod correspondant au texte du filtre. Recherchez des objets ayant des espaces dans leur nom en utilisant des guillemets.", "FashionOnly": "Affiche les équipements qui ne contiennent que du style (revêtements et ornements).", "LoadoutLight": "Affiche les équipements en fonction de leur niveau de lumière calculé. Utilisez les mot-clés \"pinnaclecap\" ou \"softcap\" pour référer aux limites de puissance de la saison actuelle.", "ModsOnly": "Affiche les équipements qui ne contiennent que des mods d'armure.", "Name": "Affiche les équipements dont le nom correspond totalement (exactname:) ou partiellement (name:) au filtre de recherche. Recherchez des phrases en utilisant des guillemets.", "Notes": "Rechercher des équipements par leur notes.", "PartialMatch": "Affiche les équipements dont le nom ou les notes ont une correspondance avec le texte du filtre. Rechercher des phrases en utilisant des guillemets.", "Season": "Affiche les équipements selon la dernière saison de Destiny 2 où ils ont été modifiés pour la dernière fois.", "Subclass": "Affiche les équipements dont le nom de doctrine ou le type de dégâts correspond au texte du filtre." }, "Loadouts": { "Abilities": "Capacités", "Actions": "Actions pour {{title}}", "AddEquippedItems": "Ajouter les objets équipés", "AddNotes": "Ajouter des notes", "AddUnequippedItems": "Ajouter les objets non équipés", "Any": "N'importe quelle classe", "Apply": "Appliquer", "ApplyInGameLoadoutInGame": "Votre équipement est prêt à être équipé mais puisque vous êtes dans une activité, vous devez l'équiper en jeu.", "ApplyMods": "Ajout des mods", "ApplySearch": "Transférer la recherche \"{{query}}\"", "ArmorStats": "Statistiques d'armure", "ArtifactUnlocks": "Déverrouillages d'artefact", "ArtifactUnlocksDesc": "En raison des limitations de Bungie.net, DIM ne peut pas configurer automatiquement votre artefact. Vous devez effectuer ces déverrouillages en jeu avant d'appliquer l'équipement.", "ArtifactUnlocksWithSeason": "Déverrouillages d'artefact – S{{seasonNumber}}", "BadLoadoutShare": "Impossible de charger l'équipement partagé", "BadLoadoutShareBody": "L'équipement que vous essayez de charger est invalide : {{error}}", "Before": "Annuler '{{name}}'", "CancelEditing": "Annuler l'édition", "CannotCustomizeSubclass": "Cette doctrine ne peut pas être configurée", "ChooseItem": "Ajouter {{name}}", "ClassType": "Équipement pour n'importe quelle classe", "ClassTypeMismatch": "Un objet pour {{className}} ne peut pas être ajouté à cet équipement", "ClassTypeMissing": "Vous n'avez aucun {{className}} pour lequel créer un équipement", "ClassType_female": "Équipement {{className}}", "ClassType_male": "Équipement {{className}}", "Classified": "Certains de vos objets sont classifiés et n'ont pas pu être inclus dans le calcul de votre puissance maximum.", "ClearLoadoutParameters": "Retirer les paramètres de l'optimiseur d'équipement", "ClearSection": "Tout retirer", "ClearSpace": "Déplacer les autres ailleurs", "ClearSpaceArmor": "Déplacer l'autre armure ailleurs", "ClearSpaceWeapons": "Déplacer les autres armes ailleurs", "ClearUnsetMods": "Retirer les autres mods", "ClearingSpace": "Déplacement des autres objets", "CopyAndEdit": "Copier et éditer", "Create": "Créer un équipement", "CurrentlyEquipped": "Actuellement équipé", "Deequip": "Dé-équipement des objets des autres personnages", "Delete": "Supprimer", "DimLoadouts": "Équipements DIM", "Edit": "Modifier l'équipement", "EditBrief": "Éditer", "EquipInGameLoadout": "Équipement de l'équipement en jeu", "EquipItems": "Équipement des objets", "EquippableDifferent1": "Plusieurs objets exotiques ont été utilisé pour calculer votre puissance maximale, le niveau affiché pourrait donc ne pas être obtenable en équipant vos objets en jeu.", "EquippableDifferent2": "La puissance maximale n'est pas limité par la règle de \"Un seul exotique\" lors du calcul de la puissance de vos récompenses puissantes et de prestige.", "Failed": "L'équipement n'a pas pu être appliqué complétement", "Fashion": "Choisir le style", "FashionOnly": "Seulement du style", "FillFromEquipped": "Remplir en utilisant équipés", "FillFromInventory": "Remplir en utilisant non-équipés", "FilteredItems": "Objets filtrés", "FindAnother": "Trouver un autre {{name}}", "FromEquipped": "Équipé", "Generated": "Équipement de {{statTotal}} points de stat", "HashtagTip": "Astuce : Utilisez des #hashtags dans vos noms ou notes d'équipement et ils apparaîtront ici.", "Import": { "BadURL": "Ce n'est pas une URL de partage d'équipement valide.", "Error": "Erreur lors de l'obtention de l'équipement :", "Error404": "Cet équipement n'existe pas.", "PasteHere": "Collez un lien d'équipement pour ouvrir l'équipement." }, "ImportLoadout": "Importer un équipement", "InGameActions": "Actions pour équipement en jeu", "InGameLoadouts": "Équipements en jeu", "IncludeRuntimeStatBenefits": "Inclus les stats des mods Puits", "IncludeRuntimeStatBenefitsDesc": "Les mods d'armure \"Puit de...\" fournissent un bonus fixe de stats de personnage lorsque vous avez des charges d'armure.\n\nAvec ce paramètre, DIM considère ces mods actifs et ajoute leurs bénéfices aux stats de cet équipement dans les calculs et les optimisations.", "ItemErrorSummary_one": "1 erreur d'objet :", "ItemErrorSummary_other": "{{count}} erreurs d'objet:", "ItemLeveling": "Infusion d'objet", "LoadoutName": "Nom de l'équipement", "LoadoutParameters": "Paramètres de l'optimiseur d'équipement", "LoadoutParametersExotic": "L'équipement doit inclure cet exotique : {{exoticName}}", "LoadoutParametersQuery": "Les objets doivent correspondre à ce filtre de recherche", "LoadoutParametersStats": "Priorités de statistiques et fourchettes de stats minimum/maximum", "Loadouts": "Équipements", "MakeRoom": "Faire de la place pour le Commis des postes", "MakeRoomDone_female_one": "À Fini de faire de la place pour 1 objet du commis des postes en déplaçant 1 objet hors du {{store}}.", "MakeRoomDone_female_other": "À Fini de faire de la place pour {{count}} objets du Commis des postes en déplaçant {{movedNum}} objets hors du {{store}}.", "MakeRoomDone_male_one": "À Fini de faire de la place pour 1 objet du commis des postes en déplaçant 1 objet hors du {{store}}.", "MakeRoomDone_male_other": "À Fini de faire de la place pour {{count}} objets du Commis des postes en déplaçant {{movedNum}} objets hors du {{store}}.", "MakeRoomDone_one": "À Fini de faire de la place pour 1 objet du commis des postes en déplaçant 1 objet hors du {{store}}.", "MakeRoomDone_other": "À Fini de faire de la place pour {{count}} objets du Commis des postes en déplaçant {{movedNum}} objets hors du {{store}}.", "MakeRoomError": "Impossible de faire de la place pour tous les objets du Commit Des Postes: {{error}}.", "ManageLoadouts": "Gérer les équipements", "MaxSlots": "Vous ne pouvez avoir que {{slots}} {{bucketName}} dans un équipement.", "MaximizeLight": "Lumière maximale", "MaximizePower": "Puissance Max", "MaximizeStat": "Maximiser la Stat", "MissingItemsWarning": "Certains objets de cet équipement ne sont plus dans votre inventaire.", "ModErrorSummary_one": "1 erreur de mod :", "ModErrorSummary_other": "{{count}} erreurs de mod:", "ModPlacement": { "InvalidMods": "Mods invalides", "InvalidModsDesc_one": "1 mod ne peut pas être placé dans n'importe quelle pièce d'armure.", "InvalidModsDesc_other": "{{count}} mods ne peuvent pas être placés dans n'importe quelle pièce d'armure.", "ModPlacement": "Placement des mods", "StackableMod": "Cumulable", "UnassignedMods": "Mods non assignés", "UnassignedModsDesc_one": "1 mod n'a pas pu être placé en raison d'un manque d'énergie ou d'emplacement de mod. L'amélioration de l'énergie de l'armure sélectionnée ne résoudra pas le problème.", "UnassignedModsDesc_other": "{{count}} mods n'ont pas pu être placés en raison d'un manque d'énergie ou d'emplacement de mod. L'amélioration de l'énergie de l'armure sélectionnée ne résoudra pas le problème.", "UnstackableMod": "Non cumulable", "UpgradeCosts": "Coûts d'amélioration", "UpgradeCostsDesc": "Certaines armures ont besoin d'une amélioration d'énergie pour pouvoir accepter les mods demandés. Au total, ces améliorations coûtent :" }, "Mods": "Mods", "ModsOnly": "Seulement des mods", "MoveItems": "Déplacement des objets", "NoSpace": "Vous n'avez plus d'espace libre dans le coffre et votre autres personnages.", "NoneMatch": "Aucun de vos équipements ne correspond aux filtres.", "NotStarted": "En attente que d'autres actions soient terminées, ou que l'inventaire soit actualisé pour terminer le chargement", "NotesPlaceholder": "Écrivez quelques notes sur cet équipement ou utilisez des #hashtags pour le catégoriser", "NotificationTitle": "Équipement : {{name}}", "OnWrongCharacterAdvice": "Cliquez ici pour trouver les objets les plus puissants de ce personnage.", "OnWrongCharacterWarning": "Les armures les plus puissantes de ce personnage sont sur un autre personnage. Pour qu'ils comptent dans le calcul de puissance des récompenses normales, de puissance, et de prestige, les armures doivent être sur ce personnage ou dans le coffre.", "OnlyItems": "Seuls les objets équipables, matériaux et consommables peuvent être ajoutés à un équipement.", "OpenInOptimizer": "Optimiser l'armure", "OpenOnStreamDeck": "Ouvrir sur Stream Deck", "PickArmor": "Choisir une armure", "PickMods": "Ajouter des mods d'armure", "Prismatic": { "Aspect": "Aspect prismatique", "Grenade": "Grenade prismatique", "Melee": "Mêlée prismatique", "Super": "Compétence de super" }, "PullFromPostmaster": "Collecter le commis des postes", "PullFromPostmasterError": "Impossible de récupérer du commis des postes: {{error}}.", "PullFromPostmasterGeneralError": "Impossible de récupérer tous les objets du commis des postes.", "PullFromPostmasterNotification_female_one": "Récupération de 1 objet du commis des postes vers {{store}}.", "PullFromPostmasterNotification_female_other": "Récupération de {{count}} objets du commis des postes vers {{store}}.", "PullFromPostmasterNotification_male_one": "Récupération de 1 objet du commis des postes vers {{store}}.", "PullFromPostmasterNotification_male_other": "Récupération de {{count}} objets du commis des postes vers {{store}}.", "PullFromPostmasterNotification_one": "Récupération de 1 objet du commis des postes vers {{store}}.", "PullFromPostmasterNotification_other": "Récupération de {{count}} objets du commis des postes vers {{store}}.", "PullFromPostmasterPopupTitle": "Récupérer du commis des postes", "Random": "Aléatoire", "Randomize": "Équipement aléatoire", "RandomizeButton": "Aléatoire", "RandomizeNew": "Créer aléatoire", "RandomizeQueryHint": "Astuce : Recherchez d'abord des objets pour réduire la liste des objets qui peuvent être choisis aléatoirement.", "RandomizeSearch": "Équiper aléatoire à partir de la recherche", "RandomizeSearchPrompt": "Équiper au hasard à partir de la recherche \"{{query}}\"?", "Redo": "Refaire", "RestoreAllItems": "Tous les objets", "SalvationsEdgeMods": "Salvation's Edge Mods", "Save": "Sauvegarder", "SaveAsDIM": "Sauvegarder comme équipement DIM", "SaveAsNew": "Sauvegarder comme nouveau", "SaveAsNewTooltip": "Garder l'équipement original et enregistrer celui-ci comme un nouvel équipement", "SaveDisabled": { "AlreadyExists": "Choisissez un nouveau nom pour l'équipement.", "Empty": "L'équipement est vide.", "NoName": "L'équipement a besoin d'un nom." }, "SaveLoadout": "Sauvegarder l'équipement", "Season": "Saison {{season}}", "SetBonusesDesc": "Required set bonuses", "Share": { "Copied": "Lien de l'équipement copié dans le presse-papiers", "CopyButton": "Copier le lien", "Error": "Erreur lors de l'obtention du lien de partage", "Fashion": "Style (revêtements & ornements)", "LoadoutOptimizer": "Paramètres de l'optimiseur d'équipement", "NativeShare": "Partager le lien", "Notes": "Notes", "NumItems_one": "{{count}} objet - les destinataires seront invités à sélectionner un objet similaire dans leur inventaire", "NumItems_other": "{{count}} objets - les destinataires seront invités à sélectionner des objets similaires dans leur inventaire", "NumMods_one": "{{count}} mod", "NumMods_other": "{{count}} mods", "Placeholder": "Chargement du lien de partage", "Subclass": "Personnalisation de la doctrine", "Summary": "Partager cet équipement contenant :", "Title": "Partager \"{{name}}\"" }, "ShareLoadout": "Partager", "ShowModPlacement": "Afficher le placement des mods", "Snapshot": "Sauvegarder comme équipement en jeu", "SocketOverrides": "Changement des options de doctrine", "SortByEditTime": "Trier par dernière modification", "SortByName": "Trier par nom", "SubclassOptions": "Options de {{subclass}}", "SubclassOptionsSearch": "Rechercher dans les options de {{subclass}}", "Succeeded": "Équipement réussi", "SyncFromEquipped": "Synchroniser avec ceux équipés", "TooManyRequested": "Vous avez {{total}} {{itemname}} mais votre équipement demande {{requested}}. Nous avons transféré tous ceux que vous possédiez.", "TuningMods": "Tuning Mods", "UnassignedModError": "Le mot n'a pas pu être équipé sur votre armure actuelle", "Undo": "Annuler", "Update": "Enregistrer les modifications", "UpdateLoadout": "Mettre à jour l'équipement", "VendorsCannotEquip": "Vous n'avez pas ces objets. Appuyez pour sélectionner un remplacement ou cliquez sur X pour retirer:" }, "Manifest": { "Download": "Téléchargement de la dernière base de données d'informations Destiny depuis Bungie...", "Error": "Erreur lors du chargement de la base de données d'information Destiny:\n{{error}}\nRechargez pour réessayer.", "Load": "Chargement de la base de données d'informations de Destiny..." }, "Milestone": { "Daily": "Défi Journalier", "OneTime": "Défi Unique", "SeasonalRank": "Rang Saisonnier {{rank}}", "Special": "Défi d’Événement Spécial", "Tutorial": "Défi Tutoriel", "Unknown": "Défi", "Weekly": "Défi Hebdomadaire" }, "Mods": { "HarmonicModDescription": "L'effet de ce mod a un coût réduis et change d'élément en fonction de la doctrine équipée." }, "MoveAmount": { "Amount": "Quantité :" }, "MovePopup": { "Acquired": "Cet élément est déverrouillé dans les collections.", "AcquiredMod": "Ce mod est débloqué dans les collections.", "AddNote": "Ajouter des notes", "AddToLoadout": "Équipement", "AddToLoadoutTitle": "Ajouter à cet équipement", "All": "Tous", "ArtifactBreaker": "Cette arme a {{breaker}} grâce à un attribut d'artefact.", "CannotCurrentlyRoll": "Cet attribut ne peut pas être obtenu sur la version actuelle de cet objet.", "CantPullFromPostmaster": "Vous devez visiter le commis des postes en jeu pour récuperer cet objet.", "CatalystProgress": "Progression du catalyseur", "CommunityData": "Informations de la communauté", "Consolidate": "Réunir", "DistributeEvenly": "Distribuer équitablement", "EnhancementTier": "Palier {{tier}}", "Equip": "Équiper sur :", "EquipWithName": "Équiper sur {{character}}", "FavoriteUnFavorite": { "Favorite": "Mettre {{itemType}} en favoris", "Favorited": "Favoris", "Unfavorite": "Enlever {{itemType}} des favoris", "Unfavorited": "Non favoris" }, "Infuse": "Infuser", "InfuseTitle": "Ouvrir l'outil d'infusion", "IntrinsicBreaker": "Cette arme a intrinsèquement {{breaker}}.", "LoadingSockets": "Les détails des attributs et des stats n'ont pas encore été chargés pour cet objets.", "LockUnlock": { "AutoLock": "L'état du verrouillage est synchronisé avec les étiquettes de cet objet", "Lock": "Bloquer {{itemType}}", "Locked": "Verrouillé", "Unlock": "Débloquer {{itemType}}", "Unlocked": "Déverrouillé" }, "MissingSockets": "Les détails des attributs et mods sont indisponibles pendant que Bungie mettent à jour leurs services. Généralement cela prend quelques heures.", "Notes": "Notes:", "OpenOnStreamDeck": "Ouvrir sur Stream Deck", "OverviewTab": "Vue d'ensemble", "Owned": "Cet élément est dans votre inventaire.", "OwnedMod": "Ce mod est dans votre inventaire de modifications.", "PullItem": "Déplacer de {{bucket}} vers {{store}}", "PullPostmaster": "Récupérer du commis des postes", "ReadLore": "Lire le lore de Ishtar Collective", "ReadLoreLink": "Lire l'histoire", "Rewards": "Récompenses:", "SendToVault": "Envoyer au Coffre", "Store": "Récuperer sur :", "StoreWithName": "Récuperer sur {{character}}", "Subtitle": { "QuestProgress": "Étape {{questStepNum}} sur {{questStepsTotal}}", "Type": "{{classType}} {{typeName}}" }, "TabList": "Onglet de détails de l'objet", "ToggleSidecar": "Développer ou réduire les actions d'objet", "TrackUntrack": { "Track": "Suivre {{itemType}}", "Tracked": "Suivi", "Untrack": "Ne plus suivre {{itemType}}", "Untracked": "Non suivi" }, "TriageTab": "Triage", "UnreliablePerkOption": "Cet attribut n'apparait que dans les collections. Il se peut qu'il ne puisse pas être aléatoirement obtenu sur cet objet.", "Vault": "Coffre", "WeaponLevel": "Niveau d'arme {{level}}" }, "Notes": { "Error": "Erreur! Max 120 caractères pour les notes.", "Help": "Ajouter des notes, #hashtags, et :symbols:" }, "Notification": { "Cancel": "Annuler", "OK": "Fermer" }, "Objectives": { "Complete": "Achevé", "Incomplete": "Incomplet" }, "Organizer": { "BulkMove": "Déplacer vers", "BulkMoveLoadoutName": "Sélectionné dans l'organisateur", "BulkTag": "Étiqueter", "Columns": { "Ammo": "Munitions", "Archetype": "Archétype", "BaseStats": "Statistiques de Base", "Breaker": "Anti-champion", "Crafted": "Date de façonnage", "CustomTotal": "Total Personnalisé", "Damage": "Dégâts", "Energy": "Énergie", "Event": "Evènement", "Featured": "Nouveaux équipements", "Foundry": "Fabricant", "Frame": "Frame", "Harmonizable": "Harmonisable", "Holofoil": "Holofoil", "Icon": "Icône", "ItemTier": "Palier", "KillTracker": "Frags", "Level": "Niveau", "Loadouts": "Équipements", "Location": "Emplacement", "Locked": "Verrouillé", "MasterworkStat": "Stats de PM", "MasterworkTier": "Palier de PM", "ModSlot": "Emplacement de Mod", "Mods": "Mods", "Name": "Nom", "New": "Nouveau", "Notes": "Notes", "OriginTraits": "Attribut d'origine", "OtherPerks": "Weapon Components", "PercentComplete": "% Complet", "Perks": "Compétences", "PerksGrid": "Grille d'attributs", "Power": "Puissance", "Quality": "Qualité %", "Recency": "Récence", "Season": "Saison", "Shaders": "Cosmétiques", "Source": "Origine", "StatQuality": "Qualité de la Stat", "StatQualityStat": "{{stat}}%", "Stats": "Statistiques", "Tag": "Étiqueter", "TertiaryStat": "3rd Stat", "Tier": "Rareté", "Traits": "Caractéristiques de l'Arme", "TuningStat": "Tuner", "WishList": "Liste de souhaits", "WishListNotes": "Notes de la liste de souhaits", "Year": "Année" }, "EnabledColumns": "Colonnes activées", "Lock": "Vérouiller", "NoItems": "Aucun objet ne correspond aux filtres. Si vous avez une recherche, essayez de la supprimer.", "NoMobile": "Tournez votre téléphone sur le côté pour utiliser l'Organisateur.", "Note": "Changer les notes", "OpenIn": "Afficher dans l'organisateur", "Organizer": "Organisateur", "SelectAll": "Tout sélectionner", "SelectItem": "Sélectionner ou désélectionner {{name}}", "ShiftTip": "Astuce: Maintenez la touche Maj et cliquez sur une cellule pour filtrer les objets", "Stats": { "Aim": "Visée", "Airborne": "Aérien", "AmmoGeneration": "Génération de munitions", "Power": "Puissance", "RPM": "CPM", "Recoil": "Recul", "Reload": "Recharger" }, "Unlock": "Déverrouiller" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "Le commis des postes est presque plein ! ({{number}}/{{postmasterSize}})", "PostmasterFull": "Le commis des postes est plein ! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "Contrats", "CatalystSource": "Source : {{source}}", "CrucibleRank": "Rangs", "Items": "Objets de quête", "Milestones": "Jalons & Défis", "NoEventChallenges": "Vous avez terminé tous les défis d'événement", "NoTrackedTriumph": "Vous n'avez aucun triomphe suivi. Suivez en autant que vous voulez sur DIM.", "PaleHeartPathfinder": "Cheminement du cœur pâle", "PercentMax": "{{pct}}% du maximum", "PercentPrestige": "{{pct}}% du rang maximum", "PointsUsed_one": "1 point utilisé", "PointsUsed_other": "{{count}} points utilisés", "PowerBonusHeader": "Récompenses +{{powerBonus}} puissance", "PowerBonusHeaderUndefined": "Autres récompenses", "Progress": "Avancement", "QueryFilteredTrackedTriumphs": "Aucun de vos triomphes suivis ne correspond à la recherche", "QuestExpired": "Expiré", "QuestExpires": "Expire dans ", "Quests": "Quêtes", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}pts", "Resets_one": "1 réinitialisation", "Resets_other": "{{count}} réinitialisations", "RewardPassEndsIn": "Reward Pass ends in ", "RewardPassPrestigeRank": "Prestige Rank {{rank}}", "SeasonalHub": "Hub saisonnier", "StatTrackers": "Compteur de Statistiques", "TrackedTriumphs": "Triomphes Suivis" }, "RecordBooks": { "HideCompleted": "Cacher les exploits complétés", "RecordBooks": "Carnet d'exploits" }, "Records": { "Title": "Archives", "UniversalOrnamentSetOther": "Autre" }, "SearchHistory": { "Date": "Dernier Utilisé", "DeleteAll": "Supprimer toutes les recherches sans étoile", "Description": "Ce sont toutes vos recherches passées et enregistrées. Vous pouvez les supprimer ici.", "Item": "Recherche d'objets", "Link": "Afficher et modifier l'historique de recherche", "Loadout": "Recherche d'équipements", "Query": "Recherche", "Title": "Historique de recherche", "UsageCount": "# Utilisé" }, "Settings": { "Appearance": "Appearance", "ArmorArchetypeModslot": "Armor Archetype / Modslot", "AutoLockTagged": "Synchroniser l'état de verrouillage des objets avec leur étiquette", "AutoLockTaggedExplanation": "DIM verrouillera et déverrouillera automatiquement les objets pour correspondre à leur étiquette. Les objets façonnés resteront déverrouillés pour permettre le refaçonnage. Lorsque ce paramètre est activé, l'icône de verrouillage ne sera pas affichée sur l'icône des objets étiquetés.", "BadgePostmaster": "Afficher le nombre d'objets au commis des postes pour le personnage actuel sur l'icône de l'application", "BadgePostmasterExplanation": "Pour utiliser cette fonctionnalité vous devez installer DIM en tant qu'application et votre système d'exploitation doit supporter l'affichage de badges", "BothDescriptions": "Les deux descriptions", "BungieDescriptionOnly": "Description de Bungie", "CharacterOrder": "Trier les personnages par", "CharacterOrderFixed": "Âge du personnage (buggé sur PC)", "CharacterOrderRecent": "Personnage le plus récent", "CharacterOrderReversed": "Personnage le plus récent (inversé)", "ColumnSize": "{{num}} objets", "ColumnSizeAuto": "Auto", "CommunityData": "Informations de la communauté sur les attributs", "CommunityDescriptionOnly": "Description de la communauté", "CsvImport": "Importer CSV", "CustomErrorLabel": "Un nom de statistique doit contenir des mots, et être différent des autres noms de statistique pour cette classe de gardien.", "CustomErrorValues": "Les poids des statistiques doivent être des nombres positifs. Au moins 2 poids de statistique doivent être au dessus de zéro.", "CustomStatChooseName": "Choisissez un nom de statistique personnalisée", "CustomStatCreate": "Créer une nouvelle statistique personnalisée", "CustomStatDelete": "Supprimer cette statistique personnalisée", "CustomStatDeleteConfirm": "Supprimer cette statistique personnalisée ?", "CustomStatDesc1": "Choisissez les statistiques d'armure souhaitées pour créer un total de statistique personnalisé.", "CustomStatDesc3": "Les statistiques personnalisées apparaîtront dans les fenêtres d'objet, l'organisateur, et les comparaisons.", "CustomStatTitle": "Total de statistique personnalisé", "Data": "Tableur", "DefaultItemSizeNote": "Une taille d'objet de 50px aura le rendu le plus net, sans flouter l'image de l'objet ou le texte.", "DontForgetDupes": "N'oubliez pas que vous pouvez rechercher is:dupe pour trouver rapidement les doublons, et vous pouvez utiliser l'outil de comparaison ou l'organisateur pour évaluer les objets en relation.", "EnableAdvancedStats": "Afficher les évaluation de stats sur les armures (D1)", "ExpandSingleCharacter": "Afficher tous les personnages", "ExportLoadoutSS": "Tableur des équipements", "ExportLoadoutSSHelp": "Télécharger une liste CSV de vos équipements DIM qui pourra être facilement visualisée dans l'application tableur de votre choix.", "ExportProfile": "Exporter la réponse de profil d'API", "ExportSS": "Tableur de l'inventaire", "ExportSSHelp": "Télécharger une liste CSV de vos objets qui peut être facilement visualisée dans l'app de votre choix.", "HidePullFromPostmaster": "Masquer le bouton \"$t(Loadouts.PullFromPostmaster)\"", "Inventory": "Affichage de l'inventaire", "InventoryColumns": "Nombre de colonne d'inventaire de personnage", "InventoryColumnsMobile": "Longueur de l'inventaire du personnage sur téléphone en mode portrait", "InventoryColumnsMobileLine2": "Les objets vont être redimensionnés pour s'adapter au nouveau paramètre", "InventoryNumberOfSpacesToClear": "Nombre d'espaces à conserver vides lors de l'utilisation du mode Farming", "Items": "Affichage des objets", "Language": "Langue", "LogOut": "Se déconnecter", "Masterworked": "Pièce Maîtresse", "MaxParallelCores": "Maximum cores for parallel tasks", "MaxParallelCoresExplanation": "Controls how many CPU cores DIM can use for intensive tasks like Loadout Optimizer and Loadout Analyzer. Higher values may improve performance but use more system resources.", "OrnamentDisplay": "Show Ornaments on item tiles", "OrnamentDisplayExplanationDisabled": "Items will never display their ornaments", "OrnamentDisplayExplanationEnabled": "Hovering or long-pressing armor will hide its ornament", "OrnamentDisplayExplanationHide": "Hovering or long-pressing an item will hide its ornament", "OrnamentDisplayExplanationShow": "Hovering or long-pressing an item will show its ornament", "ResetToDefault": "Réinitialiser", "RestoreVaultSide": "Show vaulted items in their own column", "ReverseSort": "Activer/désactiver le tri normal/inversé", "SetSort": "Trier les objets par :", "SetVaultWeaponGrouping": "Grouper les armes du coffre par :", "Settings": "Paramètres", "ShowNewItems": "Afficher un point rouge sur les nouveaux objets", "SingleCharacter": "Vue à un seul personnage", "SingleCharacterExplanation": "DIM ne montrera que le personnage le plus récemment joué.\nLes objets détenus par les personnages masqués apparaîtront dans le coffre, s'ils peuvent être utilisés par le personnage actuel.\nLes objets spécifiques aux autres classes seront complètement cachés.", "SizeItem": "Taille des objets", "SortByAmmoType": "Type de munitions", "SortByAmount": "Taille de la pile", "SortByClassType": "Classe requise", "SortByCrafted": "Façonné (D2)", "SortByDeepsight": "Souvenance (D2)", "SortByFeatured": "Nouveaux équipements / À la une (D2)", "SortByPrimary": "Niveau de puissance", "SortByRarity": "Rareté", "SortByRating": "Qualité de l'Armure (D1)", "SortByRecent": "Acquis récemment (D2)", "SortBySeason": "Saison (D2)", "SortByTag": "Étiquette ({{taglist}})", "SortByTier": "Palier (D2)", "SortByType": "Type", "SortByWeaponElement": "Type de dégâts", "SortCustom": "Tri personnalisé", "SortName": "Nom", "SpacesSize_one": "{{count}} espace", "SpacesSize_other": "{{count}} spaces", "Theme": "Thème", "Troubleshooting": "Troubleshooting", "VaultArmorGroupingStyle": "Séparez les armures sur différentes lignes en fonction de la classe", "VaultGroupingNone": "Aucun", "VaultUnder": "Show vaulted items under equipped items", "VaultWeaponGroupingStyle": "Séparer les groupes d'armes sur différentes lignes", "WeaponFrame": "Weapon Frame", "WishlistRefreshNotificationBody": "Si vous ne voyez aucuns changements, vérifiez que la source (par exemple GitHub) les reflète !", "WishlistRefreshNotificationTitle": "Listes de souhaits rechargées" }, "Sockets": { "ApplyPerks": "Appliquer les attributs", "GridStyle": "Afficher les attributs sous forme de grille", "Insert": { "Ability": "Équiper la capacité", "Aspect": "Insérer l'aspect", "Fragment": "Insérer le fragment", "Mod": "Insérer le mod", "Ornament": "Appliquer l'ornement", "Projection": "Appliquer la projection de spectre", "Shader": "Applique le revêtement", "Super": "Équiper le super", "Transmat": "Appliquer l'effet de téléportation" }, "ListStyle": "Afficher les attributs sous forme de liste", "Search": "Rechercher des noms ou des descriptions", "Select": { "Ability": "Prévisualiser la capacité", "Aspect": "Prévisualiser l'aspect", "Fragment": "Prévisualiser le fragment", "Mod": "Prévisualiser le mod", "Ornament": "Prévisualiser l'ornement", "Projection": "Prévisualiser la projection de spectre", "Shader": "Prévisualiser le revêtement", "Super": "Prévisualiser le super", "Transmat": "Prévisualiser l'effet de téléportation" }, "SelectWishlistPerks": "Aperçu des attributs de la liste de souhaits" }, "Stats": { "CrouchingSpeed": "Accroupi", "Custom": "Total Personnalisé", "CustomDesc": "Total personnalisé des statistiques de base sélectionnés, ignore les mods et les pièces maîtresses. Allez dans les paramètres pour configurer les statistiques inclues.", "DamageResistance": "Résistance aux dégats en JcE", "Discipline": "Discipline", "DropLevel": "Puissance du compte", "DropLevelExplanation1": "La puissance du compte est le niveau de puissance de base utilisé lors du calcul du niveau des récompenses.", "DropLevelExplanation2": "La puissance du compte utilise l'objet de plus haut niveau dans chaque emplacement, peu importe la classe ou la règle \"un seul exotique\".", "EquippableGear": "Équipement équipable", "FlinchResistance": "Résistance aux tremblements", "HP": "PV", "Intellect": "Intelligence", "MaxGearPower": "Puissance maximale des équipements pouvant être équipés", "MaxGearPowerAll": "Puissance maximale de tous les équipements", "MaxGearPowerOneExoticRule": "Puissance maximale de l'équipement équipable (seulement une pièce d'armure exotique équipée)", "MaxTotalPower": "Puissance totale maximale", "MetersPerSecond": "m/s", "Milliseconds": "ms", "NoBonus": "Pas de Bonus", "NotApplicable": "N/A", "OfMaxRoll": "{{range}} des stats parfaites", "PercentHelp": "Cliquez pour plus d'informations sur ce qu'est la qualité des stats.", "Percentage": "%", "PowerModifier": "Puissance accordée par la progression d'experience saisonnière", "Prestige": "Niveau de lumière: {{level}}\n{{exp}}xp jusqu'à avoir 5 particule de lumière.", "Quality": "Qualité des stats", "ShieldHP": "PV du Bouclier", "StrafingSpeed": "Déplacement latéral", "Strength": "Force", "TierProgress": "P{{tier}} {{statName}} ({{progress}}/60 pour P{{nextTier}})\n", "TierProgress_Max": "P{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "Temps avant PV entiers", "Total": "Total", "TotalHP": "PV Totaux", "WalkingSpeed": "Marche", "WeaponPart": "Pièce d'arme" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM peut maintenant stocker vos étiquettes, équipements, et paramètres sur nos propres serveurs et synchroniser ces données entre différentes versions de DIM, sans connexion additionnelle. Vous pouvez importer vos données existantes depuis la page Paramètres si vous n'aviez pas installé la Synchronisation DIM auparavant. Cela a été rendu possible par le soutiens de nos supporteurs OpenCollective!", "No": "Pas maintenant", "Title": "Activer la synchronisation DIM?", "Yes": "Activer la synchronisation" }, "AutoBackup": "Nous avons sauvegardé vos données dans un fichier appelé dim-data.json dans votre dossier de téléchargements.", "BackUpFirst": "Vous DEVEZ d'abord sauvegarder vos données avant de les supprimer. Au cas où.", "BrowserMayClearData": "Votre navigateur peut supprimer cette information si vous n'avez plus d'espace ou si vous ne visitez pas DIM fréquemment.", "DataIsLocal": "Les données d'étiquette et de notes sont seulement locales", "DeleteAllData": "Supprimer TOUTES les données des serveurs de synchronisation DIM", "DeleteAllDataConfirm": "Êtes-vous sûr de vouloir supprimer TOUTES vos données, pour tous les comptes, de la synchronisation DIM ? Vous ne pouvez pas annuler cette opération.", "Details": { "IndexedDBStorage": "Le stockage local ne sauvegardera votre information que sur ce navigateur. Effacer les données de votre navigateur supprimera cette information." }, "DimApiFinePrint": "DIM sauvegardera vos étiquettes, équipements, et paramètres sur les serveurs DIM et les synchronisera entre les différentes versions de DIM.", "DimSyncDown": "La synchronisation DIM n'est pas connectée à cause d'un problème de communication avec le serveur.", "DimSyncEnabled": "Synchronisation DIM activée", "DimSyncNotEnabled": "La synchronisation DIM n'est pas activée, donc vos paramètres, étiquettes, équipements, et les recherches ne sont stockées que localement et seront perdues si vous effacez le stockage de votre navigateur. Activez la synchronisation DIM dans les paramètres pour sauvegarder automatiquement vos données, ou sauvegardez régulièrement vos données manuellement.", "EnableDimApi": "Activer la synchronisation DIM (recommandé)", "Export": "Télécharger une sauvegarde de données", "ExportError": "Échec du téléchargement de la sauvegarde à partir de la synchronisation DIM", "ExportErrorBody": "La synchronisation DIM est peut-être en panne, ou vous avez des problèmes avec votre connexion. Nous allons télécharger une copie de vos données enregistrées localement.", "Import": "Importer une sauvegarde de données", "ImportConfirmDimApi": "Êtes vous sûr de vouloir écraser vos étiquettes, équipements et paramètres actuels avec cette version ? Cela remplacera complètement ce que vous aviez.", "ImportExport": "Sauvegarde & importation", "ImportFailed": "Échec de l'importation ! {{error}}", "ImportNoFile": "Aucun fichier sélectionné!", "ImportNotification": { "FailedBody": "Impossible d'import les données. {{error}}", "FailedTitle": "Échec de l'importation", "NoData": "Aucun équipements ou étiquettes trouvés dans la sauvegarde", "SuccessBodyForced": "{{loadouts}} équipements, {{tags}} étiquettes et vos paramètres ont été importés dans la Synchronisation DIM depuis votre sauvegarde, en remplaçant les données déjà présentes.", "SuccessBodyLocal": "{{loadouts}} équipements et {{tags}} étiquettes ont été importés de votre sauvegarde vers le stockage local en remplaçant ce qui y était déjà. Nous ne pouvons pas garantir que le stockage local ne sera pas perdu - pensez à activer la synchronisation DIM.", "SuccessTitle": "Importation réussie" }, "ImportTooManyFiles": "Veuillez ne sélectionnez qu'un seul fichier à importer.", "ImportWrongFileType": "Ce fichier n'est pas un fichier JSON. Il pourrait ne pas être une sauvegarde DIM.", "IndexedDBStorage": "Stockage local du navigateur", "LearnMore": "En savoir plus sur la synchronisation DIM", "MenuTitle": "Synchronisation & sauvegardes", "ProfileErrorBody": "Nous avons eu un problème de communication avec la synchronisation DIM. Vos derniers paramètres, étiquettes, équipements et recherches peuvent ne pas être affichés. Vos données sont toujours sur nos serveurs, et toutes les mises à jour que vous faites localement seront enregistrées lorsque nous pourrons nous reconnecter. Nous continuerons à réessayer pendant que DIM est ouvert.", "ProfileErrorTitle": "Erreur de téléchargement de la synchronisation DIM", "RefreshDimSync": "Recharger les données distantes depuis la synchronisation DIM", "UpdateErrorBody": "Nous avons rencontré un problème lors de l'enregistrement de vos données dans la synchronisation DIM. Nous continuerons à réessayer pendant que DIM est ouvert.", "UpdateErrorTitle": "Erreur de sauvegarde synchronisation DIM", "UpdateInvalid": "Échec de l'enregistrement des données vers DIM Sync", "UpdateInvalidBody": "Les données envoyées à DIM Sync n'étaient pas valides et ne seront pas enregistrées.", "UpdateInvalidBodyLoadout": "L'équipement «{{name}}» est invalide et ne sera pas sauvegardé. Si vous l'avez importé depuis un autre site, faites-leur savoir qu'ils exportent des équipements non valides.", "UpdateQueueLength_one": "{{count}} nouveau changement sera sauvegardé lorsque nous pourrons nous reconnecter.", "UpdateQueueLength_other": "{{count}} nouveaux changements seront sauvegardés lorsque nous pourrons nous reconnecter.", "Usage": "DIM utilise {{usage, humanBytes}} sur {{quota, humanBytes}} disponible pour lui sur cet appareil. Cela inclus la base de données d'objets de Destiny téléchargée depuis Bungie.net." }, "StreamDeck": { "Authorize": "Connecter l'application", "Enable": "Extension Stream Deck", "Error": { "Body": "There was an error sending data to the Stream Deck plugin. Please contact the plugin developer. {{error}}", "Title": "Stream Deck Plugin Error" }, "FinePrint": "Activer la connexion avec le plugin DIM Stream Deck. Ce plugin est un projet séparé qui n'est ni écrit ni supporté par l'équipe DIM.", "Install": "Installer le plugin", "MissingAuthorization": "Vous devez autoriser l'application Stream Deck à se connecter à DIM. Allez dans les paramètres et cliquez sur \"Connecter l'application\".", "Tooltip": { "Application": "Application Stream Deck", "AuthRequired": "Cliquez sur ce bouton ou allez dans les paramètres et cliquez sur \"Connecter l'application\".", "Error": "Votre extension Stream Deck n'est plus prise en charge. Veuillez mettre à jour vers la dernière version. Cette extension nécessite au moins :", "ErrorConnection": "si vous utilisez déjà la dernière version, vérifiez si une extension de navigateur bloque la connexion.", "ExtensionIssue": "Problème d'extension", "Plugin": "Extension", "Title": "Extension Stream Deck pour DIM", "Version": "Version :" } }, "StripSockets": { "Action": "Vider les emplacements", "ArmorMods": "{{count}}x Mod d'armure", "Button": "Vider {{numSockets}} emplacements", "Cancel": "Annuler", "Choose": "Choisissez les emplacements à vider", "DiscountedMods": "{{count}}x mods à prix réduis", "Done": "Emplacements vidés", "NoSockets": "Aucun emplacement à vider", "Ok": "Ok", "Ornaments": "{{count}}x Ornement", "Others": "{{count}}x Projection de spectre", "Running": "Vidage des emplacements", "Shaders": "{{count}}x Revêtement", "Subclass": "{{count}}x Option de doctrine", "WeaponMods": "{{count}}x Mod d'arme" }, "Tags": { "Archive": "Archive", "ClearTag": "Retirer l'étiquette", "Favorite": "Préféré", "Infuse": "Infuser", "Junk": "Camelote", "Keep": "Garder", "LockAll": "Verouiller les objets", "TagItem": "Étiqueter", "UnlockAll": "Déverrouillez les objets" }, "Triage": { "AccountsForArtifice": "Cela vérifie si une armure d'artifice pourrait être meilleure si un mod de statistique +3 était utilisé.", "BetterArmor": "Armure strictement meilleure", "BetterArtificeArmor": "Meilleure armure d'artifice", "BetterStatArmor": "Armure avec meilleures statistiques", "BetterStatArtificeArmor": "Armure d'artifice avec meilleures statistiques", "BetterWorseArmor": "Meilleure/Pire armure", "BetterWorseIncludes": "Identifie les pièces d'armure avec :", "HighStats": "Statistiques élevées", "InLoadouts": "Dans les équipements", "OwnedCount": "# Possédés", "PerkBetterArmorDesc": "Les mêmes, ou plus, attributs intrinsèques ou emplacement de mod spécial.", "PerkWorseArmorDesc": "Le même attribut intrinsèque, ou aucun.", "SimilarItems": "Objets similaires", "StatBetterArmorDesc": "Toutes les statistiques au moins aussi élevées, et au moins une meilleure statistique.", "StatNotPerkArmorDesc": "Cela ne vérifie que les statistiques. Une moins bonne pièce d'armure pourrait quand même avoir un emplacement de mod spécial ou un attribut intrinsèque.", "StatWorseArmorDesc": "Aucune meilleure statistique, et au moins une moins bonne statistique.", "ThisItem": "Cet objet", "WorseArmor": "Armure strictement moins bonne", "WorseArtificeArmor": "Armure non-artifice moins bonne", "WorseStatArmor": "Armure avec pires statistiques", "WorseStatArtificeArmor": "Armure d'artifice avec pires statistiques", "YourBestItem": "Votre meilleur objet" }, "Triumphs": { "GildingTriumph": "Triomphe doré", "HideCompleted": "Masquer les triomphes complétés", "RevealRedacted": "Afficher les triomphes masqués", "SortRecords": "Trier les triomphes par complétion" }, "Vendors": { "Collections": "Collections", "Engram": "Rang", "FilterToUnacquired": "Afficher uniquement les objets non récupérés", "HideSilverItems": "Masquer les objets en argentum", "NoItems": "Ce vendeur ne propose actuellement aucun objet.", "RefreshTime": "Actualisation de l'inventaire dans :", "Vendors": "Marchands" }, "Views": { "About": { "APIHistory": "Voir l'historique de toutes les actions effectués par DIM (et d'autre applications liées à Destiny)", "BungieCopyright": "Toutes les images et le contenu sont la propriété de Bungie.", "CommunityInsight": "Informations de la communauté sur les attributs et les statistiques, fournis par {{clarityLink}}. Si vous remarquez des imprécisions ou si vous avez des questions, rejoignez {{clarityDiscordLink}}.", "Discord": "Discord", "DiscordHelp": "Posez des questions, donnez des retours, et obtenez du support sur notre Discord.", "FAQ": "Questions fréquemment posés", "FAQAccess": "Comment est-ce que DIM a accès à mes données sur Destiny?", "FAQAccessAnswer": "Nous utilisation l'authentification de l'application Bungie pour permettre à DIM d'affiche et de déplacer vos objets. DIM ne voit jamais votre nom d'utilisateur ou votre mot de passe. Cela fonctionne de la même manière que l'application Compagnon.", "FAQKeyboard": "Est-ce que DIM supporte des raccourcis clavier?", "FAQKeyboardAnswer": "Oui! Appuyez sur \"?\" pour voir une liste de tout les raccourcis disponibles.", "FAQLogout": "Comment puis-je me déconnecter de DIM?", "FAQLogoutAnswer": "Ouvrez le menu à partir de l'icône en haut à gauche et choisissez \"Déconnexion\"", "FAQLostItem": "J'ai perdu mes objets en utilisant votre outil!", "FAQLostItemAnswer": "Bungie n'autorise pas les applications à supprimer les objets (même leur propre application!). C'est plus que probablement un transfert échoué, laissant alors votre objet dans votre coffre ou sur un autre personnage. Vous pouvez chercher cet objet. Si cela n'aide pas, rechargez la page. Regardez {{link}} ou dans le jeu pour voir si votre objet existe toujours. Nous sommes sure qu'il est toujours là.", "FAQMobile": "DIM supporte t'il les mobiles? Y aura t'il une application?", "FAQMobileAnswer": "Le site web DIM peut être chargé sur les téléphones et les tablettes, et vous pouvez l'ajouter à votre écran d'accueil pour une expérience similaire à celle d'une application.", "GitHub": "GitHub", "GitHubHelp": "Si vous souhaitez contribuer à ce projet, retrouvez-nous sur la page de notre projet sur {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM est une application gratuite et à code source ouvert créée par des développeurs communautaires en utilisant les mêmes services que ceux utilisés par Bungie.net et l'application compagnon de Destiny.", "Schedule": { "beta": "Cette version bêta de DIM est mise à jour à chaque fois que nous changeons le code - elle a les dernières fonctionnalités et corrections mais aussi les derniers bugs!", "release": "Cette version de DIM est mise à jour une fois par semaine, à approximativement minuit les Dimanche, heure du pacifique." }, "Translation": "Rejoignez l'équipe de traduction!", "TranslationText": "Nous utilisons {{link}} pour faciliter la traduction. Si vous souhaitez améliorer l'une des traductions de DIM, rejoignez l'équipe.", "Version": "Version {{version}} ({{flavor}}), built on {{date}}", "Wiki": "Mode d'emploi DIM", "WikiHelp": "Apprendre à utiliser les fonctionnalités de DIM." }, "Login": { "Auth": "Autoriser avec Bungie.net", "EnableDimSyncWarning": "Vous aviez précédemment désactivé la synchronisation \nDIM et n'utilisiez que le stockage de données local. L'activation de la synchronisation DIM remplacera toutes les données locales par les données de la synchronisation DIM. Vous devriez sauvegarder vos données avant d'activer la synchronisation DIM. Vous pouvez restaurer à partir de cette sauvegarde dans les Paramètres.", "Explanation": "Permettre à DIM de visualiser et de modifier vos personnages Destiny, coffre et sa progression.", "LearnMore": "En savoir plus sur les comptes et la connexion", "NewAccount": "Se connecter avec un autre compte Bungie.net", "Permission": "Nous avons besoin de votre permission..." }, "Support": { "BackersDetail": "Soutenez-nous avec un don unique ou mensuel et aidez-nous à poursuivre notre développement actif.", "FreeToDownload": "DIM est un produit gratuit au téléchargement et à l'utilisation. Le code source de DIM est open source et libre. Vous ne verrez jamais une pub sur DIM. C'est notre engagement.", "OpenCollective": "Nous utilisons {{link}} comme un service pour offrir une compensation à nos développeurs pour leur dévouement et leur temps consacré à ce projet.", "Store": "Nous avons des goodies avec notre logo et d'autres design en vente sur {{link}}", "Support": "Soutenir DIM" } }, "WishListRoll": { "BestRatedTip_one": "Cet attribut correspond exactement à une arme de votre liste de souhaits.", "BestRatedTip_other": "Ces attributs correspondent exactement à une arme de votre liste de souhaits.", "Clear": "Effacer la liste de souhaits", "CopiedLine": "Élément de liste de souhaits copié dans le presse-papier", "CopyLine": "Copier les attributs sélectionnés en tant qu'élément de liste de souhaits", "DupeRolls": " (+{{num, number}} doublons ignorés)", "ExternalSource": "Ajouter une autre liste de souhaits", "ExternalSourcePlaceholder": "Coller l'URL de la liste de souhaits ici", "Header": "Liste de souhaits", "Import": "Charger listes de souhaits", "ImportError": "Erreur lors du chargement de la liste de souhaits depuis \"{{url}}\" : {{error}}", "ImportFailed": "Aucune de vos listes de souhaits ne contenait d'armes valides.", "ImportNoFile": "Aucun fichier sélectionné.", "InvalidExternalSource": "Veuillez entrer une URL valide pour votre liste de souhaits externe. L'URL doit commencer par un des éléments suivants:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "Dernière mise à jour : {{lastUpdatedDate}} à {{lastUpdatedTime}}", "Num": "{{num, number}} objets dans votre liste de souhaits", "NumRolls": "{{num, number}} jets", "Refresh": "Rafraîchir la liste de souhaits", "SourceAlreadyAdded": "Liste de souhaits déjà ajoutée", "UpdateExternalSource": "Ajouter une liste de souhaits", "Voltron": "voltron (par défaut)", "WishListNotes": "Notes de la liste de souhaits :", "WorstRatedTip_one": "Cet attribut correspond exactement à une arme de votre liste d'indésirables.", "WorstRatedTip_other": "Ces attributs correspondent exactement à une arme de votre liste d'indésirables." }, "no-space": "no-space", "wrong-level": "wrong-level" } ================================================ FILE: src/locale/it.json ================================================ { "AWA": { "ConfirmDescription": "Si prega di utilizzare la Companion App di Destiny 2 per autorizzare DIM alla modifica degli oggetti.", "ConfirmTitle": "Conferma Azione", "Error": "Errore nel cambio di modifiche o peculiarità", "ErrorMessage": "Non abbiamo potuto equipaggiare {{plug}} in {{item}}.\n\n{{error}}", "FailedToken": "Impossibile ottenere permesso per cambiare l'oggetto", "IrreversiblePlugging": "Non possiedi {{plug}}, perciò non lo sovrascriveremo." }, "Accounts": { "Choose": "Profili per {{bungieName}}", "ErrorLoadInventory": "Impossible caricare i tuoi personaggi ed inventario di Destiny {{version}}", "ErrorLoadManifest": "Impossibile caricare il database informazioni di Destiny da Bungie", "ErrorLoading": "Impossibile caricare gli account di Destiny da Bungie.net", "MissingAccountWarning": "Se non vedi il tuo account qui, potresti non aver effettuato l'accesso all'account Bungie corretto, oppure Bungie.net potrebbe essere in manutenzione.", "MissingDescription": "L'account che stai cercando di visualizzare non è un account collegato al tuo profilo Bungie.net. Seleziona uno dei tuoi account qui sotto.", "MissingTitle": "Account non trovato", "NoCharacters": "Non possiedi personaggi di Destiny associati a questo account Bungie.net. Prova a connetterti con un account diverso.", "NoCharactersTitle": "Nessun personaggio trovato", "SwitchAccounts": "Puoi cambiare account successivamente dal menu nella testata.", "Title": "Account" }, "Activities": { "Activities": "Attività", "Hard": "Difficile", "Nightfall": "Assalto Cala la Notte", "Normal": "Normale", "WeeklyHeroic": "Assalto Eroico Settimanale" }, "Armory": { "AlternateItems": "Versione alternative", "Armory": "Armeria", "DifferentSeason": "Riproposta da una stagione diversa", "NoNotes": "Nessuna Nota", "OpenInArmory": "mostra in Armeria", "Season": "Stagione {{season}}, Anno {{year}}", "TrashlistedRolls_one": "Roll Lista degli Scarti", "TrashlistedRolls_other": "{{count, number}} Roll Lista degli Scarti", "Unknown": "Oggetto Sconosciuto", "UnknownPerkHash": "L'hash della peculiarità {{hash}} ({{perkName}}) non compare su questo oggetto, quindi questo roll della Lista Desideri non è valido. Contatta l'autore della Lista Desideri per correggerlo. Nota che le Liste Desideri dovrebbero sempre specificare la versione non potenziata delle peculiarità.", "WishlistedRolls_one": "Roll Lista dei Desideri", "WishlistedRolls_other": "{{count, number}} Roll Lista dei Desideri", "YourItems": "I tuoi oggetti" }, "Browsercheck": { "Samsung": "Samsung Internet può rendere i siti troppo scuri quando la modalità scura è attiva. Abilita in Impostazioni > Labs > Utilizza tema scuro oppure passa a un altro browser.", "Steam": "Il browser dell'overlay di Steam è molto vecchio e alcune o tutte le funzionalità di DIM potrebbero non funzionare. Non possiamo fornire supporto.", "Unsupported": "Il team di DIM non supporta l'utilizzo di questo browser. Alcune o tutte le funzioni di DIM potrebbero non funzionare." }, "Bucket": { "Armor": "Armatura", "Class": "Sottoclasse", "General": "Generale", "Ghost": "Spettro", "Inventory": "Inventario", "Postmaster": "Amministratori", "Progress": "Progresso", "Reputation": "Reputazione", "Unknown": "Sconosciuto", "Vault": "Deposito", "Weapons": "Armi" }, "BulkNote": { "Append": "Aggiungi alle note / aggiungi #hashtags", "Confirm": "Aggiorna note", "Remove": "Rimuovi dalle note / rimuovi #hashtags", "Replace": "Sostituisci note", "Title_one": "Modificate note su 1 oggetto", "Title_other": "Modificate note su {{count}} oggetti" }, "BungieAlert": { "Title": "Un messaggio da Bungie:" }, "BungieService": { "AppNotPermitted": "DIM non è autorizzato ad effettuare questa operazione.", "DestinyCannotPerformActionAtThisLocation": "Non puoi equipaggiare o cambiare modifiche mentre sei in un'attività. Prova ad andare in orbita o in un'area social. Questa è una limitazione delle API di Bungie.net, non di DIM.", "DestinyItemUnequippable": "Non puoi equipaggiare questo oggetto. Se l'ultima attività di questo personaggio ha bloccato il suo equipaggiamento, prova ad accedere di nuovo al personaggio.", "DestinyLegacyPlatform": "I servizi di Bungie al momento hanno un bug che impedisce a DIM di caricare le informazioni del tuo account di Destiny 2 se hai giocato a Destiny 1 su una console di vecchia generazione. Bungie lo risolverà presto, ma fino ad allora, dovrai giocare Destiny 1 su una console della generazione attuale per accedere ai tuoi dati.", "DevVersion": "Stai utilizzando una versione di DIM per sviluppatori? Devi registrare la tua estensione per Chrome su Bungie.net.", "Difficulties": "Bungie.net sta avendo dei problemi al momento.", "ErrorTitle": "Errore di Bungie.NET", "ItemUniquenessExplanation": "Un personaggio può avere soltanto un '{{name}}'.", "Maintenance": "I server di Bungie.net sono offline per manutenzione.", "MissingInventory": "Bungie.net non ha restituito il tuo inventario, forse perché le tue impostazioni di privacy lo impediscono. Prova ad effettuare nuovamente l'accesso.", "NetworkError": "Errore Network - {{status}} {{statusText}}", "NoAccount": "Non è stato trovato alcun account di Destiny. Sei sicuro di aver selezionato la piattaforma corretta?", "NoAccountForPlatform": "Impossibile trovare un account Destiny per te su {{platform}}.", "NotConnected": "Potresti non essere connesso a internet.", "NotConnectedOrBlocked": "Potresti non essere connesso ad internet, oppure una estensione per blocco pubblicità o privacy potrebbe bloccare Bungie.net.", "NotLoggedIn": "Si prega di autorizzare DIM per poter utilizzare questa applicazione.", "Slow": "Bungie.net è rallentato in questo momento", "SlowDetails": "Bungie.net sta impiegando molto tempo per inviarci le tue informazioni. Questo può succedere quando molti giocatori sono in gioco contemporaneamente, oppure Bungie.net sta avendo problemi. Potresti anche avere problemi di connessione. Continueremo ad attendere una risposta.", "SlowResponse": "Bungie.net ha impiegato troppo tempo per rispondere.", "Throttled": "Bungie.net sta limitando il numero di richieste che DIM può fare.", "Twitter": "Ottieni informazioni sullo stato del servizio su:", "UnknownError": "Messaggio Bungie.net: {{message}}", "VendorNotFound": "I dati dei mercanti non sono disponibili." }, "Compare": { "Archetype": "Archetipo", "AssumeMasterworked": "Presupponi prodigiosi", "AssumeMasterworkedDescription": "Statistiche se il Prodigio fosse al massimo, senza le modifiche attuali", "BaseStatsDescription": "Statistiche di base, senza Prodigio o modifiche", "Button": "Confronta", "ButtonHelp": "Confronta Oggetti", "CompareBaseStats": "Mostra statistiche di base", "CurrentStats": "Statistiche attuali", "CurrentStatsDescription": "Statistiche attuali, compreso modifiche e livello Prodigio", "Error": { "Invalid": "Non ci sono oggetti validi per il confronto.", "Unmatched": "Impossibile confrontare oggetti di tipo diverso." }, "InitialItem": "Questo è l'oggetto dal quale è stato lanciato lo Strumento di Confronto", "IsVendorItem": "Questo oggetto non è nel tuo inventario, ma {{vendorName}} lo vende.", "NoModArmor": "Pre-modifiche" }, "Cooldown": { "Grenade": "Recupero delle granate: {{cooldown}}", "Melee": "Recupero del corpo a corpo: {{cooldown}}", "Super": "Recupero della super: {{cooldown}}" }, "Countdown": { "Days_compact_one": "{{count}}gg", "Days_compact_other": "{{count}}gg", "Days_one": "1 Giorno", "Days_other": "{{count}} Giorni" }, "Csv": { "EmptyFile": "Non c'erano righe nel file.", "ImportConfirm": "Sei sicuro di voler importare etichette/note dal CSV? Questo sovrascriverà le etichette/note di tutti gli oggetti nel tuo foglio elettronico.", "ImportFailed": "Impossibile importare etichette/note dal CSV: {{error}}", "ImportSuccess_one": "Etichette/note caricate per un oggetto.", "ImportSuccess_other": "Etichette/note caricate per {{count}} oggetti.", "ImportWrongFileType": "Il file non è un file CSV.", "WrongFields": "Il CSV deve avere le colonne 'Id', 'Notes', 'Tags' e 'Hash'." }, "Dialog": { "Cancel": "Cancella", "OK": "OK" }, "EnergyMeter": { "Energy": "Energia", "Unused": "Inutilizzata", "UpgradeNeeded": "La capacità energetica di questo oggetto è {{energyCapacity}}. Per alloggiare la Modifica selezionata, la sua capacità energia deve essere {{energyUsed}}.", "Used": "Utilizzata" }, "ErrorBoundary": { "Title": "Qualcosa è andato storto" }, "ErrorPanel": { "BrowserTooOld": "Il tuo browser è troppo vecchio per utilizzare DIM. Aggiorna il tuo browser all'ultima versione.", "BrowserTooOldTitle": "Browser incompatibile", "Description": "Prova a visualizzare il tuo inventario sulla Companion App di Destiny 2 per vedere se Bungie.net sta funzionando.", "ReadTheGuide": "Leggi la nostra Guida Utente (link nel menu) per la risoluzione problemi.", "SystemDown": "Questo influisce su tutte le app di Destiny, e il team di DIM non può né risolverlo né aggirarlo.", "Troubleshooting": "Guida alla risoluzione dei problemi" }, "FarmingMode": { "D2Desc_female_one": "DIM sta impedendo agli oggetti di finire dagli Amministratori assicurandosi che ci sia sempre uno spazio libero per tipo di oggetto sulla {{store}}.", "D2Desc_female_other": "DIM sta impedendo agli oggetti di finire dagli Amministratori assicurandosi che ci siano sempre {{count}} spazi liberi per tipo di oggetto sul {{store}}.", "D2Desc_male_one": "DIM sta impedendo agli oggetti di finire dagli Amministratori assicurandosi che ci sia sempre uno spazio libero per tipo di oggetto sul {{store}}.", "D2Desc_male_other": "DIM sta impedendo agli oggetti di finire dagli Amministratori assicurandosi che ci siano sempre {{count}} spazi liberi per tipo di oggetto sul {{store}}.", "D2Desc_one": "DIM sta impedendo agli oggetti di finire dagli Amministratori assicurandosi che ci sia sempre uno spazio libero per tipo di oggetto sul {{store}}.", "D2Desc_other": "DIM sta impedendo agli oggetti di finire dagli Amministratori assicurandosi che ci siano sempre {{count}} spazi liberi per tipo di oggetto sul {{store}}.", "Desc_female_one": "DIM sta spostando Engrammi e Lumen dalla {{store}} nel Deposito e tenendo uno spazio vuoto per tipo di oggetto per evitare che qualcosa finisca dagli Amministratori.", "Desc_female_other": "DIM sta spostando Engrammi e Lumen dal {{store}} nel Deposito e tenendo {{count}} spazi vuoti per tipo di oggetto per evitare che qualcosa finisca dagli Amministratori.", "Desc_male_one": "DIM sta spostando Engrammi e Lumen dal {{store}} nel Deposito e tenendo uno spazio vuoto per tipo di oggetto per evitare che qualcosa finisca dagli Amministratori.", "Desc_male_other": "DIM sta spostando Engrammi e Lumen dal {{store}} nel Deposito e tenendo {{count}} spazi vuoti per tipo di oggetto per evitare che qualcosa finisca dagli Amministratori.", "Desc_one": "DIM sta spostando Engrammi e Lumen dal {{store}} nel Deposito e tenendo uno spazio vuoto per tipo di oggetto per evitare che qualcosa finisca dagli Amministratori.", "Desc_other": "DIM sta spostando Engrammi e Lumen dal {{store}} nel Deposito e tenendo {{count}} spazi vuoti per tipo di oggetto per evitare che qualcosa finisca dagli Amministratori.", "FarmingMode": "Modalità Farming", "FarmingModeNote": "(mantiene spazio per i drop)", "MakeRoom": { "Desc": "DIM sposta solo gli Engrammi e i consumabili per i Lumen dal {{store}} al deposito o agli altri personaggi per evitare che qualcosa finisca dagli Amministratori.", "Desc_female": "DIM sposta solo gli Engrammi e i consumabili per i Lumen dalla {{store}} al deposito o agli altri personaggi per evitare che qualcosa finisca dagli Amministratori.", "Desc_male": "DIM sposta solo gli Engrammi e i consumabili per i Lumen dal {{store}} al deposito o agli altri personaggi per evitare che qualcosa finisca dagli Amministratori.", "MakeRoom": "Fai spazio per poter raccogliere oggetti, spostando l'equipaggiamento", "Tooltip": "Se selezionato, DIM sposterà armi e equipaggiamento per creare spazio per gli engrammi nel deposito." }, "OutOfRoom": "Non hai più spazio per spostare oggetti da {{character}}. È il momento di fare pulizia!", "OutOfRoomTitle": "Spazio esaurito", "Stop": "Stop", "Vault": "Sposterà gli oggetti nel Deposito per fare spazio." }, "FashionDrawer": { "Accept": "Salva stile", "CannotFitOrnament": "Questo oggetto non ha un alloggiamento per decori oppure non hai decori per esso.", "CannotFitShader": "Questo oggetto non può usare uno shader", "ClearOrnaments": "Cancella Decori", "ClearOrnamentsTitle": "Reimposta tutti i decori su \"nessuna preferenza\"", "ClearShaders": "Cancella Shader", "ClearShadersTitle": "Reimposta tutti gli shader su \"nessuna preferenza\"", "NoPreference": "Nessuna preferenza - questo alloggiamento non verrà cambiato", "Reset": "Cancella stile", "Sync": "Sincronizza", "SyncOrnaments": "Sincronizza Decori", "SyncOrnamentsTitle": "Usa i decori dello stesso set su tutti gli oggetti, se sono sbloccati", "SyncShaders": "Sincronizza Shader", "SyncShadersTitle": "Usa lo stesso shader su tutti gli oggetti", "Title": "Scegli shader e decori", "UseEquipped": "Usa lo stile equipaggiato" }, "FileUpload": { "Instructions": "Clicca o trascina i files" }, "Filter": { "Adept": "\\(affinata\\)", "AmmoType": "Mostra gli oggetti basandosi sul tipo di munizioni.", "Armor": "Mostra gli oggetti che sono armature.", "Armor3": "Mostra gli oggetti che utilizzano il sistema di statistiche Armatura 3.0 introdotto con I Confini del Destino.", "ArmorCategory": "Mostra le armature ordinate per categoria.", "ArmorIntrinsic": "Mostra le armature leggendarie che hanno una caratteristica intrinseca, come l'Armatura dell'Artificio.", "Artifice": "Mostra armatura dell'Artificio.", "Ascended": "Mostra gli oggetti che hanno un potenziamento \"ascensione\" che sono stati ascesi.", "Breaker": "Filtra per tipo di anti-campioni o per tipo di campione corrispondente. breaker:intrinsic mostra gli oggetti con abilità anti-campioni intrinseche.", "BulkClear_one": "Etichetta rimossa da 1 oggetto.", "BulkClear_other": "Etichetta rimossa da {{count}} oggetti.", "BulkRevert_one": "Etichetta ripristinata su 1 oggetto.", "BulkRevert_other": "Etichetta ripristinata su {{count}} oggetti.", "BulkTag_one": "Etichettato l'oggetto selezionato come {{tag}}.", "BulkTag_other": "Etichettati i {{count}} oggetti selezionato come {{tag}}.", "Catalyst": "Mostra i catalizzatori basandosi sul loro stato. catalyst:complete mostra i catalizzatori che hai completato e inserito, catalyst:incomplete mostra i catalizzatori che hai sbloccato ma non hai ancora completato l'obbiettivo oppure non hai ancora inserito, e catalyst:missing mostra gli oggetti che possono avere un catalizzatore ma non è ancora stato trovato.", "Class": "Mostra gli oggetti in base alla loro affinità di classe.", "Combine": "I filtri possono essere combinati o raggruppati con delle parentesi, \"or\" e \"and\" per restringere la ricerca, ad esempio \"{{example}}\".", "ContributePower": "Mostra gli oggetti che hanno Potere e possono contribuire al tuo livello Potere.", "Cosmetic": "Mostra gli oggetti di stile o cosmetici.", "Craftable": "Mostra gli oggetti che sono forgiabili.", "CraftedDupe": "Mostra le armi duplicate dove almeno uno dei duplicati è forgiato.", "Curated": "Mostra gli oggetti che hanno un roll curato.", "CurrentClass": "Mostra tutti gli oggetti che sono equipaggiabili sul Guardiano attualmente in uso.", "CustomStatLower": "Mostra le armature le quali statistiche sono strettamente più basse di un'altra armatura dello stesso tipo, contando solo le statistiche che sono in qualunque totale personalizzato della lista di quella classe di statistiche armatura.", "DamageType": "Mostra gli oggetti basandosi sul tipo di danno.", "Deepsight": "Mostra le armi con Risonanza Profonda, quelle con il modello estratto, o che possono avere la Risonanza Profonda attivata utilizzando un Armonizzatore della Vista Profonda.", "Deprecated": "Questo filtro non è più supportato.", "Description": "Descrizione", "DescriptionFilter": "Mostra gli oggetti la cui descrizione ha una corrispondenza parziale con il testo del filtro. Cerca intere frasi utilizzando le virgolette.", "DisabledModSlot": "Mostra gli oggetti con una modifica disabilitata.", "Dupe": "Mostra gli oggetti duplicati, compresi quelli ri-emessi", "DupeArchetype": "Raggruppa le armature con lo stesso archetipo di statistiche.", "DupeCount": "Oggetti che hanno un numero specifico di duplicati.", "DupeLower": "Oggetti duplicati, compresi quelli ri-emessi, che non sono il duplicato col Potere più alto. Soltanto un duplicato viene scelto come il più alto, gli altri vengono considerati più bassi.", "DupePerks": "Mostra gli oggetti le quali peculiarità sono duplicate o un sottoinsieme di un altro oggetto dello stesso tipo.", "DupeSetBonus": "Raggruppa le armature con lo stesso bonus set.", "DupeStats": "Mostra le armature con statistiche di base identiche e modifiche di regolazione statistiche corrispondenti come Artificio o di calibrazione.", "DupeTertiary": "Raggruppa le armature con la stessa statistica terziaria.", "DupeTraits": "Armi le quali caratteristiche sono duplicate o un sottoinsieme di un'altra arma dello stesso tipo.", "DupeTunedStat": "Raggruppa le armature con la stessa statistica calibrata.", "DupeUntunedStats": "Raggruppa le armature con statistiche di base identiche, ignorando le modifiche di aggiustamento statistiche.", "DupeZeroStats": "Raggruppa le armature con le stesse 3 statistiche di base non-zero.", "Energy": "Mostra gli oggetti che utilizzano il sistema di modifiche Armatura 2.0 introdotto in Ombre dal Profondo.", "EnergyCapacity": "Mostra gli oggetti basandosi sulla loro capacità energetica attuale.", "Engrams": "Mostra engrammi.", "Enhanceable": "Mostra le armi che posso essere potenziate.", "Enhanced": "Mostra le armi in base al loro grado di potenziamento.", "EnhancedPerk": "Mostra le armi che hanno il numero specificato di colonne con peculiarità migliorate.", "EnhancementReady": "Mostra le armi che hanno raggiunto le soglie di livello per il miglioramento delle peculiarità.", "Equipment": "Oggetti che possono essere equipaggiati.", "Equipped": "Oggetti che sono al momento equipaggiati su un personaggio.", "Event": "Mostra oggetti in base a quale evento di Destiny 2 sono comparsi.", "ExtraPerk": "Mostra le armi leggendarie con roll casuali con una peculiarità selezionabile aggiuntiva.", "Featured": "Oggetti che contano come uno dei \"Nuovi equipaggiamenti\" o \"Oggetti in evidenza\" nella stagione attuale.", "Filter": "Filtro", "FilterWith": "Filtra con:", "Focusable": "Mostra gli oggetti che possono essere concentrati da un mercante", "Foundry": "Mostra gli oggetti in base alla fonderia che li ha creati.", "Glimmer": "Mostra gli oggetti consumabili che sono relativi al guadagno di Lumen.", "Harrowed": "\\(Tormentata\\)", "HasNotes": "Mostra gli oggetti che hanno delle note.", "HasOrnament": "Mostra gli oggetti sui quali è applicato un decoro.", "HasShader": "Mostra oggetti che hanno uno shader applicato.", "Holofoil": "Mostra le armi con ololamina.", "InDimLoadout": "is:indimloadout mostra gli oggetti che sono inclusi in qualsiasi dotazione su DIM.", "InInGameLoadout": "is:iningameloadout mostra gli oggetti che sono in qualsiasi dotazione in gioco.", "InInventory": "Mostra gli oggetti di cui hai almeno una copia nel tuo inventario. Utile soltanto nelle schermate dei mercanti e record.", "InLoadout": "is:inloadout mostra gli oggetti che sono inclusi in qualsiasi dotazione. Cercare con inloadout: mostra gli oggetti che sono inclusi in dotazioni con titoli corrispondenti. Quando usato con un hashtag, inloadout: mostra gli oggetti le quali dotazioni hanno l'hashtag nel titolo o nelle note. Quando usato con un intervallo, mostra gli oggetti che sono compresi entro quel dato numero di dotazioni.", "Infusable": "Mostra gli oggetti che possono essere infusi.", "InfusionFodder": "Mostra gli oggetti che potrebbero essere infusi in versioni con Potere più basso dello stesso oggetto usando solo lumen.", "IsAdept": "Mostra le armi compatibili con le modifiche dell'Adepto.", "IsCrafted": "Mostra le armi che sono state forgiate.", "ItemHash": "Mostra gli oggetti con l'hash dell'oggetti dell'inventario specificato. Per utenti avanzati.", "ItemId": "Mostra gli oggetti con l'ID dell'oggetto dell'inventario specificato. Per utenti avanzati.", "Leveling": { "Complete": "{{term}} - mostra gli oggetti che sono completi - con ogni potenziamento sbloccato.", "Incomplete": "{{term}} - mostra gli oggetti che non sono completi - c'è ancora almeno un potenziamento da sbloccare.", "NeedsXP": "{{term}} - mostra gli oggetti ai quali manca ancora XP.", "Upgraded": "{{term}} - mostra gli oggetti che hanno abbastanza XP per sbloccare tutti i potenziamenti, ma questi non sono ancora stati sbloccati.", "XPComplete": "{{term}} - mostra gli oggetti che non possono ricevere XP (sia che i potenziamenti siano sbloccati o no)." }, "Location": "Mostra gli oggetti basandosi sulla loro posizione all'interno dell'app. left/middle/right è una indicazione visiva del personaggio, e mentre inleftchar funzionerà sempre, le altre due dipendono da quanti personaggi hai. current è il tuo ultimo/attuale personaggio connesso (segnato con un triangolo giallo).", "LockAllFailed": "Impossibile bloccare gli oggetti", "LockAllSuccess": "Bloccati {{num}} oggetti", "Locked": "Mostra gli oggetti basandosi sullo stato del loro blocco.", "Masterwork": "Mostra gli oggetti basandosi sulla loro statistica o livello di prodigio.", "MasterworkKills": "Mostra gli oggetti in base al loro contatore uccisioni prodigioso.", "MaxPower": "Mostra gli oggetti al livello di Potere più alto per ogni alloggiamento.", "MaxPowerLoadout": "Mostra gli oggetti nella dotazione che potrebbero massimizzare il tuo livello di Potere per ciascun personaggio.", "Memento": "Mostra le armi che hanno un alloggiamento per memento.", "ModSlot": "Mostra le armature con uno specifico tipo di alloggiamento modifiche.", "Mods": { "Y3": "Mostra gli oggetti con qualsiasi modifica equipaggiata." }, "Name": "Mostra gli oggetti il cui nome corrisponde (exactname:) o corrisponde parzialmente (name:) al testo del filtro. Cerca intere frasi utilizzando le virgolette.", "NamedStat": "Mostra armature che hanno punti nella statistica specificata.", "Negate": "Per negare una ricerca, utilizzare un segno meno oppure la parola \"not\" come prefisso, ad esempio\"{{notexample}}\" o \"{{notexample2}}\".", "NewItems": "Mostra i nuovi oggetti.", "Notes": "Cerca gli oggetti che hai etichettato con note personalizzate.", "OriginTrait": "Mostra le armi che hanno una peculiarità originale.", "Ornament": "Mostra gli oggetti con decori e li filtra in base allo stato.", "PartialMatch": "Mostra gli oggetti nei quali il nome, descrizione, qualsiasi peculiarità, o qualsiasi modifica ha una corrispondenza parziale al testo nel filtro. Cerca intere frasi utilizzando le virgolette.", "PatternUnlocked": "Mostra gli oggetti che hanno un modello di forgiatura sbloccato, anche se l'oggetto non è stato forgiato.", "Perk": "Mostra gli oggetti in cui una delle peculiarità o modifiche ha una corrispondenza parziale con il testo del filtro nel nome o descrizione. Cerca intere frasi usando le virgolette.", "PerkName": "Mostra gli oggetti con una peculiarità o una modifica il cui nome corrisponde (exactperk:) o corrisponde in parte (perkname:) al testo del filtro. Cerca intere frasi utilizzando apostrofi.", "PinnacleReward": "Mostra gli incarichi che portano ad una ricompensa di punta.", "Postmaster": "Oggetti che sono dagli Amministratori.", "PowerKeywords": "Utilizza le parole chiave \"pinnaclecap\" o \"softcap\" invece di un numero per riferirti al livelli limite di Potere della stagione attuale.", "PowerLevel": "Mostra gli oggetti in base al loro livello Potere. $t(Filter.PowerKeywords)", "PowerfulReward": "Mostra gli incarichi che portano ad una ricompensa potente.", "PrismaticDamageType": "Mostra gli oggetti in base al tipo di danno da Luce od Oscurità. Quelli della Luce sono Arco, Solare e Vuoto. Quelli dell'Oscurità sono Stasi e Telascura.", "Quality": "Mostra gli oggetti basandosi sulla percentuale totale della qualità delle statistiche. '{{quality}}' può essere usato al posto di '{{percentage}}'.", "RandomRoll": "Mostra gli oggetti che droppano con peculiarità casuali.", "RarityTier": "Mostra gli oggetti basandosi sulla loro rarità.", "Reforgeable": "Mostra gli oggetti che possono essere riforgiati dall'Armaiolo.", "Release": "Mostra gli oggetti disponibili da una specifica release o evento.", "RequiredLevel": "Mostra gli oggetti in base al livello richiesto.", "RetiredPerk": "Mostra le armi con peculiarità non più ottenibili.", "SearchPrompt": "Cerca tra comandi per filtri disponibili", "Season": "Mostra oggetti in base a quale stagione di Destiny 2 sono comparsi.", "StackFull": "Mostra gli oggetti che sono alla massima capacità delle loro pile. (Nuclei ottimizzanti, Strane Monete, Materiali dell'Armaiolo ecc)", "StackLevel": "Mostra gli oggetti basandosi sulla quantità delle loro pile.", "Stackable": "Mostra gli oggetti accumulabili (sintesi munizioni, strane monete, ecc)", "StatLower": "Mostra le armature le cui statistiche sono rigorosamente più basse di un'altra dello stesso tipo.", "Stats": "Mostra gli oggetti basandosi su un valore di statistica specifico. $t(Filter.StatsExtras)", "StatsBase": "Filtra le armature basandosi sul loro valore di statistiche di base, senza contare le modifiche inserite o prodigio. $t(Filter.StatsExtras)", "StatsExtras": "Supporta l'aggiunta di statistiche connettendo più nomi di statistiche con i simboli + e &. Ci sono anche le parole chiave speciali highest, second highest, thirdhighest, ecc con corrispondenza alle statistiche basandosi sul loro grado all'interno delle statistiche di un oggetto. Ogni statistica personalizzata ha anche il suo termine di ricerca specifico, mostrato nella sezione delle impostazioni Etichette personalizzate.", "StatsLoadout": "Trova una serie di oggetti da equipaggiare per massimizzare il valore di una statistica in particolare.", "StatsMax": "Trova armature con i valori più alti per una statistica specifica. Include tutti gli oggetti con il valore più alto.", "StatsOrdinal": "Trova le armature 3.0 con la concentrazione di statistiche specificate.", "Tags": { "Tag": "Mostra gli oggetti con un'etichetta specifica.", "Tagged": "Mostra gli oggetti che hanno una qualsiasi etichetta." }, "Tier": "Mostra gli oggetti in base al loro grado da 0 a 5.", "Timelost": "\\(perduta nel tempo\\)", "Tracked": "Mostra le imprese/taglie basandosi sul loro stato monitorato.", "Transferable": "Oggetti che possono essere spostati tra i personaggi.", "Trashlist": "Mostra gli oggetti che corrispondono alla lista degli scarti della tua Lista Desideri.", "TunedStat": "Mostra gli oggetti con modifiche di calibrazione per la statistica specificata.", "Unascended": "Mostra gli oggetti che hanno un potenziamento \"ascensione\" che non sono stati ascesi.", "Undo": "Annulla", "UnlockAllFailed": "Impossibile sbloccare oggetti", "UnlockAllSuccess": "Sbloccati {{num}} oggetti", "Vendor": "Articolo disponibile da un mercante specifico.", "VendorItem": "L'oggetto proviene da un mercante, non dal tuo inventario. Utile per escludere gli oggetti dei mercanti dall'Ottimizzatore di Dotazioni.", "Weapon": "Mostra gli oggetti che sono armi.", "WeaponLevel": "Mostra le armi in base al loro Livello Arma.", "WeaponType": "Mostra le armi basandosi sul tipo di arma.", "Wishlist": "Mostra gli oggetti che corrispondono alla tua Lista Desideri.", "WishlistDupe": "Mostra gli oggetti duplicati dove almeno un duplicato è nella tua Lista Desideri.", "WishlistEnabled": "Mostra gli oggetti che sono idonei per avere roll della Lista Desideri.", "WishlistNotes": "Mostra oggetti della Lista Desideri con le note corrispondenti alla ricerca.", "WishlistUnknown": "Mostra gli oggetti senza roll consigliati nella Lista Desideri caricata.", "Year": "Mostra gli oggetti divisi per anno di Destiny in cui sono apparsi." }, "General": { "ClickForDetails": "Clicca per dettagli", "Close": "Chiudi", "Confirm": "Confermi?", "UserGuideLink": "Guida utente" }, "Glyphs": { "Axe": "Ascia", "DarkAbility": "Abilità dell'Oscurità", "Gilded": "Indorato", "Harmonic": "Armonico", "HiveSword": "Spada dell'Alveare", "LightAbility": "Abilità della Luce", "LightLevel": "Livello Potere", "Misadventure": "Disavventura", "Missing": "Mancante", "OpenSymbolsPicker": "Apri Selettore Simboli", "Prismatic": "Prismatica", "Quickfall": "Caduta Rapida", "RespawnRestricted": "Rientro Limitato", "ScorchCannon": "Cannone Rovente", "SearchSymbols": "Cerca Simboli...", "Smoke": "Fumogeno" }, "Header": { "About": "Chi siamo", "AutoRefresh": "DIM si ricaricherà automaticamente finché stai ancora giocando.", "BulkTag": "Etichetta oggetti in massa", "BungieNetAlert": "Avviso da Bungie", "Clear": "Cancella filtro di ricerca", "CompareMatching": "Confronta Oggetti", "DeleteSearch": "Elimina Ricerca", "FilterHelp": "Cerca oggetto/peculiarità, {{example}}, e altro", "FilterHelpBrief": "Cerca oggetti", "FilterHelpLoadouts": "Cerca nomi dotazioni e note", "FilterHelpMenuItem": "Aiuto per Filtri...", "FilterHelpOptimizer": "Filtra le armature incluse nelle dotazioni, ad esempio: {{example}}", "FilterHelpProgress": "Cerca pietre miliari e taglie", "FilterHelpRecords": "Cerca trionfi e collezioni", "FilterMatchCount_one": "1 oggetto", "FilterMatchCount_other": "{{count}} oggetti", "Filters": "Filtri", "InstallDIM": "Installa come App", "InstallDIMBanner": "Installa DIM come app sulla tua schermata iniziale", "Inventory": "Inventario", "IosPwaPrompt": "Su Safari, clicca l'icona \"condividi\" (il bottone centrale in basso) e seleziona \"Aggiungi a schermata Home\".", "KeyboardShortcuts": "Scorciatoie da tastiera", "LaunchDIMAlone": "Finestra Separata", "MaterialCounts": "Inventario Materiali", "Menu": "Menù", "ProfileAge": "Dati aggiornati inviati dai server di Destiny {{age}} fa.\nAggiornare da DIM potrebbe farti ricevere dati più aggiornati, ma Bungie.net potrebbe inviarti informazioni dalla cache.", "Refresh": "Aggiorna i dati di Destiny [R]", "ReloadApp": "Ricarica App", "ReportBug": "Segnala un bug", "SaveSearch": "Salva Ricerca", "SearchActions": "Apri azioni per ricerca", "SearchResults": "Mostra Oggetti", "Shop": "Negozio", "TagAs": "Etichetta come '{{tag}}'", "UpgradeDIM": "Aggiorna DIM", "WhatsNew": "Novità" }, "Help": { "CannotMove": "Non posso spostare quell'oggetto da questo personaggio.", "NoStorage": "DIM non può salvare dati", "NoStorageMessage": "DIM non può memorizzare dati nel tuo browser. Questo può succedere se stai navigando in modalità privacy o in incognito, o quando hai poco spazio su disco, oppure un bug del browser. Prova a riavviare il computer! Non riuscirai a connetterti o usare DIM fino a quando non avrai risolto." }, "Hotkey": { "Armory": "Mostra l'Armeria per un oggetto", "CheatSheetTitle": "Scorciatoie da tastiera:", "ClearDialog": "Chiudi", "ClearNewItems": "Pulisci nuovi oggetti", "Enter": "INVIO", "ItemPopupTab": "Cambia scheda dettagli oggetto", "LockUnlock": "Blocca o sblocca un oggetto", "MarkItemAs": "Contrassegna oggetto come '{{tag}}'", "Menu": "Attiva/Disattiva menù", "Note": "Inserisci note", "Pull": "Invia l'oggetto al personaggio attivo", "RefreshInventory": "Aggiorna l'inventario", "ShowHotkeys": "Mostra scorciatoie da tastiera", "StartSearch": "Inizia una ricerca", "StartSearchClear": "Inizia una ricerca", "Tab": "TAB", "Vault": "Invia l'oggetto al deposito" }, "InGameLoadout": { "ClearSlot": "Cancella alloggiamento {{index}}", "Create": "Crea dotazione", "CreateTitle": "Crea dotazione in gioco dall'equipaggiamento attuale", "CurrentlyEquipped": "Attualmente equipaggiato", "DeleteFailed": "Eliminazione dotazione non riuscita", "Deleted": "Dotazione eliminata", "DeletedBody": "Eliminata la dotazione in gioco allo slot {{index}}", "EditFailed": "Aggiornamento dotazione non riuscito", "EditIdentifiers": "Modifica identificatori", "EditTitle": "Modifica nome e icona dotazione", "EquipNotReady": "Equipaggiamento in gioco non pronto", "EquipReady": "Equipaggiamento in gioco pronto", "LoadoutDetails": "Dettagli dotazione", "MatchingLoadouts": "Dotazioni corrispondenti:", "PrepareEquip": "Prepara equipaggiamento", "Replace": "Sostituisci dotazione {{index}}", "Save": "Aggiorna dotazione", "SaveIdentifiers": "Aggiorna identificatori", "SnapshotFailed": "Salvataggio equipaggiamento in uso non riuscito" }, "Infusion": { "Filter": "Filtra oggetti", "InfuseSource": "Seleziona gli oggetti in cui infondere {{name}}", "InfuseTarget": "Seleziona oggetto da infondere in {{name}}", "InfusionMaterials": "Materiali per Infusione", "NoItems": "Nessun oggetto disponibile per l'Infusione.", "NoTransfer": "Trasferimento materiale d'Infusione\n{{target}} non può essere spostato.", "SwitchDirection": "Cambia", "TransferItems": "Trasferisci" }, "Inventory": { "ClickToExpand": "(Clicca per espandere)", "MissingSilver": "Il tuo saldo Argento è disponibile solo mentre stai giocando." }, "Item": { "SetBonus": { "NPiece_one": "{{count}} pezzo", "NPiece_other": "{{count}} pezzi" }, "ThumbsDown": "Pollici in giù", "ThumbsUp": "Pollici in su" }, "ItemFeed": { "ClearFeed": "Cancella elenco", "Description": "Oggetti Recenti", "HideTagged": "Nascondi etichettati", "NoNewItems": "Nessun nuovo oggetto", "ShowOlderItems": "Mostra oggetti più vecchi" }, "ItemMove": { "Consolidate": "{{name}} consolidati", "Distributed": "{{name}} distribuiti\n {{name}} sono ora suddivisi in modo equo tra i personaggi.", "MovingItem": "Trasferimento al deposito", "MovingItem_female": "Trasferimento a {{target}}", "MovingItem_male": "Trasferimento a {{target}}", "ToStore": "Tutti i {{name}} sono ora sul tuo {{store}}.", "ToVault": "Tutti i {{name}} sono ora nel deposito." }, "ItemPicker": { "ChooseItem": "Scegli un oggetto:", "SearchPlaceholder": "Cerca oggetti" }, "ItemService": { "BucketFull": { "Guardian": "Il {{store}} ha troppe '{{itemtype}}'.", "Guardian_female": "Il {{store}} ha troppe '{{itemtype}}'.", "Guardian_male": "Il {{store}} ha troppe '{{itemtype}}'.", "Vault": "Il {{store}} ha troppe '{{itemtype}}'." }, "Classified": "Questo oggetto è nascosto e non può essere trasferito attualmente.", "Classified2": "Oggetto nascosto. Bungie non fornisce ancora informazioni su questo oggetto. Aggiungi delle note a questo oggetto ed utilizza il filtro di ricerca \"notes:\" per trovarlo.", "Deequip": "Impossibile trovare un altro oggetto da equipaggiare per rimuovere {{itemname}}", "ExoticError": "'{{itemname}}' non può essere equipaggiato, poiché l'esotico nello slot {{slot}} non può essere rimosso. ({{error}})", "NotEnoughRoom": "Non c'è nulla che possiamo spostare dal {{store}} per fare spazio per {{itemname}}", "NotEnoughRoomGeneral": "Non c'è abbastanza spazio per spostare questo oggetto.", "OnlyEquippedClassLevel": "Questo oggetto può essere equipaggiato solo su un {{class}} al livello {{level}} o superiore.", "OnlyEquippedLevel": "Questo oggetto può essere equipaggiato solamente su personaggi al livello {{level}} o superiore.", "PostmasterAlmostFull": "Quasi pieno!", "PostmasterFull": "Pieno!", "PreviewVendor": "Anteprima contenuto {{type}}", "StackFull": "Hai già una pila piena di {{name}}", "StoreName": "{{className}} {{genderRace}}" }, "KillType": { "ClassAbilities": "Abilità di classe", "Finisher": "Mossa finale", "Grenade": "Granata", "Melee": "Corpo a corpo", "Precision": "Precisione", "Super": "Super" }, "LB": { "AddStack": "Aggiungi un'altra copia di questa modifica", "AdvancedOptions": "Opzioni Avanzate", "ChooseAMod": "Scegli le tue modifiche", "ChooseASetBonus": "Scegli i tuoi bonus set", "ChooseAnExotic": "Scegli la tua esotica", "ClearLocked": "Pulisci Bloccati", "ContainsVendorItems": "Questo equipaggiamento contiene oggetti dei mercanti", "Current": "Corrente", "Equip": "Equipaggia su {{character}}", "Exclude": "Oggetti esclusi", "ExcludeHelp": "Shift + click su un oggetto (o trascina e rilascia in questo riquadro) per creare dei set senza delle specifiche armature.", "ExistingBuildStats": "Statistiche build esistenti", "ExistingBuildStatsNote": "Mostra solo le build con statistiche rigorosamente più alte.", "FilterSets": "Filtra i set", "Help": { "And": "Le armature che presentano tutte queste caratteristiche verranno utilizzate (\"and)", "ChangeNodes": "All'interno del gioco cambia i nodi di Intelletto, Disciplina o Forza che sono mostrati, per creare ogni dotazione.", "Discipline": "La Disciplina riduce il tempo di ricarica delle Granate", "DragAndDrop": "Trascina e rilascia un oggetto nel suo riquadro per costruire un set con quella specifica armatura", "Help": "Hai bisogno di aiuto?", "HigherTiers": "Utilizzare Gradi più alti è meglio", "Intellect": "L'Intelletto riduce il tempo di ricarica della Super", "Lock": "Blocca una serie di peculiarità cliccando uno dei riquadri soprastanti e seleziona le peculiarità", "MultiPerk": "Per utilizzare un'armatura con più caratteristiche fai Shift + click sulle caratteristiche desiderate", "NoPerk": "Se una caratteristica non è visibile, significa che non possiedi alcuna armatura con quella caratteristiche", "Or": "Le armature con qualsiasi di queste caratteristiche verranno utlizzare (\"or\")", "ShiftClick": "Shift + click per escludere un oggetto dal set", "StatsIncrease": "Quando la difesa di un oggetto aumenta, aumentano anche le sue statistiche (intelletto/disciplina/forza).", "Strength": "La Forza riduce il tempo di ricarica del Corpo a Corpo", "Synergy": "Cerca di trovare armature con caratteristiche che aumentino le munizioni per i tipi di armi che vuoi utilizzare.", "Tier11Example": "4/5/2 (una build Tier 11) corrisponde a 4 Intelletto, 5 Disciplina, 2 Forza (4+5+2 = Tier 11)" }, "HideAllConfigs": "Nascondi tutte le configurazioni", "HideConfigs": "Nascondi configurazioni", "IncompatibleWithOptimizer": "Questo oggetto non è compatibile con l'Ottimizzatore. Riacquisisci una nuova versione dalle Collezioni.", "LB": "Ottimizzatore di Dotazioni", "LightMode": { "HelpCurrent": "Calcola le dotazioni con i livelli di difesa attuali.", "HelpScaled": "Calcola le dotazioni, supponendo che tutti gli oggetti abbiano 350 di difesa.", "LightMode": "Tema chiaro" }, "Loading": "Caricando i migliori set", "LockEquipped": "Blocca Equipaggiati", "LockPerk": "Blocca peculiarità", "Locked": "Oggetti Bloccati", "LockedHelp": "Trascina e rilascia un qualsiasi oggetto nel suo riquadro per costruire un set con quella specifica armatura. Shift + click per escludere oggetti.", "Missing2": "Manca un raro, un leggendario o un esotico per costruire un set completo!", "ProcessingMode": { "Fast": "Veloce", "Full": "Completa", "HelpFast": "Controlla solo tra il tuo equipaggiamento migliore.", "HelpFull": "Controlla tra più elementi del tuo equipaggiamento, ma impiega più tempo.", "ProcessingMode": "Procedura" }, "RemoveStack": "Rimuovi una copia di questa modifica", "Scaled": "In Scala", "SearchAMod": "Cerca una Modifica per nome o descrizione", "SearchASetBonus": "", "SearchAnExotic": "Cerca una esotica per nome o descrizione", "SelectExotic": "Seleziona esotica", "SelectMods": "Seleziona modifiche", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Modifiche Attività", "SelectSetBonus": "Seleziona bonus set", "SelectSubclassOptions": "Personalizza sottoclasse", "ShowAllConfigs": "Mostra tutte le configurazioni", "ShowConfigs": "Mostra configurazioni", "ShowGear": "Armatura per {{class}}", "Vendor": "Includi oggetti dei Mercanti" }, "Loading": { "Accounts": "Caricamento accounts di Destiny...", "Code": "Caricamento codice di DIM...", "FilterHelp": "Caricamento consigli per filtri ricerca...", "Profile": "Caricamento del profilo di Destiny...", "Vendors": "Caricamento dei mercanti di Destiny..." }, "LoadoutAnalysis": { "Analyzed": "Analizzate {{numLoadouts}} dotazioni", "Analyzing": "Analizzate {{numAnalyzed}}/{{numLoadouts}} dotazioni", "BetterStatsAvailable": { "Description": "Scegliere un'armatura diversa per questa dotazione consentirà di raggiungere statistiche più alte. Scegli \"$t(Loadouts.OpenInOptimizer)\" per vedere delle build migliori.", "Name": "Migliori statistiche ottenibili" }, "BetterStatsAvailableFontNote": "Nota: questa dotazione utilizza modifiche \"Fonte di...\" che inducono il superamento di 200 punti di una statistica. DIM può identificare statistiche migliori riducendo il numero di punti in eccesso. Se questo fosse indesiderato, disabilita \"$t(Loadouts.IncludeRuntimeStatBenefits)\" nella dotazione.", "DoesNotRespectExotic": { "Description": "Le impostazioni dell'Ottimizzatore di Dotazioni per questa dotazione specifica la scelta di un'esotica, ma la dotazione non corrisponde con quell'esotica.", "Name": "Esotica errata" }, "DoesNotSatisfyStatConstraints": { "Description": "Le impostazioni dell'Ottimizzatore di Dotazioni per questa dotazione specificano delle statistiche minime, ma la dotazione non le raggiunge.", "Name": "Statistiche minime errate" }, "EmptyFragmentSlots": { "Description": "Ci sono alloggiamenti vuoti per frammenti in questa sottoclasse.", "Name": "Alloggiamenti frammenti vuoti" }, "InvalidMods": { "Description": "Alcune modifiche in questa dotazione sono obsolete o non possono adattarsi in nessun pezzo della tua armatura.", "Name": "Modifiche obsolete" }, "InvalidSearchQuery": { "Description": "Questa dotazione è stata creata con una query di ricerca non valida nell'Ottimizzatore di Dotazioni.", "Name": "Query di ricerca non valida" }, "ItemsDoNotMatchSearchQuery": { "Description": "Questa dotazione è stata creata con una query di ricerca nell'Ottimizzatore di Dotazioni, e quella query di ricerca esclude almeno uno degli oggetti nella dotazione.", "Name": "Ricerca esclude oggetti" }, "MissingItems": { "Description": "Alcuni degli oggetti in questa dotazione non sono più nel tuo inventario.", "Name": "Oggetti mancanti" }, "ModsDontFit": { "Description": "L'armatura in questa dotazione non può ospitare tutte le modifiche della dotazione, anche se l'armatura fosse stata potenziata.", "Name": "Modifiche non assegnate" }, "NeedsArmorUpgrades": { "Description": "Le armature in questa dotazione devono essere potenziate per alloggiare tutte le modifiche o per raggiungere le statistiche specificate.", "Name": "Richiede potenziamento armatura" }, "NotAFullArmorSet": { "Description": "Questa dotazione non può essere analizzata ulteriormente perché non include un set completo di armature.", "Name": "Set armature incompleto" }, "TooManyFragments": { "Description": "Ci sono più frammenti configurati nella sottoclasse che quelli consentiti dagli aspetti.", "Name": "Troppi frammenti" }, "UsesSeasonalMods": { "Description": "Questa dotazione si basa su modifiche che sono disponibili solo in alcune stagioni. Quando la stagione finisce, alcune modifiche non saranno disponibili o supereranno la capacità energetica dell'armatura.", "Name": "Utilizza modifiche stagionali" } }, "LoadoutBuilder": { "All": "Tutto", "AlwaysAutoMods": "Artificio e modifiche di calibrazione saranno sempre scelti automaticamente.", "AnyExotic": "Qualunque Esotica", "AnyExoticDescription": "I set devono contenere una esotica, ma qualunque esotica va bene.", "Artifice": "Artificio", "AssumeMasterwork": "Presupponi prodigiosi", "AssumeMasterworkOptions": { "All": "Tutte le armature: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "Tutte le armature: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nArmature esotiche 2.0: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Aggiornata per accettare modifiche dell'artificio.", "Current": "Statistiche attuali, livello energetico presunto almeno {{minLoItemEnergy}}.", "Legendary": "Leggendarie:\n$t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nEsotiche:\n$t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Bonus alle statistiche per prodigio completo, presupposto livello energia almeno 10.", "None": "Tutte le armature:\n$t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "Aggiungi modifiche statistiche automaticamente", "AutomaticallyPicked": "Questa Modifica è stata aggiunta automaticamente per migliorare le statistiche della build.", "CompareLoadout": "Confronta dotazioni", "ConfirmOverwrite": "Sei sicuro di voler sostituire l'armatura nella dotazione \"{{name}}\" con questo nuovo set di armature?", "DecreaseStatPriority": "Diminuisci la priorità della statistica", "DisabledByAutoStatMods": "Le Modifiche alle statistiche vengono scelte automaticamente dall'Ottimizzatore di Dotazioni.", "DisabledDueToMaintenance": "L'Ottimizzatore di Dotazioni è al momento disabilitato a causa della manutenzione delle API di Bungie.", "EquipItems": "Usa", "ExcludeItem": "Escludi Oggetto", "ExcludeVendors": "Cerca \"not:vendor\" per escludere gli oggetti dei mercanti dall'Ottimizzatore di Dotazioni.", "ExcludedItems": "Oggetti esclusi", "ExistingLoadout": "Dotazione Esistente", "Exotic": "Armatura Esotica", "ExoticClassItemPerks": "Se vuoi peculiarità specifiche, utilizza ricerche come exactperk:\"spirito della verità\". Clicca sulle peculiarità nei risultati dell'Ottimizzatore per aggiungerle o rimuoverle dal filtro oggetti.", "ExoticSpecialCategory": "Speciale", "FOTLWildcardWarning": "Questo set contiene una maschera della Festa delle Anime Perdute. Applica manualmente la modifica corretta per attivare il bonus set desiderato.", "Filter": "Impostazioni", "IgnoreStat": "Se non selezionata, l'Ottimizzatore di Dotazioni farà finta che questa statistica non esista quando genererà i set", "IncreaseStatPriority": "Aumenta la priorità della statistica", "Legendary": "Leggendaria", "LimitToNewFeaturedGear": "Limita a equipaggiamento nuovo/in evidenza", "LockItem": "Blocca oggetto", "MissingClass": "Build per: {{className}}", "MissingClassDescription": "La build che stai cercando di visualizzare è per una classe di personaggio che non hai.", "MwExotic": "Esotica", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "Una query di ricerca attiva sta limitando gli oggetti che DIM può includere nelle build", "AllowAutoStatMods": "Consenti a DIM di includere automaticamente ulteriori modifiche delle statistiche", "AlwaysInvalidMods": "Queste modifiche non si adattano a nessuno dei tuoi oggetti:", "AssumeMasterworked": "Consenti a DIM di raccomandarti armature da portare al massimo livello prodigio", "AssumptionsRestricted": "DIM non può raccomandarti dei cambiamenti al tipo di energia dell'armatura:", "BadSlot": "Nell'alloggiamento {{bucketName}}, nessun oggetto di quelli consentiti poteva alloggiare queste modifiche:", "ExoticDoesNotExist": "Non possiedi nessuna delle armature esotiche selezionate nel tuo inventario.", "Header": "Nessuna build trovata. Queste sono le possibili ragioni per cui DIM non ha trovato nessuna build:", "LowerBoundsFailed": "Molti set non soddisfacevano i requisiti minimi delle statistiche", "MaybeAllowMoreItems": "Considera la possibilità di permettere altri oggetti:", "MaybeDecreaseLowerBounds": "Considera di abbassare i requisiti minimi delle statistiche", "MaybeRemoveMods": "Considera di spostare queste modifiche:", "MaybeRemoveSearchQuery": "Considera di cancellare o cambiare il filtro nella barra di ricerca", "ModAssignmentFailed": "Molti set non potevano alloggiare tutte le modifiche richieste", "RemoveMods": "Rimuovi queste modifiche", "RemoveSetBonuses": "Considera di rimuovere alcuni bonus set", "SetBonuses": "Hai scelto alcuni bonus set, forse non hai gli oggetti giusti per utilizzarli." }, "NoExotic": "Nessuna Esotica", "NoExoticDescription": "Equivale a cercare \"not:exotic\" nella barra di ricerca - i set non useranno armature esotiche.", "NoExoticPreference": "Nessuna esotica selezionata", "NoExoticPreferenceDescription": "Le armature esotiche saranno utilizzate se massimizzeranno le statistiche.", "NoLoadoutsToCompare": "Nessuna dotazione da confrontare", "None": "Nessuno", "OptimizerExplanationGuide": "Leggi la Guida Utente per maggiori informazioni e un video tutorial.", "OptimizerExplanationMods": "Scegli un'esotica, modifiche e una sottoclasse.\nEsse contribuiranno alle statistiche della dotazione, mentre qualsiasi modifica già sulle armature verrà ignorata.", "OptimizerExplanationSearch": "Utilizza la barra di ricerca per restringere l'insieme di armature da considerare, ad esempio {{example}}. Se nessuna armatura in uno slot corrisponde alla ricerca, tutti gli oggetti verranno considerati per quello slot.", "OptimizerExplanationStats": "Trascina le statistiche più importanti in alto, e disabilita quelle che non vuoi massimizzare.", "OptimizerSet": "Set Ottimizzatore", "PinnedItems": "Oggetti fissati", "PinnedItemsFinePrint": "I filtri di ricerca sono salvati con le impostazioni dell'Ottimizzatore di Dotazioni, ma gli oggetti bloccati o esclusi no. I blocchi e le esclusioni verranno ignorati quando DIM controllerà le dotazioni esistenti per build con statistiche migliori.", "ProcessingSets": "Ricerca dei set con statistiche più alte...", "SaveAs": "Salva come", "SetBonus": "Bonus set", "SpeedReport": "Controllate {{combos, number}} combinazioni in {{time}} secondi utilizzando {{cpus}} core della CPU.", "StatConstraints": "Priorità e limiti statistiche", "StatMax": "Max", "StatMin": "Min", "StatRangeTooltip": "Con le attuali impostazioni min/max, esistono dotazioni che hanno da {{min}} a {{max}} punti in questa statistica. Fai doppio click per impostare il minimo a {{max}}.", "StatTotal": "Totale: {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "Impossibile aggiungere tutte le modifiche.", "UnableToAddAllModsBody": "Non c'erano abbastanza spazi disponibili per installare {{mods}}.", "UnlockItem": "Sblocca oggetto" }, "LoadoutFilter": { "Contains": "Mostra le dotazioni che hanno un oggetto o una modifica che corrisponde al testo del filtro. Cerca oggetti con spazi nel nome usando le virgolette.", "FashionOnly": "Mostra le dotazioni che contengono solo cosmetici (shader o decori).", "LoadoutLight": "Mostra le dotazioni basandosi sul livello di Potere calcolato. Utilizza le parole chiave pinnaclecap o softcap invece di un numero per fare riferimento ai limiti di Potere della stagione attuale.", "ModsOnly": "Mostra le dotazioni che contengono solo modifiche armatura.", "Name": "Mostra le dotazioni il cui nome corrisponde (exactname:) o corrisponde parzialmente (name:) al testo del filtro. Cerca intere frasi utilizzando le virgolette.", "Notes": "Cerca dotazioni con il loro campo note.", "PartialMatch": "Mostra dotazioni il cui nome o note hanno una corrispondenza parziale al testo del filtro. Cerca intere frasi usando le virgolette.", "Season": "Mostra dotazioni in base a quale stagione di Destiny 2 sono state modificate per l'ultima volta.", "Subclass": "Mostra le dotazioni il cui nome di sottoclasse o tipo di danno corrisponde parzialmente al testo del filtro." }, "Loadouts": { "Abilities": "Abilità", "Actions": "Azioni per {{title}}", "AddEquippedItems": "Aggiungi Equipaggiati", "AddNotes": "Aggiungi note", "AddUnequippedItems": "Aggiungi non equipaggiati", "Any": "Qualsiasi classe", "Apply": "Applica", "ApplyInGameLoadoutInGame": "La tua dotazione è pronta per essere equipaggiata ma dato che sei in una attività devi equipaggiarla in gioco.", "ApplyMods": "Applicazione modifiche", "ApplySearch": "Trasferisci la ricerca \"{{query}}\"", "ArmorStats": "Statistiche armatura", "ArtifactUnlocks": "Sblocchi Artefatto", "ArtifactUnlocksDesc": "A causa delle limitazioni di Bungie.net, DIM non può configurare automaticamente il tuo Artefatto. Devi eseguire questi sblocchi nel gioco prima di applicare la Dotazione.", "ArtifactUnlocksWithSeason": "Sblocchi Artefatto – S{{seasonNumber}}", "BadLoadoutShare": "Impossibile caricare dotazione condivisa", "BadLoadoutShareBody": "La dotazione che stai cercando di caricare non è valida: {{error}}", "Before": "Prima '{{name}}'", "CancelEditing": "Annula modifica", "CannotCustomizeSubclass": "Questa sottoclasse non può essere configurata", "ChooseItem": "Aggiungi {{name}}", "ClassType": "Dotazione per ogni classe", "ClassTypeMismatch": "Un oggetto per {{className}} non può essere aggiunto a questa dotazione", "ClassTypeMissing": "Non hai un oggetto da {{className}} per il quale creare una dotazione", "ClassType_female": "Dotazione {{className}}", "ClassType_male": "Dotazione {{className}}", "Classified": "Alcuni dei tuoi oggetti sono nascosti, e non possono essere usati nel calcolo per il massimo Potere.", "ClearLoadoutParameters": "Rimuovi impostazioni Ottimizzatore di Dotazioni", "ClearSection": "Rimuovi tutto", "ClearSpace": "Sposta via gli altri", "ClearSpaceArmor": "Sposta via le altre armature", "ClearSpaceWeapons": "Sposta via le altre armi", "ClearUnsetMods": "Rimuovi altre modifiche", "ClearingSpace": "Spostando via gli altri oggetti", "CopyAndEdit": "Copia e modifica", "Create": "Crea dotazione", "CurrentlyEquipped": "Attualmente equipaggiato", "Deequip": "Togliendo oggetti dagli altri personaggi", "Delete": "Elimina", "DimLoadouts": "Dotazioni DIM", "Edit": "Modifica dotazione", "EditBrief": "Modifica", "EquipInGameLoadout": "Equipaggiamento dotazione in gioco", "EquipItems": "Equipaggiando oggetti", "EquippableDifferent1": "Sono stati utilizzati più esotici per calcolare il tuo Potere massimo, quindi il numero mostrato potrebbe non essere raggiungibile equipaggiando gli oggetti in gioco.", "EquippableDifferent2": "Il Potere massimo non è limitato alla regola del \"una sola esotica\" quando viene determinato il Potere dei tuoi drop/potenti/ricompense di punta.", "Failed": "Dotazione non applicata completamente", "Fashion": "Scegli lo stile", "FashionOnly": "Solo Stile", "FillFromEquipped": "Riempi utilizzando gli equipaggiati", "FillFromInventory": "Riempi utilizzando i non equipaggiati", "FilteredItems": "Oggetti Filtrati", "FindAnother": "Trova un altro {{name}}", "FromEquipped": "Equipaggiato", "Generated": "{{statTotal}} Punti statistiche dotazione", "HashtagTip": "Consiglio: Usa gli #hashtag nel nome della tua dotazione e verranno mostrati qui.", "Import": { "BadURL": "URL di condivisione dotazione non valido.", "Error": "Errore caricamento dotazione:", "Error404": "Questa dotazione non esiste.", "PasteHere": "Incolla un collegamento di una dotazione per aprirlo." }, "ImportLoadout": "Importa dotazione", "InGameActions": "Azioni per dotazione in gioco", "InGameLoadouts": "Dotazioni in gioco", "IncludeRuntimeStatBenefits": "Includi statistiche modifiche Fonte", "IncludeRuntimeStatBenefitsDesc": "Le modifiche \"Fonte di...\" forniscono un aumento fisso delle statistiche del personaggio mentre possiedi cariche di armatura.\nCon questa impostazione, DIM considera queste modifiche attive e aggiunge i loro benefici alle statistiche di questa dotazione nei calcoli e nelle ottimizzazioni.", "ItemErrorSummary_one": "1 errore oggetto:", "ItemErrorSummary_other": "{{count}} errori oggetto:", "ItemLeveling": "Elementi da Livellare", "LoadoutName": "Nome dotazione", "LoadoutParameters": "Impostazioni Ottimizzatore di Dotazioni", "LoadoutParametersExotic": "La dotazione deve includere questa esotica: {{exoticName}}", "LoadoutParametersQuery": "Gli oggetti devono corrispondere a questo filtro", "LoadoutParametersStats": "Priorità statistiche e intervalli minimi/massimi", "Loadouts": "Dotazioni", "MakeRoom": "Crea spazio per gli Amministratori", "MakeRoomDone_female_one": "Ho terminato di creare spazio per 1 oggetto dell'Amministratore togliendo 1 oggetto dalla {{store}}.", "MakeRoomDone_female_other": "Ho terminato di creare spazio per {{count}} oggetti dell'Amministratore togliendo {{movedNum}} oggetti dalla {{store}}.", "MakeRoomDone_male_one": "Ho terminato di creare spazio per 1 oggetto dell'Amministratore togliendo 1 oggetto dalla {{store}}.", "MakeRoomDone_male_other": "Ho terminato di creare spazio per {{count}} oggetti dell'Amministratore togliendo {{movedNum}} oggetti dalla {{store}}.", "MakeRoomDone_one": "Ho terminato di creare spazio per 1 oggetto dell'Amministratore togliendo 1 oggetto dalla {{store}}.", "MakeRoomDone_other": "Ho terminato di creare spazio per {{count}} oggetti dell'Amministratore togliendo {{movedNum}} oggetti dalla {{store}}.", "MakeRoomError": "Impossibile creare spazio per tutti gli oggetti degli Amministratori: {{error}}.", "ManageLoadouts": "Gestisci dotazioni", "MaxSlots": "Puoi avere soltanto {{slots}} {{bucketName}} in una dotazione.", "MaximizeLight": "Potere Max", "MaximizePower": "Potere Massimo", "MaximizeStat": "Massimizza Statistica", "MissingItemsWarning": "Alcuni degli oggetti in questa dotazione non sono più nel tuo inventario.", "ModErrorSummary_one": "1 errore modifica:", "ModErrorSummary_other": "{{count}} errori modifica:", "ModPlacement": { "InvalidMods": "Modifica non valida", "InvalidModsDesc_one": "1 modifica non può essere alloggiata in alcun pezzo di armatura.", "InvalidModsDesc_other": "{{count}} modificche non possono essere alloggiate in alcun pezzo di armatura.", "ModPlacement": "Posizionamento modifiche", "StackableMod": "Cumulabile", "UnassignedMods": "Modifiche non assegnate", "UnassignedModsDesc_one": "1 modifica non può essere alloggiata per insufficienza di capacità energetica o di alloggiamenti modifiche. Potenziare l'energia dell'armatura selezionata non risolverà il problema.", "UnassignedModsDesc_other": "{{count}} Modifiche non possono essere alloggiate per insufficienza di capacità energetica o di alloggiamenti Modifiche. Potenziare l'energia dell'armatura selezionata non risolverà il problema.", "UnstackableMod": "Non cumulabile", "UpgradeCosts": "Costi potenziamento", "UpgradeCostsDesc": "Alcune armature necessitano di potenziamento alla capacità energetica per alloggiare le modifiche richieste. In totale, questi sono i costi di potenziamento:" }, "Mods": "Modifiche", "ModsOnly": "Solo modifiche", "MoveItems": "Spostando oggetti", "NoSpace": "Non hai più spazio nel Deposito e nei personaggi.", "NoneMatch": "Nessuna delle tue dotazioni corrisponde ai filtri.", "NotStarted": "In attesa del completamento di altre azioni, o la fine del caricamento di un aggiornamento dell'inventario", "NotesPlaceholder": "Scrivi alcune note riguardo questa dotazione, oppure usa gli #hashtag per categorizzarla", "NotificationTitle": "Dotazione: {{name}}", "OnWrongCharacterAdvice": "Clicca qui per trovare gli oggetti con il più alto Potere di questo personaggio.", "OnWrongCharacterWarning": "L'armatura più potente di questo personaggio è su un altro. Per contare nel Livello Potere delle ricompense, potenti e punta, l'armatura deve essere su questo personaggio o nel Deposito.", "OnlyItems": "Solo oggetti, che si possono equipaggiare, materiali e consumabili si possono aggiungere ad una dotazione.", "OpenInOptimizer": "Ottimizza Armatura", "OpenOnStreamDeck": "Apri su Stream Deck", "PickArmor": "Scegli armatura", "PickMods": "Aggiungi modifiche armatura", "Prismatic": { "Aspect": "Natura prismatica", "Grenade": "Granata prismatica", "Melee": "Corpo a corpo prismatico", "Super": "Super abilità" }, "PullFromPostmaster": "Prendi dagli Amministratori", "PullFromPostmasterError": "Impossibile prendere dagli Amministratori: {{error}}.", "PullFromPostmasterGeneralError": "Impossibile spostare tutti gli oggetti dagli Amministratori.", "PullFromPostmasterNotification_female_one": "Spostamento di 1 oggetto dagli Amministratori a {{store}}.", "PullFromPostmasterNotification_female_other": "Spostamento di {{count}} oggetti dagli Amministratori a {{store}}.", "PullFromPostmasterNotification_male_one": "Spostamento di 1 oggetto dagli Amministratori a {{store}}.", "PullFromPostmasterNotification_male_other": "Spostamento di {{count}} oggetti dagli Amministratori a {{store}}.", "PullFromPostmasterNotification_one": "Spostamento di 1 oggetto dagli Amministratori a {{store}}.", "PullFromPostmasterNotification_other": "Spostamento di {{count}} oggetti dagli Amministratori a {{store}}.", "PullFromPostmasterPopupTitle": "Prendi dagli Amministratori", "Random": "Casuale", "Randomize": "Dotazione casuale", "RandomizeButton": "Randomizza", "RandomizeNew": "Crea casuale", "RandomizeQueryHint": "Consiglio: Cerca prima gli oggetti per limitare quelli che possono essere scelti casualmente.", "RandomizeSearch": "Randomizza da Ricerca", "RandomizeSearchPrompt": "Randomizzare i tuoi oggetti equipaggiati da ricerca\"{{query}}\"?", "Redo": "Ripeti", "RestoreAllItems": "Tutti gli Elementi", "SalvationsEdgeMods": "Modifiche dell'Orlo della Salvezza", "Save": "Salva", "SaveAsDIM": "Salva come dotazione DIM", "SaveAsNew": "Salva come Nuovo", "SaveAsNewTooltip": "Mantieni la dotazione originale e salva questa come nuova dotazione", "SaveDisabled": { "AlreadyExists": "Scegli un nuovo nome per la dotazione.", "Empty": "La dotazione è vuota.", "NoName": "La dotazione ha bisogno di un nome." }, "SaveLoadout": "Salva dotazione", "Season": "Stagione {{season}}", "SetBonusesDesc": "Bonus set richiesti", "Share": { "Copied": "Collegamento alla dotazione copiato negli appunti", "CopyButton": "Copia Collegamento", "Error": "Errore ottenimento del collegamento di condivisione", "Fashion": "Stile (shader & decori)", "LoadoutOptimizer": "Impostazioni Ottimizzatore di Dotazioni", "NativeShare": "Condividi Collegamento", "Notes": "Note", "NumItems_one": "{{count}} oggetto - ai destinatari verrà richiesto di selezionare un oggetto simile dal loro inventario", "NumItems_other": "{{count}} oggetti - ai destinatari verrà richiesto di selezionare degli oggetti simili dal loro inventario", "NumMods_one": "{{count}} modifica", "NumMods_other": "{{count}} Modifiche", "Placeholder": "Caricamento collegamento di condivisione", "Subclass": "Personalizzazione sottoclasse", "Summary": "Condividi questa dotazione contenente:", "Title": "Condividi \"{{name}}\"" }, "ShareLoadout": "Condividi", "ShowModPlacement": "Mostra posizionamento modifiche", "Snapshot": "Salva come Dotazione in gioco", "SocketOverrides": "Cambiamento opzioni sottoclasse", "SortByEditTime": "Ordina per ultima modifica", "SortByName": "Ordina per nome", "SubclassOptions": "{{subclass}} opzioni", "SubclassOptionsSearch": "Cerca {{subclass}} opzioni", "Succeeded": "Dotazione applicata correttamente", "SyncFromEquipped": "Sincronizza da equipaggiati", "TooManyRequested": "Hai un totale di {{total}} {{itemname}} ma la tua dotazione ne richiede {{requested}}. Abbiamo trasferito tutti quelli che avevi.", "TuningMods": "Modifiche di calibrazione", "UnassignedModError": "La Modifica non è adatta alla tua armatura attuale", "Undo": "Annulla", "Update": "Salva Cambiamenti", "UpdateLoadout": "Aggiorna dotazione", "VendorsCannotEquip": "Non possiedi questi oggetti. Tocca per trovare un sostituto oppure clicca la X per rimuovere:" }, "Manifest": { "Download": "Scaricamento del database di informazioni di Destiny più recente da Bungie...", "Error": "Errore nel caricamento del database informazioni di Destiny:\n{{error}}\nRicarica per riprovare.", "Load": "Carico il database di informazioni di Destiny..." }, "Milestone": { "Daily": "Sfida Giornaliera", "OneTime": "Sfida Unica", "SeasonalRank": "Grado Stagionale {{rank}}", "Special": "Sfida Evento Speciale", "Tutorial": "Sfida Tutorial", "Unknown": "Sfida", "Weekly": "Sfida Settimanale" }, "Mods": { "HarmonicModDescription": "L'effetto di questa Modifica ha un costo ridotto e cambia elemento a seconda della sottoclasse equipaggiata." }, "MoveAmount": { "Amount": "Quantità:" }, "MovePopup": { "Acquired": "Questo oggetto è sbloccato nelle collezioni.", "AcquiredMod": "Questa Modifica è sbloccata nelle collezioni.", "AddNote": "Aggiungi note", "AddToLoadout": "Dotazione", "AddToLoadoutTitle": "Aggiungilo a una dotazione", "All": "Tutto", "ArtifactBreaker": "Questa arma possiede {{breaker}} a causa di una peculiarità sbloccata nell'artefatto.", "CannotCurrentlyRoll": "Questa peculiarità non può essere ottenuta sulla versione attuale di questo oggetto.", "CantPullFromPostmaster": "Devi visitare gli Amministratori in gioco per recuperare questo oggetto.", "CatalystProgress": "Progresso Catalizzatore", "CommunityData": "Spiegazione della community", "Consolidate": "Consolida", "DistributeEvenly": "Distribuisci Equamente", "EnhancementTier": "Grado {{tier}}", "Equip": "Equipaggia su:", "EquipWithName": "Equipaggia su {{character}}", "FavoriteUnFavorite": { "Favorite": "Aggiungi a preferite {{itemType}}", "Favorited": "Preferita", "Unfavorite": "Elimina dalle preferite {{itemType}}", "Unfavorited": "Non preferita" }, "Infuse": "Infondi", "InfuseTitle": "Apri la ricerca oggetti da infondere", "IntrinsicBreaker": "Questa arma possiede intrinsecamente {{breaker}}.", "LoadingSockets": "I dettagli delle peculiarità e statistiche per questo oggetto non sono ancora stati caricati.", "LockUnlock": { "AutoLock": "Lo stato bloccato è sincronizzato con le etichette dell'oggetto", "Lock": "Blocca {{itemType}}", "Locked": "Bloccato", "Unlock": "Sblocca {{itemType}}", "Unlocked": "Sbloccato" }, "MissingSockets": "I dettagli di modifiche e peculiarità non sono disponibili mentre Bungie sta aggiornando i propri servizi. Torneranno quando avranno finito, solitamente dopo alcune ore.", "Notes": "Note:", "OpenOnStreamDeck": "Apri su Stream Deck", "OverviewTab": "Panoramica", "Owned": "Questo oggetto è nel tuo inventario.", "OwnedMod": "Questa modifica è nel tuo inventario delle modifiche.", "PullItem": "Prendi da {{bucket}} per {{store}}", "PullPostmaster": "Prendi dagli Amministratori", "ReadLore": "Leggi la storia su Ishtar Collective (solo in Inglese)", "ReadLoreLink": "Leggi storia", "Rewards": "Ricompense:", "SendToVault": "Invia al Deposito", "Store": "Sposta su:", "StoreWithName": "Sposta su {{character}}", "Subtitle": { "QuestProgress": "Fase {{questStepNum}} di {{questStepsTotal}}", "Type": "{{typeName}} {{classType}}" }, "TabList": "Schede dettagli oggetto", "ToggleSidecar": "Espandi o comprimi le azioni per l'oggetto", "TrackUntrack": { "Track": "Segui {{itemType}}", "Tracked": "Seguito", "Untrack": "Non seguire {{itemType}}", "Untracked": "Non seguito" }, "TriageTab": "Triage", "UnreliablePerkOption": "Questa peculiarità compare solo nella vista delle Collezioni. Potrebbe non comparire casualmente su questo oggetto.", "Vault": "Deposito", "WeaponLevel": "Livello Arma {{level}}" }, "Notes": { "Error": "Errore! Massimo 120 caratteri per nota.", "Help": "Aggiungi note, #hashtags e :symbols:" }, "Notification": { "Cancel": "Cancella", "OK": "Ignora" }, "Objectives": { "Complete": "Completo", "Incomplete": "Incompleto" }, "Organizer": { "BulkMove": "Sposta a", "BulkMoveLoadoutName": "Seleziona in Gestione Deposito", "BulkTag": "Etichetta", "Columns": { "Ammo": "Munizioni", "Archetype": "Archetipo", "BaseStats": "Statistiche Base", "Breaker": "Modifica anti-campioni", "Crafted": "Data di forgiatura", "CustomTotal": "Totale Personalizzato", "Damage": "Tipo di Danno", "Energy": "Energia", "Event": "Evento", "Featured": "Nuovi equipaggiamenti", "Foundry": "Fonderia", "Frame": "Telaio", "Harmonizable": "Armonizzabile", "Holofoil": "Ololamina", "Icon": "Icona", "ItemTier": "Grado", "KillTracker": "Uccisioni", "Level": "Livello", "Loadouts": "Dotazioni", "Location": "Ubicazione", "Locked": "Bloccato", "MasterworkStat": "Stat. prodigio", "MasterworkTier": "Grado prodigio", "ModSlot": "Alloggiamento modifiche", "Mods": "Modifiche", "Name": "Nome", "New": "Nuovo", "Notes": "Note", "OriginTraits": "Peculiarità originale", "OtherPerks": "Componenti arma", "PercentComplete": "% Completato", "Perks": "Perks", "PerksGrid": "Griglia peculiarità", "Power": "Potere", "Quality": "Qualità %", "Recency": "In possesso da", "Season": "Stagione", "Shaders": "Cosmetici", "Source": "Fonte", "StatQuality": "Qualità Statistiche", "StatQualityStat": "{{stat}}%", "Stats": "Statistiche", "Tag": "Etichetta", "TertiaryStat": "3a statistica", "Tier": "Rarità", "Traits": "Caratteristiche Arma", "TuningStat": "Calibratore", "WishList": "Lista dei desideri", "WishListNotes": "Note Lista Desideri", "Year": "Anno" }, "EnabledColumns": "Colonne Abilitate", "Lock": "Blocca", "NoItems": "Nessun oggetto corrisponde ai filtri. Se hai una query di ricerca, prova a cancellarla.", "NoMobile": "Gira il telefono in orizzontale per usare Gestione Deposito.", "Note": "Imposta note", "OpenIn": "Mostra in Gestione Deposito", "Organizer": "Gestione Deposito", "SelectAll": "Seleziona Tutto", "SelectItem": "Seleziona o deleseziona {{name}}", "ShiftTip": "Suggerimento: Tieni premuto il tasto Maiusc e clicca su una cella per filtrare gli oggetti", "Stats": { "Aim": "Mira assistita", "Airborne": "Efficacia in volo", "AmmoGeneration": "Generazione Munizioni", "Power": "Potere", "RPM": "Proiettili al minuto", "Recoil": "Direzione del rinculo", "Reload": "Velocità di ricarica" }, "Unlock": "Sblocca" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "Lo spazio degli Amministratori è quasi pieno! ({{number}}/{{postmasterSize}})", "PostmasterFull": "Lo spazio degli Amministratori è pieno! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "Taglie", "CatalystSource": "Fonte: {{source}}", "CrucibleRank": "Gradi", "Items": "Oggetti Imprese", "Milestones": "Pietre miliari & Sfide", "NoEventChallenges": "Hai completato tutte le sfide dell'evento", "NoTrackedTriumph": "Non stai seguendo alcun Trionfo. Segui tutti quelli che vuoi su DIM.", "PaleHeartPathfinder": "Strumento di Scoperta del Pallido Cuore", "PercentMax": "{{pct}}% al massimo", "PercentPrestige": "{{pct}}% al reset", "PointsUsed_one": "1 punto utilizzato", "PointsUsed_other": "{{count}} punti utilizzati", "PowerBonusHeader": "Ricompense +{{powerBonus}} Potere", "PowerBonusHeaderUndefined": "Altre Ricompense", "Progress": "Progresso", "QueryFilteredTrackedTriumphs": "Nessuno dei tuoi trionfi seguiti corrisponde alla ricerca", "QuestExpired": "Scaduta", "QuestExpires": "Scade tra ", "Quests": "Imprese", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}punti", "Resets_one": "1 reset", "Resets_other": "{{count}} reset", "RewardPassEndsIn": "Il Pass Ricompense finisce in ", "RewardPassPrestigeRank": "Grado Prestigio {{rank}}", "SeasonalHub": "Hub stagionale", "StatTrackers": "Contatori Statistiche", "TrackedTriumphs": "Trionfi Seguiti" }, "RecordBooks": { "HideCompleted": "Nascondi completati", "RecordBooks": "Registri" }, "Records": { "Title": "Record", "UniversalOrnamentSetOther": "Altro" }, "SearchHistory": { "Date": "Recenti", "DeleteAll": "Elimina tutte le ricerche non salvate", "Description": "Queste sono tutte le tue ricerche recenti e salvate. Le puoi cancellare da qui.", "Item": "Ricerche di oggetti", "Link": "Visualizza e modifica la cronologia di ricerca", "Loadout": "Ricerche di dotazioni", "Query": "Cerca", "Title": "Cronologia Ricerca", "UsageCount": "# Utilizzi" }, "Settings": { "Appearance": "Aspetto", "ArmorArchetypeModslot": "Archetipo armatura / slot modifica", "AutoLockTagged": "Sincronizza il blocco dell'oggetto con l'etichetta", "AutoLockTaggedExplanation": "DIM bloccherà e sbloccherà automaticamente gli oggetti per corrispondere alle loro etichette. Gli oggetti forgiati resteranno sbloccati per consentire la ri-forgiatura. Quando questa impostazione è attiva, l'icona di blocco non sarà mostrata nelle tessere degli oggetti etichettati.", "BadgePostmaster": "Mostra il numero degli oggetti dagli Amministratori per il personaggio attuale sull'icona dell'app", "BadgePostmasterExplanation": "Affinché questo funzioni devi installare DIM come app e il tuo sistema operativo deve supportare la visualizzazione dei badge", "BothDescriptions": "Entrambe le descrizioni", "BungieDescriptionOnly": "Descrizioni di Bungie", "CharacterOrder": "Ordina personaggi per", "CharacterOrderFixed": "Età del personaggio (malfunzionante su PC)", "CharacterOrderRecent": "Personaggio più recente", "CharacterOrderReversed": "Personaggio più recente (ordine inverso)", "ColumnSize": "{{num}} oggetti", "ColumnSizeAuto": "Automatico", "CommunityData": "Spiegazioni delle peculiarità della community", "CommunityDescriptionOnly": "Descrizioni della community", "CsvImport": "Importa da CSV", "CustomErrorLabel": "Il nome di una statistica deve contenere caratteri di parole, ed essere diversa dalle altre statistiche per questa classe di Guardiano.", "CustomErrorValues": "Il pesi delle statistiche devono essere numeri positivi.\nAlmeno 2 pesi di statistiche devono essere sopra zero.", "CustomStatChooseName": "Scegli un nome personalizzato statistica", "CustomStatCreate": "Crea un nuovo nome personalizzato statistica", "CustomStatDelete": "Cancella questa statistica personalizzata", "CustomStatDeleteConfirm": "Cancellare questa statistica personalizzata?", "CustomStatDesc1": "Scegli le statistiche armatura desiderate per creare una statistica totale personalizzata.", "CustomStatDesc3": "Le statistiche personalizzate appariranno nei popup degli oggetti, Gestione Deposito e Confronta.", "CustomStatTitle": "Totale statistica personalizzata", "Data": "Fogli di calcolo", "DefaultItemSizeNote": "Una dimensione di 50px sarà la più nitida, senza sfocare l'immagine dell'oggetto o il testo.", "DontForgetDupes": "Non dimenticare che puoi cercare is:dupe per trovare velocemente oggetti duplicati, e che puoi utilizzare lo strumento di confronto o \n Gestione Deposito per valutarli.", "EnableAdvancedStats": "Mostra i giudizi sulle statistiche delle armature (D1)", "ExpandSingleCharacter": "Mostra tutti i personaggi", "ExportLoadoutSS": "Foglio di calcolo delle dotazioni", "ExportLoadoutSSHelp": "Scarica un lista in CSV delle tue dotazioni DIM che può essere facilmente consultata nell'applicazione per fogli di calcolo che preferisci.", "ExportProfile": "Esporta risposta profilo API", "ExportSS": "Scarica inventario in formato foglio elettronico", "ExportSSHelp": "Scarica una lista CSV dei tuoi oggetti, che può essere facilmente visualizzata in un'applicazione di fogli elettronici di tua scelta.", "HidePullFromPostmaster": "Nascondi il pulsante \"$t(Loadouts.PullFromPostmaster)\"", "Inventory": "Aspetto dell'inventario", "InventoryColumns": "Larghezza dell'inventario", "InventoryColumnsMobile": "Larghezza inventario personaggio su dispositivi mobili in modalità verticale", "InventoryColumnsMobileLine2": "Gli oggetti saranno ridimensionati per adattarsi alla nuova impostazione", "InventoryNumberOfSpacesToClear": "Numero di spazi vuoti da creare quando si utilizza la Modalità Farming", "Items": "Aspetto degli oggetti", "Language": "Lingua", "LogOut": "Disconnetti", "Masterworked": "Prodigioso", "MaxParallelCores": "Numero di core della CPU per attività in parallelo", "MaxParallelCoresExplanation": "Controlla quanti core della CPU può utilizzare DIM per compiti intensivi come l'Ottimizzatore di Dotazioni e l'analisi delle dotazioni. Valori più alti possono migliorare le prestazioni ma utilizzano più risorse di sistema.", "OrnamentDisplay": "Mostra i Decori sulle tessere degli oggetti", "OrnamentDisplayExplanationDisabled": "Gli oggetti non mostreranno mai i loro Decori", "OrnamentDisplayExplanationEnabled": "Passare sopra col puntatore o premere a lungo su un'armatura nasconde il suo Decoro", "OrnamentDisplayExplanationHide": "Passare sopra col puntatore oppure premere a lungo su un oggetto nasconde il suo Decoro", "OrnamentDisplayExplanationShow": "Passare sopra col puntatore oppure premere a lungo su un oggetto mostra il suo Decoro", "ResetToDefault": "Ripristina", "RestoreVaultSide": "Mostra gli oggetti nel Deposito nella loro colonna", "ReverseSort": "Ordinamento normale/inverso", "SetSort": "Ordina gli oggetti per:", "SetVaultWeaponGrouping": "Raggruppa le armi nel Deposito per:", "Settings": "Impostazioni", "ShowNewItems": "Mostra un punto rosso sui nuovi oggetti", "SingleCharacter": "Visualizzazione singolo personaggio", "SingleCharacterExplanation": "DIM mostrerà solamente il personaggio giocato più di recente.\nGli oggetti dei personaggi nascosti appariranno nel Deposito, se possono essere utilizzati dal personaggio attuale.\nGli oggetti specifici delle altre classi saranno completamente nascosti.", "SizeItem": "Dimensione oggetti", "SortByAmmoType": "Tipo di munizioni", "SortByAmount": "Dimensione della pila", "SortByClassType": "Classe Richiesta", "SortByCrafted": "Oggetti Forgiati (D2)", "SortByDeepsight": "Vista Profonda", "SortByFeatured": "Nuovi equipaggiamenti / In evidenza (D2)", "SortByPrimary": "Livello di Potere", "SortByRarity": "Rarità", "SortByRating": "Qualità dell'armatura (D1)", "SortByRecent": "Acquisizione recente (D2)", "SortBySeason": "Stagione (D2)", "SortByTag": "Etichetta ({{taglist}})", "SortByTier": "Grado (D2)", "SortByType": "Tipo", "SortByWeaponElement": "Tipo di danno", "SortCustom": "Ordinamento Personalizzato", "SortName": "Nome", "SpacesSize_one": "{{count}} spazio", "SpacesSize_other": "{{count}} spazi", "Theme": "Tema", "Troubleshooting": "Risoluzione problemi", "VaultArmorGroupingStyle": "Separa le armature su righe diverse ordinate per classe", "VaultGroupingNone": "Nessuno", "VaultUnder": "Mostra gli oggetti nel Deposito sotto gli oggetti equipaggiati", "VaultWeaponGroupingStyle": "Separa i gruppi di armi su righe diverse", "WeaponFrame": "Telaio arma", "WishlistRefreshNotificationBody": "Se non vedi nessun cambiamento, assicurati che la fonte (come GitHub) li rifletta!", "WishlistRefreshNotificationTitle": "Lista Desideri ricaricata" }, "Sockets": { "ApplyPerks": "Applica Peculiarità", "GridStyle": "Mostra le peculiarità come una griglia", "Insert": { "Ability": "Equipaggia Abilità", "Aspect": "Inserisci Natura", "Fragment": "Inserisci Frammento", "Mod": "Inserisci Modifica", "Ornament": "Applica Decoro", "Projection": "Applica Proiezione Spettro", "Shader": "Applica Shader", "Super": "Equipaggia Super", "Transmat": "Applica Effetto Transmat" }, "ListStyle": "Mostra le peculiarità come una lista", "Search": "Cerca nomi o descrizioni", "Select": { "Ability": "Anteprima Abilità", "Aspect": "Anteprima Natura", "Fragment": "Anteprima Frammento", "Mod": "Anteprima Modifica", "Ornament": "Anteprima Decoro", "Projection": "Anteprima Proiezione Spettro", "Shader": "Anteprima Shader", "Super": "Anteprima Super", "Transmat": "Anteprima Effetto Transmat" }, "SelectWishlistPerks": "Anteprima delle peculiarità della Lista Desideri" }, "Stats": { "CrouchingSpeed": "Da accovacciato", "Custom": "Totale Personalizzato", "CustomDesc": "Totale personalizzato delle statistiche di base selezionate, ignorando modifiche o prodigio. Controlla Impostazioni per configurare quali statistiche vengono incluse.", "DamageResistance": "Resistenza danni PvE", "Discipline": "Disciplina", "DropLevel": "Potere account", "DropLevelExplanation1": "Il Potere dell'account è il livello base quando si calcola l'aumento di livello delle ricompense.", "DropLevelExplanation2": "Il Potere dell'account utilizza il livello più alto in ogni alloggiamento, indipendentemente dalla classe richiesta o dalla regola di \"una sola esotica\".", "EquippableGear": "Oggetti equipaggiabili", "FlinchResistance": "Resistenza al sussulto", "HP": "Salute", "Intellect": "Intelletto", "MaxGearPower": "Potere Massimo degli oggetti equipaggiabili", "MaxGearPowerAll": "Potere max di tutto l'equipaggiamento", "MaxGearPowerOneExoticRule": "Massimo Potere degli oggetti equipaggiabili\n(soltanto un'armatura esotica equipaggiata)", "MaxTotalPower": "Potere massimo totale", "MetersPerSecond": "m/s", "Milliseconds": "ms", "NoBonus": "Nessun Bonus", "NotApplicable": "N/A", "OfMaxRoll": "{{range}} del roll massimo", "PercentHelp": "Clicca per maggiori informazioni riguardo la Qualità delle Statistiche.", "Percentage": "%", "PowerModifier": "Potere dato dal progresso dell'esperienza stagionale", "Prestige": "Livello Prestigio: {{level}}\n{{exp}}xp fino alle 5 particelle di Luce.", "Quality": "Qualità statistiche", "ShieldHP": "Capacità scudo", "StrafingSpeed": "Mentre miri", "Strength": "Forza", "TierProgress": "T{{tier}} {{statName}} ({{progress}}/60 per T{{nextTier}})\n", "TierProgress_Max": "T{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "Tempo per salute piena", "Total": "Totale", "TotalHP": "Salute totale", "WalkingSpeed": "Mentre cammini", "WeaponPart": "Parte di arma" }, "Storage": { "ApiPermissionPrompt": { "Description": "Adesso DIM può immagazzinare le tue etichette, dotazioni ed impostazioni e sincronizzarle tra tutte le sue versioni, senza accessi separati. Puoi importare i tuoi dati esistenti dalla pagina Impostazioni se non hai già abilitato DIM Sync. Tutto questo è possibile grazie al supporto dei nostri backers OpenCollective!", "No": "Non adesso", "Title": "Attivare DIM Sync?", "Yes": "Attiva Sync" }, "AutoBackup": "Abbiamo salvato i tuoi dati in un file, nella tua cartella di download, chiamato dim-data.json, in caso servissero.", "BackUpFirst": "Devi prima effettuare un backup dei tuoi dati. Non si sa mai.", "BrowserMayClearData": "Il browser può cancellare queste informazioni se esaurisci la memoria o se non visiti DIM spesso.", "DataIsLocal": "I dati di etichette e note sono solo in locale", "DeleteAllData": "Cancella TUTTI i dati dai server di DIM Sync", "DeleteAllDataConfirm": "Sei sicuro di voler eliminare TUTTI i tuoi dati, da tutti gli account, da DIM Sync? Non è possibile annullare questa operazione.", "Details": { "IndexedDBStorage": "Archiviazione in locale salverà le informazioni solo su questo browser. Se cancelli i tuoi dati di navigazione, esse andranno perse." }, "DimApiFinePrint": "DIM salverà le tue etichette, dotazioni e impostazioni nei server di DIM e li sincronizzerà tra tutte le sue versioni.", "DimSyncDown": "DIM Sync non è connesso a causa di un problema di comunicazione con il server.", "DimSyncEnabled": "DIM Sync Attivato", "DimSyncNotEnabled": "DIM Sync non è abilitato, quindi le tue impostazioni, etichette, dotazioni e ricerche sono memorizzate in locale e andranno perse se pulisci la memoria del browser. Attiva DIM Sync nelle Impostazioni per eseguire il backup automatico dei dati, oppure fai regolarmente un backup manuale.", "EnableDimApi": "Abilita DIM Sync (consigliato)", "Export": "Scarica Backup Dati", "ExportError": "Impossibile scaricare il backup da DIM Sync", "ExportErrorBody": "DIM Sync potrebbe essere offline, oppure hai problemi di connessione. Scaricheremo una copia dei tuoi dati salvati in locale invece.", "Import": "Importa Backup Dati", "ImportConfirmDimApi": "Sei sicuro di voler sovrascrivere le tue etichette, dotazioni e impostazioni attuali? Sostituirà completamente quelli che avevi.", "ImportExport": "Backup & Importazione", "ImportFailed": "Importazione Fallita! {{error}}", "ImportNoFile": "Nessun file selezionato!", "ImportNotification": { "FailedBody": "Impossibile importare dati. {{error}}", "FailedTitle": "Importazione Fallita", "NoData": "Nessuna dotazione o etichetta trovati nel backup", "SuccessBodyForced": "Importate impostazioni, {{loadouts}} dotazioni, e {{tags}} oggetti etichettati dal tuo backup in DIM Sync, sostituendo ciò che era già presente.", "SuccessBodyLocal": "Importate impostazioni, {{loadouts}} dotazioni e {{tags}} oggetti etichettati dal tuo backup nell'archivio locale, sovrascrivendo ciò che era presente. Non possiamo garantire che l'archivio locale non venga perduto - considera l'attivazione di DIM Sync.", "SuccessTitle": "Importazione avvenuta con successo" }, "ImportTooManyFiles": "Per favore seleziona un solo file da importare.", "ImportWrongFileType": "Il file non è un file JSON. Potrebbe non essere un backup di DIM.", "IndexedDBStorage": "Memoria del browser locale", "LearnMore": "Informazioni su DIM Sync", "MenuTitle": "Sync & Backup", "ProfileErrorBody": "Abbiamo avuto un problema di comunicazione con DIM Sync. Le tue ultime impostazioni, etichette, dotazioni e ricerche potrebbero non essere mostrate. I tuoi dati sono ancora nei nostri server, ed ogni aggiornamento che farai in locale sarà salvato appena potremo riconnetterci. Continueremo a riprovare mentre DIM è aperto.", "ProfileErrorTitle": "Errore Dowload DIM Sync", "RefreshDimSync": "Ricarica i dati remoti da DIM Sync", "UpdateErrorBody": "Abbiamo avuto un problema salvando i tuoi dati su DIM Sync. Continueremo a riprovare mentre DIM è aperto.", "UpdateErrorTitle": "Errore Salvataggio DIM Sync", "UpdateInvalid": "Errore salvataggio dati su DIM Sync", "UpdateInvalidBody": "I dati inviati a DIM Sync non erano validi e non saranno salvati.", "UpdateInvalidBodyLoadout": "La dotazione \"{{name}}\" non è valida e non sarà salvata. Se l'hai importata da un altro sito, fagli sapere che stanno esportando dotazioni non valide.", "UpdateQueueLength_one": "{{count}} nuova modifica sarà salvata quando potremo riconnetterci.", "UpdateQueueLength_other": "{{count}} nuove modifiche saranno salvate quando potremo riconnetterci.", "Usage": "DIM sta usando {{usage, humanBytes}} su {{quota, humanBytes}} disponibili su questo dispositivo. Questo comprende i database di Destiny scaricati da Bungie.net." }, "StreamDeck": { "Authorize": "Connetti applicazione", "Enable": "Plugin Stream Deck", "Error": { "Body": "Si è verificato un errore nell'invio dei dati al plugin di Stream Deck. Contatta lo sviluppatore del plugin. {{error}}", "Title": "Errore plugin Stream Deck" }, "FinePrint": "Abilita la connessione con il plugin dello Stream Deck di DIM. Questo plugin è un progetto separato che non è scritto né supportato dal team di DIM.", "Install": "Installa plugin", "MissingAuthorization": "Devi autorizzare l'applicazione Stream Deck per connettersi a DIM. Vai nelle Impostazioni e clicca \"Connetti applicazione\".", "Tooltip": { "Application": "Applicazione Stream Deck", "AuthRequired": "Clicca questo bottone oppure vai nelle Impostazioni e clicca \"Connetti applicazione\".", "Error": "Il tuo plugin Stream Deck non è più supportato. Aggiornalo all'ultima versione. Questo plugin richiede almeno:", "ErrorConnection": "se stai già utilizzando la versione più recente, controlla che qualche estensione del browser non stia bloccando l'applicazione.", "ExtensionIssue": "Problema con Estensioni", "Plugin": "Plugin", "Title": "Plugin Stream Deck per DIM", "Version": "Versione:" } }, "StripSockets": { "Action": "Svuota alloggiamenti", "ArmorMods": "{{count}}x modifiche armatura", "Button": "Svuotamento {{numSockets}} alloggiamenti", "Cancel": "Cancella", "Choose": "Scegli gli alloggiamenti da svuotare", "DiscountedMods": "{{count}}x modifiche sconto", "Done": "Alloggiamenti svuotati", "NoSockets": "Nessun alloggiamento da svuotare", "Ok": "Ok", "Ornaments": "{{count}}x Decoro", "Others": "{{count}}x Proiezioni Spettro", "Running": "Svuotamento alloggiamenti", "Shaders": "{{count}}x Shader", "Subclass": "{{count}}x Opzione sottoclasse", "WeaponMods": "{{count}}x Modifica arma" }, "Tags": { "Archive": "Archivia", "ClearTag": "Cancella Etichetta", "Favorite": "Preferito", "Infuse": "Infondi", "Junk": "Smantella", "Keep": "Tieni", "LockAll": "Blocca Oggetti", "TagItem": "Etichetta", "UnlockAll": "Sblocca Oggetti" }, "Triage": { "AccountsForArtifice": "Controlla se un pezzo di Armatura dell'Artificio potrebbe essere migliore, se una Modifica statistiche +3 fosse utilizzata.", "BetterArmor": "Armatura Strettamente Migliore", "BetterArtificeArmor": "Miglior Armatura Artificio", "BetterStatArmor": "Migliori Statistiche Armatura", "BetterStatArtificeArmor": "Migliori Statistiche Armatura Artificio", "BetterWorseArmor": "Migliore/Peggiore Armatura", "BetterWorseIncludes": "Identifica i pezzi di armatura con:", "HighStats": "Statistiche alte", "InLoadouts": "In dotazione", "OwnedCount": "# in possesso", "PerkBetterArmorDesc": "Le stesse, o più, caratteristiche intrinseche o alloggiamenti modifiche speciali.", "PerkWorseArmorDesc": "La stessa caratteristica intrinseca, o nessuna.", "SimilarItems": "Oggetti simili", "StatBetterArmorDesc": "Tutte le statistiche egualmente alte, e almeno una statistica migliore.", "StatNotPerkArmorDesc": "Controlla solo le statistiche. Un pezzo inferiore potrebbe comunque avere un alloggiamento modifiche speciale o caratteristiche intrinseche.", "StatWorseArmorDesc": "Nessuna statistica migliore, e almeno una statistica peggiore.", "ThisItem": "Questo oggetto", "WorseArmor": "Armatura Strettamente Peggiore", "WorseArtificeArmor": "Peggior Armatura Non-Artificio", "WorseStatArmor": "Peggiori Statistiche Armatura", "WorseStatArtificeArmor": "Peggiori Statistiche Armatura non-Artificio", "YourBestItem": "Il tuo miglior oggetto" }, "Triumphs": { "GildingTriumph": "Trionfo Indorato", "HideCompleted": "Nascondi trionfi completati", "RevealRedacted": "Mostra trionfi segreti", "SortRecords": "Ordina i trionfi per completamento" }, "Vendors": { "Collections": "Collezioni", "Engram": "Grado", "FilterToUnacquired": "Mostra solo oggetti non acquisiti", "HideSilverItems": "Nascondi oggetti acquistabili con Argento", "NoItems": "Questo mercante al momento non offre alcun oggetto.", "RefreshTime": "L'inventario si aggiorna tra:", "Vendors": "Mercanti" }, "Views": { "About": { "APIHistory": "Mostra lo storico di tutte le operazioni fatte da DIM (e altre app per Destiny)", "BungieCopyright": "Tutte le immagini ed i contenuti sono proprietà di Bungie.", "CommunityInsight": "Spiegazione dele peculiarità e statistiche personaggio a cura di {{clarityLink}}. Se noti delle imprecisioni o hai delle domande, entra su {{clarityDiscordLink}}.", "Discord": "Discord", "DiscordHelp": "Fai domande, valuta, e ricevi supporto nei nostri canali Discord.", "FAQ": "Domande frequenti", "FAQAccess": "Come fa DIM ad avere accesso ai miei dati di Destiny?", "FAQAccessAnswer": "Usiamo l'autenticazione della app di Bungie per darti l'accesso a DIM per vedere e muovere i tuoi oggetti. DIM non vede mai il tuo nome utente o password. Questa è la stessa maniera in cui funziona la Companion app.", "FAQKeyboard": "Su DIM funzionano le scelte rapide da tastiera (scorciatoie)?", "FAQKeyboardAnswer": "Sì! Premi \"?\" per vedere una lista delle scorciatoie disponibili.", "FAQLogout": "Come posso effettuare il log out da DIM?", "FAQLogoutAnswer": "Apri il menù dall'icona in alto a sinistra e scegli \"Disconnetti\"", "FAQLostItem": "Ho perso un mio oggetto utilizzando DIM!", "FAQLostItemAnswer": "Bungie non permette di cancellare oggetti (neanche la loro app!). Molto probabilmente un trasferimento è fallito, ed ha lasciato il tuo oggetto nel deposito o in un altro personaggio. Puoi cercare l'oggetto e se ciò non dovesse funzionare, ricarica la pagina. Controlla su {{link}} o nel gioco per vedere se il tuo oggetto esiste ancora. Siamo sicuri che è ancora lì.", "FAQMobile": "DIM supporterà dispositivi mobili? Ci sarà una app?", "FAQMobileAnswer": "Il sito di DIM può essere aperto su telefoni e tablet, e puoi aggiungerlo alla tua schermata home per una esperienza simile ad un'app.", "GitHub": "GitHub", "GitHubHelp": "Se sei interessato a contribuire al progetto, visita la pagina su {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM é un'app gratuita ed open source costruita da sviluppatori della comunità basandosi sugli stessi servizi di Bungie.net e la Companion App di Destiny.", "Schedule": { "beta": "Questa versione beta di DIM è aggiornata ogni volta che cambiamo il codice - ha tutte le ultime novità e correzioni, ma anche tutti i nuovi bug!", "release": "Questa versione di DIM viene aggiornata una volta alla settimana, approssimativamente a mezzanotte di domenica, orario degli Stati Uniti, costa del Pacifico." }, "Translation": "Unisciti al Team Traduzione!", "TranslationText": "Utilizziamo {{link}} per la traduzione. Se vuoi migliorare la traduzione di DIM in altre lingue, unisciti al team.", "Version": "Versione {{version}} ({{flavor}}), pubblicata il {{date}}", "Wiki": "Guida utente di DIM", "WikiHelp": "Impara a utilizzare le funzionalità di DIM." }, "Login": { "Auth": "Richiedi autorizzazione su Bungie.net", "EnableDimSyncWarning": "In precedenza hai disabilitato DIM Sync e stavi usando soltanto la memoria locale. Attivando DIM Sync sovrascriverai qualsiasi salvataggio locale con i dati di DIM Sync. Dovresti fare un backup dei tuoi dati prima di attivare DIM Sync. Potrai ripristinarli da quel backup nelle Impostazioni.", "Explanation": "Permetti a DIM di consultare e modificare i tuoi personaggi, deposito e progresso di Destiny.", "LearnMore": "Informazioni sugli account e accesso", "NewAccount": "Accedi con un diverso account Bungie.net", "Permission": "Ci serve il tuo permesso..." }, "Support": { "BackersDetail": "Supportaci con una donazione singola oppure con una mensile e aiutaci a continuare il nostro sviluppo.", "FreeToDownload": "DIM è un prodotto scaricabile ed utilizzabile gratuitamente. Il codice sorgente di DIM è open source e chiunque può migliorarlo. Non vedrai mai pubblicità su DIM. Ci impegnamo a tal fine.", "OpenCollective": "Utilizziamo {{link}} come servizio per fornire un compenso ai nostri sviluppatori per la loro dedizione e il tempo investito in questo progetto.", "Store": "Abbiamo gadget col nostro logo ed altri design in vendita su {{link}}", "Support": "Supporta DIM" } }, "WishListRoll": { "BestRatedTip_one": "Questa peculiarità corrisponde esattamente ad un roll della tua Lista Desideri.", "BestRatedTip_other": "Queste peculiarità corrispondono esattamente ad un roll della tua Lista Desideri.", "Clear": "Cancella Lista Desideri", "CopiedLine": "Roll Lista Desideri copiato negli appunti", "CopyLine": "Copia le peculiarità selezionate come roll Lista Desideri", "DupeRolls": " (+{{num, number}} doppioni ignorati)", "ExternalSource": "Aggiungi un'altra Lista dei desideri", "ExternalSourcePlaceholder": "Incolla qui l'URL della Lista desideri", "Header": "Lista dei desideri", "Import": "Carica i roll della Lista Desideri", "ImportError": "Errore nel caricamento della Lista Desideri da \"{{url}}\":{{error}}", "ImportFailed": "Nessuna delle tue Liste Desideri contieneva roll validi.", "ImportNoFile": "Nessun file selezionato.", "InvalidExternalSource": "Inserisci un URL valido come origine della Lista Desideri esterna. L'URL deve iniziare con uno dei seguenti:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "Ultimo aggiornamento: {{lastUpdatedDate}} alle {{lastUpdatedTime}}", "Num": "{{num, number}} roll nella tua Lista Desideri", "NumRolls": "{{num, number}} roll", "Refresh": "Aggiorna Lista Desideri", "SourceAlreadyAdded": "Lista dei desideri già aggiunta", "UpdateExternalSource": "Aggiungi Lista dei desideri", "Voltron": "voltron (default)", "WishListNotes": "Note della Lista Desideri:", "WorstRatedTip_one": "Questa peculiarità corrisponde esattamente ad un roll della tua lista degli scarti.", "WorstRatedTip_other": "Queste peculiarità corrispondono esattamente ad un roll della tua lista degli scarti." }, "no-space": "nessuno spazio", "wrong-level": "livello errato" } ================================================ FILE: src/locale/ja.json ================================================ { "AWA": { "ConfirmDescription": "DIMでのアイテム変更には、Destiny 2コンパニオンアプリでの承認が必要です。", "ConfirmTitle": "操作を確認", "Error": "Modまたはパークの変更でエラーが発生しました", "ErrorMessage": "{{item}} に {{plug}} を装備できませんでした。\n\n{{error}}", "FailedToken": "アイテムを変更する権限を取得できません", "IrreversiblePlugging": "{{plug}} を所有していないため、上書きされません。" }, "Accounts": { "Choose": "{{bungieName}} のプロファイル", "ErrorLoadInventory": "Destiny {{version}} のキャラクターと所持アイテムの情報をロードできません", "ErrorLoadManifest": "Bungieからのデータ読み込みに失敗しました", "ErrorLoading": "Bungie.netからDestinyのアカウント情報をロードできません", "MissingAccountWarning": "ここに自分のアカウントが表示されない場合は、正しい Bungie.net アカウントにログインしていないか、 Bungie.net がメンテナンスでダウンしている可能性があります。", "MissingDescription": "表示しようとしているアカウントは、Bungie.netプロフィールにリンクされていません。以下のアカウントから選択してください。", "MissingTitle": "アカウントが見つかりません", "NoCharacters": "このBungie.netアカウントには、Destinyのキャラクターが関連付けられていません。別のアカウントでログインしてみてください。", "NoCharactersTitle": "キャラクターが見つかりませんでした", "SwitchAccounts": "ヘッダーのメニューで、後からアカウントを切り替えることができます。", "Title": "アカウント" }, "Activities": { "Activities": "アクティビティ", "Hard": "ハード", "Nightfall": "ナイトフォールストライク", "Normal": "ノーマル", "WeeklyHeroic": "週間英雄ストライク" }, "Armory": { "AlternateItems": "全バージョン一覧", "Armory": "武器庫", "DifferentSeason": "別シーズン版", "NoNotes": "メモなし", "OpenInArmory": "アーマリーで見る", "Season": "{{year}} 年目、シーズン {{season}}", "TrashlistedRolls_other": "{{count, number}} 個のウイッシュリストのロール", "Unknown": "不明のアイテム", "UnknownPerkHash": "このアイテムにパーク:{{perkName}}のハッシュ: {{hash}}が見つからないため、このウィッシュリストロールは無効です。ウィッシュリストの管理者に問い合わせてください。なお、ウィッシュリストでは常に非強化版パークを指定する必要があります。", "WishlistedRolls_other": "{{count, number}} 個のウィッシュリストと一致しました", "YourItems": "所持品" }, "Browsercheck": { "Samsung": "Samsungブラウザでは、ダーク モードがオンになっているとサイトが暗くなりすぎることがあります。ブラウザから 設定 > ラボ > Webサイトのダークテーマを使用 を有効にするか、別のブラウザに切り替えてください。", "Steam": "Steam オーバーレイ ブラウザは非常に古いため、DIMの一部の機能もしくは全て動作しない可能性があることからサポート対象外とします。", "Unsupported": "DIM チームはこのブラウザの利用をサポートしていません。DIM の一部または全ての機能が動作しない可能性があります。" }, "Bucket": { "Armor": "防具", "Class": "サブクラス", "General": "全般", "Ghost": "ゴースト", "Inventory": "所持品", "Postmaster": "ポストマスター", "Progress": "進行状況", "Reputation": "ランク", "Unknown": "不明", "Vault": "保管庫", "Weapons": "武器" }, "BulkNote": { "Append": "メモに追加 / #hashtag を追加", "Confirm": "メモを更新", "Remove": "メモ削除 / #hashtag 削除", "Replace": "メモを置き換える", "Title_other": "{{count}} 個のアイテムのメモをまとめて変更する" }, "BungieAlert": { "Title": "Bungieからメッセージが届いています:" }, "BungieService": { "AppNotPermitted": "DIM にはこの操作を行なう権限がありません。", "DestinyCannotPerformActionAtThisLocation": "アクティビティ中はアイテムを装備したり、MODを変更することはできません。 オービットやソーシャルエリアに移動してみてください。これはDIMではなくBungie.net APIの制限です。", "DestinyItemUnequippable": "このアイテムを装備できません。このキャラクターの最後のアクティビティが装備をロックした場合は、キャラクターに再度ログインしてみてください。", "DestinyLegacyPlatform": "Bungieのサービスは現在、前世代機でDestiny 1をプレイするとDestiny 2のアカウント情報がDIMでロードできないバグがあります。Bungieは近日中にこれを修正する予定ですが、その時までは現行機でDestiny 1をプレイしていれば、DIMが利用できます。", "DevVersion": "DIMの開発版を使っていますか? その場合、Bungie.netにchrome拡張機能を認証させる必要があります。", "Difficulties": "Bungie.netで現在、問題が発生しています。", "ErrorTitle": "Bungie.net エラー", "ItemUniquenessExplanation": "キャラクターは '{{name}}' を一つしか持てません。", "Maintenance": "Bungie.netのサーバーがメンテナンスによってダウンしています。", "MissingInventory": "所持品データを取得できませんでした。Bungieのプライバシー設定を確認のうえ、Dimをログアウトして再ログインしてください。", "NetworkError": "ネットワークエラー。{{status}} {{statusText}}", "NoAccount": "Destiny のアカウントが見つかりません。正しいプラットフォームが選択されているかご確認ください。", "NoAccountForPlatform": "{{platform}} でDestinyのアカウントが見つかりませんでした。", "NotConnected": "インターネットに接続してない可能性があります。", "NotConnectedOrBlocked": "インターネットに接続されていない可能性があります。 または広告ブロックまたは Bungie.netのプライバシー拡張がブロックされています。", "NotLoggedIn": "このアプリを使用するために DIM を承認してください。", "Slow": "現在、Bungie.netのサーバー応答が遅くなっています。", "SlowDetails": "Bungie.netは現在、問い合わせの返答に時間がかかっています。多くのプレイヤーが同時にサーバー接続している場合や、Bungie.netに問題がある場合にこのような事態が発生することがあります。また、利用されているインターネット接続に問題がある可能性もあります。引き続き、サーバーからの返答をお待ちください。", "SlowResponse": "Bungie.netのサーバーからの応答時間が非常に遅く、返答がありません。", "Throttled": "Bungie.netは、DIMが作成できるリクエストの数を制限しています。", "Twitter": "最新情報はこちらから:", "UnknownError": "Bungie.netからのメッセージ: {{message}}", "VendorNotFound": "ベンダー情報が取得できません。" }, "Compare": { "Archetype": "内在特性", "AssumeMasterworked": "マスターワーク済みと仮定", "AssumeMasterworkedDescription": "マスターワーク済かつModなしでのステータス値", "BaseStatsDescription": "マスターワークもModも適用していない基本ステータス値", "Button": "比較", "ButtonHelp": "アイテムを比較する", "CompareBaseStats": "基本ステータス値を表示", "CurrentStats": "現在のステータス", "CurrentStatsDescription": "マスターワークとModを考慮した現在ステータス値", "Error": { "Invalid": "比較できるアイテムがありません。", "Unmatched": "このアイテムは比較されているアイテムのタイプと一致しません。" }, "InitialItem": "比較ツールが起動されたアイテムです", "IsVendorItem": "このアイテムは保管庫にありませんが、 {{vendorName}} が販売しています。", "NoModArmor": "Pre-mods" }, "Cooldown": { "Grenade": "グレネードのクールダウン: {{cooldown}}", "Melee": "近接のクールダウン: {{cooldown}}", "Super": "スーパースキルのクールダウン: {{cooldown}}" }, "Countdown": { "Days_compact_other": "{{count}}日間", "Days_other": "{{count}}日間" }, "Csv": { "EmptyFile": "ファイルにラインがありませんでした。", "ImportConfirm": "タグ/メモを CSV からインポートするよろしいですか。タグ/メモ スプレッドシートに含まれているすべてのアイテムが上書きされます。", "ImportFailed": "タグ/メモを CSV からインポートに失敗しました: {{error}}", "ImportSuccess_other": "{{count}} アイテムのタグ/メモが読み込まれます。", "ImportWrongFileType": "CSV ファイルではありません。", "WrongFields": "CSVには「Id」、「Notes」、「Tag」、および「Hash」の列が必要です" }, "Dialog": { "Cancel": "キャンセル", "OK": "OK" }, "EnergyMeter": { "Energy": "エネルギー", "Unused": "不使用", "UpgradeNeeded": "このアイテムの現エネルギー容量は {{energyCapacity}} です。選択した mod が適用するように、そのエネルギー容量は {{energyUsed}} でなければなりません。", "Used": "使用中" }, "ErrorBoundary": { "Title": "エラー発生" }, "ErrorPanel": { "BrowserTooOld": "ブラウザのバージョンが古すぎるためDIMを使用できません。ブラウザを最新バージョンに更新してください。", "BrowserTooOldTitle": "非対応ブラウザを検出", "Description": "Bungie.net がダウンしているか確認するには、Destiny 2公式アプリで所持品データを読み込めるか試してください。", "ReadTheGuide": "トラブルシューティングの手順については、ユーザーガイド(メニュー内のリンク) をご覧ください。", "SystemDown": "Destinyに関連するすべてのアプリに影響する内容で、 DIM チームにて修正したり回避ができない内容です。", "Troubleshooting": "トラブルシューティングガイド" }, "FarmingMode": { "D2Desc_female_other": "DIMはアイテムタイプごとに {{count}} 個の空白が常に {{store}} 個あることを確認することで、ポストマスターにアイテムが移動しないようにしています。", "D2Desc_male_other": "DIMはアイテムタイプごとに {{count}} 個の空白が常に {{store}} 個あることを確認することで、ポストマスターにアイテムが移動しないようにしています。", "D2Desc_other": "DIMはアイテムタイプごとに {{count}} 個の空白が常に {{store}} 個あることを確認することで、ポストマスターにアイテムが移動しないようにしています。", "Desc_female_other": "DIMはエングラムとグリマーアイテムを {{store}} から保管庫に移動し、アイテムタイプごとに {{count}} 個のスペースを開けたままにして、ポストマスターへ行かないようにします。", "Desc_male_other": "DIMはエングラムとグリマーアイテムを {{store}} から保管庫に移動し、アイテムタイプごとに {{count}} 個のスペースを開けたままにして、ポストマスターへ行かないようにします。", "Desc_other": "DIMはエングラムとグリマーアイテムを {{store}} から保管庫に移動し、アイテムタイプごとに {{count}} 個のスペースを開けたままにして、ポストマスターへ行かないようにします。", "FarmingMode": "ファーミングモード", "FarmingModeNote": "(ドロップのためのスペースを確保)", "MakeRoom": { "Desc": "DIMはエングラムとグリマーアイテムのみを{{store}} から保管庫、または他のキャラクターに移動し、ポストマスターに行かないようにします。", "Desc_female": "DIMはエングラムとグリマーアイテムのみを{{store}} から保管庫、または他のキャラクターに移動し、ポストマスターに行かないようにします。", "Desc_male": "DIMはエングラムとグリマーアイテムのみを{{store}} から保管庫、または他のキャラクターに移動し、ポストマスターに行かないようにします。", "MakeRoom": "装備を動かしてアイテムを拾うためのスペースを確保する", "Tooltip": "チェックをオンにすると、DIMは武器と防具を動かして保管庫のスペースをエングラム用に作ります。" }, "OutOfRoom": "{{character}} からアイテムを移動するには空きスペースがありません。ゴミ箱を片付ける時です!", "OutOfRoomTitle": "スペースありません。", "Stop": "停止", "Vault": "アイテムを保管庫に移動してスペースを確保します。" }, "FashionDrawer": { "Accept": "ファッションを保存", "CannotFitOrnament": "このアイテムには装飾用のソケットがないか、装飾そのものがありません。", "CannotFitShader": "このアイテムはシェーダーを装備できません", "ClearOrnaments": "装備品をクリア", "ClearOrnamentsTitle": "全ての装飾品を「優先なし」にリセット", "ClearShaders": "シェーダーをクリア", "ClearShadersTitle": "全てのシェーダーを「優先なし」にリセット", "NoPreference": "優先なし - このソケットは変更されません", "Reset": "ファッションをクリア", "Sync": "同期", "SyncOrnaments": "装備品を同期", "SyncOrnamentsTitle": "ロック解除されている場合は、全てのアイテムに同じセットの装飾品を利用する", "SyncShaders": "シェーダーを同期", "SyncShadersTitle": "全てのアイテムに同じシェーダーを利用する", "Title": "シェーダーと装備品を選択", "UseEquipped": "装備済みのファッションを利用する" }, "FileUpload": { "Instructions": "ボタンをクリックするか、ここにファイルをドラッグ&ドロップしてください。" }, "Filter": { "Adept": "新・", "AmmoType": "弾薬タイプ(キネティック・特殊・ヘビー)に基づいてアイテムを表示します。", "Armor": "アーマーアイテムを表示します。", "Armor3": "DLC「運命の境界」以降から登場したアーマー3.0アイテムを表示します。", "ArmorCategory": "指定したアイテムカテゴリーに絞り込みます。", "ArmorIntrinsic": "戦略的アーマーのような、固有パークを持つレジェンダリーアーマーを表示します。", "Artifice": "戦略的アーマーを表示します。", "Ascended": "上昇した上昇ノードを持つアイテムを表示。", "Breaker": "チャンピオンを怯ませる武器タイプでフィルタリングします。breaker:instrinsicは、内在特性で怯ませる能力を持つアイテムを表示します。", "BulkClear_other": "{{count}} 個のアイテムからタグを削除。", "BulkRevert_other": "{{count}} 個のアイテムのタグを元に戻しました。", "BulkTag_other": "選択した{{count}} 個のアイテムに{{tag}} のタグを付けました。", "Catalyst": "媒体をステータスに応じて表示します。catalyst:completeは、挑戦を完了し適用済の媒体を表示します。catalyst:incompleteは、媒体は入手済ですが、目標を達成していないか、媒体を適用していないものを表示します。catalyst:missing は、媒体を適用できる可能性があるものの、まだ媒体を見つけていないアイテムを表示します。", "Class": "タイタン、ハンター、ウォーロックのいずれが装備できるかに基づいてアイテムを表示します。", "Combine": "フィルターは、{{example}} のように、括弧や「or」「and」で組み合わせたり、グループ化することでアイテムを絞り込めます。", "ContributePower": "パワー値があり、合計パワーレベルの変動要素となるアイテムを表示します。", "Cosmetic": "フレアや装飾アイテムのみに絞り込みます。", "Craftable": "形成可能なアイテムを表示します。", "CraftedDupe": "重複している武器のうち、少なくとも1つは形成されたものを表示します。", "Curated": "固定パーク構成(Curated Roll)のアイテムを表示します。", "CurrentClass": "現在ログインしているガーディアンが装備可能なアイテムを表示します。", "CustomStatLower": "カスタム後のステータス合計値のみを考慮して、同じサブクラス防具でも厳密に低いステータス値を持つ防具を表示します。", "DamageType": "ダメージタイプに基づいてアイテムを表示します。", "Deepsight": "ディープサイトの共振を抽出できる武器を表示します。ディープサイトの共振は武器パターンを抽出したり、一部の対象武器はディープサイト・ハーモナイザーを使用してディープサイトの発動を付与できます。", "Deprecated": "このフィルターは利用できません。", "Description": "説明", "DescriptionFilter": "説明がフィルターテキストと部分的に一致するアイテムを表示します。 引用符を使用してフレーズ全体を検索します。", "DisabledModSlot": "現在無効化されているMODを装着したアイテムを表示します。", "Dupe": "再登場版も含む、重複アイテムを表示します。", "DupeArchetype": "同じステータスの特性を持つ防具グループを表示します。", "DupeCount": "指定された重複数を持つアイテムを表示します。", "DupeLower": "再登場版を含め、最高パワーでない重複アイテムすべてを表示します。1つのアイテムだけが最高パワーとしてカウントされ、それ以外の重複アイテムとして見なされます。", "DupePerks": "パーク効果が同じ、または一部共通しているアイテムを表示します。", "DupeSetBonus": "同じセットボーナスを持つ防具グループを表示します。", "DupeStats": "同じ基礎ステータスを持ち、戦略的アーマーやチューニングMod適用済防具などで同じ特性を持つ防具を表示します。", "DupeTertiary": "同じ内在特性を持つ防具グループを表示します。", "DupeTraits": "同じタイプの武器において、特性が重複またはすべて含む上位互換のアイテムを表示します。", "DupeTunedStat": "同じチューニングModを持つ防具グループを表示します。", "DupeUntunedStats": "調整Modによるステータス値を無視し、同一のステータスを持つ防具グループを表示します。", "DupeZeroStats": "3つのパラメーターが0の同じ防具グループを表示します。", "Energy": "DLC「影の砦」以降から登場したアーマー2.0アイテムを表示します。", "EnergyCapacity": "現在のエネルギー容量に基づいてアイテムを表示します。", "Engrams": "エングラムを表示", "Enhanceable": "強化特性を付与できる武器を表示します。", "Enhanced": "強化Tierに基づいて武器を表示します。", "EnhancedPerk": "指定した数の強化パークを持つ武器を表示します。", "EnhancementReady": "パークの強化が可能なレベルに達した武器を表示します。", "Equipment": "装備可能なアイテムを表示します。", "Equipped": "現在キャラクターに装備されているアイテムを表示します。", "Event": "任意のイベントから登場したアイテムを絞り込めます。", "ExtraPerk": "パーク列に追加選択肢があるランダムロールのレジェンダリー武器を表示します。", "Featured": "今シーズンの\"新しい装備\"または\"注目の装備\"を表示します。", "Filter": "フィルター", "FilterWith": "フィルター絞込:", "Focusable": "現在ベンダーで購入可能なアイテムを表示します", "Foundry": "指定した武器メーカーのアイテムを表示します。", "Glimmer": "グリマーの増加に関連する消耗品のアイテムを表示しています。", "Harrowed": "\\(苦悩\\)", "HasNotes": "メモが適用されているアイテムを表示します。", "HasOrnament": "装飾品が施されているアイテムが表示されます。", "HasShader": "シェーダーが適用されているアイテムを表示します。", "Holofoil": "ホロフォイル武器を表示します。", "InDimLoadout": "is:ininingameloadoutはゲーム内ロードアウトで使用しているアイテムを強調表示します。", "InInGameLoadout": "is:ininingameloadoutはゲーム内ロードアウトで使用するアイテムを表示します。", "InInventory": "保管庫で最低1つは所有しているアイテムを表示します。このフィルターはベンダーまたはレコードの画面でのみ有効です。", "InLoadout": "is:inloadout は、任意のロードアウトに含まれるアイテムを表示します。inloadout: で検索すると、タイトルが一致するロードアウトに含まれるアイテムが表示されます。ハッシュタグと一緒に使うと、inloadout: はタイトルやノートにハッシュタグが含まれるロードアウトを持つアイテムを表示します。", "Infusable": "融合強化に使用できるアイテムを表示します。", "InfusionFodder": "高パワーの同じアイテムとグリマーだけで融合できる、低パワーのアイテムを表示します。", "IsAdept": "熟練Modが利用できる武器を表示します。", "IsCrafted": "形成された武器を表示します。", "ItemHash": "与えられた保管庫アイテムハッシュを持つアイテムを表示。上級ユーザー向け。", "ItemId": "与えられた保管庫アイテム ID を持つアイテムを表示。上級ユーザー向け。", "Leveling": { "Complete": "{{term}} - アップグレードを完了したアイテムを表示", "Incomplete": "{{term}} - アップグレードが完了してないアイテムを表示", "NeedsXP": "{{term}} - 経験値を積むことができるアイテムを表示", "Upgraded": "{{term}} - 全ノードを開放できるが、まだ開放していないアイテムを表示", "XPComplete": "{{term}} - 経験値を積むことが出来ないアイテム (アップグレードの完了に関わらず) を表示" }, "Location": "アプリケーション内の位置に基づいて項目が表示されます。left/middle/rightはcharの視覚的な位置です。inleftcharは常に動作しますが、残りの2つはユーザの文字数に基づいています。currentは最後/現在のログ文字です(黄色い三角形で示されています)。", "LockAllFailed": "アイテムのロックに失敗しました", "LockAllSuccess": "{{num}} 個のアイテムをロックしました", "Locked": "ロック中またはロック解除されているアイテムだけを表示します。", "Masterwork": "マスターワークのステータス値またはレベルに基づいてアイテムを表示します。", "MasterworkKills": "マスターワークのキルトラッカー数に基づいてアイテムを表示します。", "MaxPower": "スロット毎の最大出力でアイテムを表示します。", "MaxPowerLoadout": "各キャラクタークラスのパワーレベルを最大化するアイテムのロードアウトを表示します。", "Memento": "記憶を装着している武器を表示します。", "ModSlot": "特定の種類のMODを装着できるスロットを備えたアーマーを表示します。", "Mods": { "Y3": "MODが適用されているアイテムを表示します。" }, "Name": "名前がフィルターテキストと一致する (exactname:)、または部分的に一致する (name:) 項目を表示します。 引用符を使用してフレーズ全体を検索します。", "NamedStat": "ステータスのある防具を表示", "Negate": "検索結果から除外するには、\"{{notexample}}\" や \"{{notexample2}}\" のように、検索語の前にマイナス記号または \"not\" という単語を付けます。", "NewItems": "新しいアイテムを表示", "Notes": "カスタムノートでタグ付けしたアイテムを検索します。", "OriginTrait": "起源特性を持つ武器を表示します。", "Ornament": "装飾可能なアイテム、装飾状況を表示", "PartialMatch": "名前、説明、いずれかのパーク、またはいずれかの mod がフィルターテキストと部分的に一致するアイテムを表示します。 引用符を使用してフレーズ全体を検索します。", "PatternUnlocked": "形成したことがなくても、パターンを所持しているアイテムを表示します。", "Perk": "アイテムの名前や説明に含まれるフィルターのテキストに、パークやmodが部分的に一致しているアイテムを表示します。引用符を使ってフレーズ全体を検索します。", "PerkName": "パークまたはModの名前が一致(exactperk:) または一部該当 (perkname:) のアイテムを表示します。引用符を使用してフレーズ全体を検索します。", "PinnacleReward": "最高峰の報酬を得られる目標を表示します。", "Postmaster": "現在ポストマスターに保管されているアイテムを表示します。", "PowerKeywords": "現在のシーズンのパワー制限を参照するには、数値の代わりにハードキャップまたはソフトキャップキーワードを使用してください。", "PowerLevel": "パワーレベル $t(Filter.PowerKeywords) に基づいてアイテムを表示します。", "PowerfulReward": "完了すると強力な報酬を獲得できる目標を表示します。", "PrismaticDamageType": "光属性または闇属性かに基づいてアイテムを表示します。光属性はアーク、ソーラー、ボイドで、闇属性はステイシスとストランドです。", "Quality": "アイテムステータスの品質により表示。'{{percentage}}'と'{{quality}}'は同じことです。", "RandomRoll": "ランダムロールでドロップされた項目を表示します。", "RarityTier": "指定したアイテムのレアリティで絞り込みます。", "Reforgeable": "銃器技師で融合可能なアイテムを表示", "Release": "特定イベントなどから入手可能な限定アイテムを表示します。", "RequiredLevel": "必要なレベルに応じてアイテムを表示", "RetiredPerk": "入手不可のパークが付与された武器を表示します", "SearchPrompt": "利用可能なフィルタコマンドを検索", "Season": "Destiny 2のシーズンに登場したアイテムを表示します。", "StackFull": "スタック上限に達しているアイテムを表示します(強化のコア、奇妙なコイン、銃器技師の材料など)", "StackLevel": "スタック内のアイテムの量に基づいてアイテムを表示します。", "Stackable": "複数個をスタックして所持できるアイテムを表示します(弾薬合成、奇妙なコインなど)", "StatLower": "同じ種類の防具の中で、他のアーマーよりも厳密にステータスが低い防具を表示します。", "Stats": "特定のステータス値に基づいてアイテムを表示します。 $t(Filter.StatsExtras)", "StatsBase": "装備済のModやマスターワークの増量分を含まず、基礎のステータス値に基づいてアーマーをフィルタリングします。$t(Filter.StatsExtras)", "StatsExtras": "複数のアーマーステータスの数値を+や&記号でつなぐ、高度な絞り込みを実装しました。 また、アーマーのステータス値の中での順位で数値を絞り込むための専用キーワード、highest、secondhighest、thirdhighestなどがあります。各カスタムステータス値にて使用する専用の検索ワードもあります。", "StatsLoadout": "特定の統計の最大合計値を装備するアイテムのセットを検索します。", "StatsMax": "特定の統計情報で最も高い数のアーマーを見つけます。 最高の統計を持つすべてのアイテムが含まれます。", "StatsOrdinal": "指定ステータスに特化したアーマー3.0防具を表示します。", "Tags": { "Tag": "特定のタグが付いているアイテムを表示します。", "Tagged": "タグ付けされたアイテムだけを表示します。" }, "Tier": "レベルに応じたアイテムを表示します。", "Timelost": "\\(時間超越\\)", "Tracked": "クエスト、バウンティのステータスを基に表示", "Transferable": "キャラクター間で移動可能なアイテムを表示します。", "Trashlist": "ウィッシュリストで、ゴミアイテムとしてカウントされているアイテムを表示します", "TunedStat": "指定ステータスのチューニングMODスロットを持つ防具を表示します。", "Unascended": "上昇していない上昇ノードを持つアイテムを表示。", "Undo": "元に戻す", "UnlockAllFailed": "アイテムのアンロックに失敗しました", "UnlockAllSuccess": "{{num}} 個のアイテムのロックを解除しました", "Vendor": "アイテムは特定のベンダーから入手できます。", "VendorItem": "アイテムは非所持かつベンダーで販売されている物だけを表示します。ロードアウト最適化にてベンダーアイテムを除外するのに便利です。", "Weapon": "武器アイテムを表示します。", "WeaponLevel": "武器レベルに基づいて武器を表示します。", "WeaponType": "指定した武器タイプを表示します", "Wishlist": "ウィッシュリストに一致するアイテムを表示します。", "WishlistDupe": "重複アイテムのうち、ウィッシュリストのパークで1つでも合致しているものがあれば表示します。", "WishlistEnabled": "ウィッシュリストロールの対象となるアイテムを表示します。", "WishlistNotes": "ウィッシュリストのメモにある単語と一致するアイテムを表示します。", "WishlistUnknown": "ウィッシュリストと比較して、1つも推奨ロールがなかったアイテムを表示します。", "Year": "何年目から登場したアイテムかを絞り込みます。" }, "General": { "ClickForDetails": "クリックして詳細表示", "Close": "終了", "Confirm": "本当に消しますか?", "UserGuideLink": "ユーザーガイド" }, "Glyphs": { "Axe": "斧", "DarkAbility": "暗黒スキル", "Gilded": "金", "Harmonic": "ハーモニック", "HiveSword": "ハイブソード", "LightAbility": "光スキル", "LightLevel": "光レベル", "Misadventure": "不運", "Missing": "紛失", "OpenSymbolsPicker": "シンボルピッカーを開く", "Prismatic": "プリズマティック", "Quickfall": "クイックフォール", "RespawnRestricted": "リスポーン制限", "ScorchCannon": "スコーチキャノン", "SearchSymbols": "アイコン検索...", "Smoke": "煙幕" }, "Header": { "About": "DIMについて", "AutoRefresh": "プレイ中ならば DIM は自動的に再読み込みされます。", "BulkTag": "選択したアイテムにタグを付けています", "BungieNetAlert": "Bungie Alert", "Clear": "フィルターをクリア", "CompareMatching": "アイテムを比較する", "DeleteSearch": "検索の削除", "FilterHelp": "検索 アイテム/パーク, {{example}},", "FilterHelpBrief": "アイテムを検索する", "FilterHelpLoadouts": "ロードアウトの名前やノートを検索", "FilterHelpMenuItem": "フィルターヘルプ", "FilterHelpOptimizer": "特定のビルドに含まれる防具をフィルターします。例:{{example}}", "FilterHelpProgress": "マイルストーンとバウンティーを検索", "FilterHelpRecords": "勝利の道のりとコレクションを検索する", "FilterMatchCount_other": "{{count}} アイテム", "Filters": "フィルター", "InstallDIM": "アプリとしてインストールする", "InstallDIMBanner": "DIM をホーム画面のアプリとしてインストール", "Inventory": "所持品", "IosPwaPrompt": "Safariで共有アイコン(下の真ん中のボタン)をクリックし、\"ホーム画面に追加\"を選択します。", "KeyboardShortcuts": "キーボード ショート カット", "LaunchDIMAlone": "ウィンドウを分割する", "MaterialCounts": "素材数", "Menu": "メニュー", "ProfileAge": "Destinyサーバーの最終更新は {{age}} (時/分/秒)前です。DIMで更新しても、Bungie.netが古い情報を返すことがあります。", "Refresh": "更新する [R] キー", "ReloadApp": "アプリの再読み込み", "ReportBug": "バグの報告", "SaveSearch": "検索を保存", "SearchActions": "検索アクションを開く", "SearchResults": "アイテムを表示", "Shop": "ショップ", "TagAs": "{{tag}} としてタグ", "UpgradeDIM": "DIMのアップデート", "WhatsNew": "新着情報" }, "Help": { "CannotMove": "キャラクターからこのアイテムを外せません", "NoStorage": "DIM のデータ保存機能が使用できません", "NoStorageMessage": "ブラウザにデータを保存できませんでした。プライベートモードやシークレットモードでの閲覧、空きディスク容量が少ない、またはブラウザのバグが考えられます。まずはPCを再起動してみてください。この問題を解決するまで、DIMへのログイン・使用はできません。" }, "Hotkey": { "Armory": "アイテムの武器庫を表示", "CheatSheetTitle": "キーボード ショートカット:", "ClearDialog": "ダイアログを閉じる", "ClearNewItems": "新しいアイテムを既読済にする", "Enter": "入力する", "ItemPopupTab": "アイテム詳細タブへ切り替え", "LockUnlock": "アイテムをロックまたはロック解除する", "MarkItemAs": "アイテムを '{{tag}}' とマークする", "Menu": "ゲームメニューを表示", "Note": "メモを入力", "Pull": "アイテムを使用中キャラに持たせる", "RefreshInventory": "所持品を更新", "ShowHotkeys": "キーボードショートカットを表示", "StartSearch": "検索開始", "StartSearchClear": "新規検索", "Tab": "タブ", "Vault": "アイテムを保管庫に送る" }, "InGameLoadout": { "ClearSlot": "スロット {{index}} を削除", "Create": "ロードアウトを作成", "CreateTitle": "現在の装備からゲーム内ロードアウトを作成", "CurrentlyEquipped": "現在装備中", "DeleteFailed": "ロードアウトを削除できませんでした", "Deleted": "ロードアウトを削除しました", "DeletedBody": "スロット {{index}} のゲーム内ロードアウトをクリアしました。", "EditFailed": "ロードアウトの更新に失敗", "EditIdentifiers": "アイコンとタイトル変更", "EditTitle": "ロードアウト名とアイコンを編集", "EquipNotReady": "所持品内にアイテムが揃っていません", "EquipReady": "ゲーム内で切替可能", "LoadoutDetails": "ロードアウト詳細", "MatchingLoadouts": "一致するロードアウト:", "PrepareEquip": "ロードアウト切替準備", "Replace": "ロードアウト {{index}} を置き換え", "Save": "ロードアウトを更新", "SaveIdentifiers": "アイコンを更新", "SnapshotFailed": "装備しているロードアウトのスナップショットに失敗しました" }, "Infusion": { "Filter": "アイテムをフィルタする", "InfuseSource": "{{name}} と融合するアイテムを選択してください", "InfuseTarget": "{{name}} と融合するアイテムを選択してください", "InfusionMaterials": "融合材料", "NoItems": "融合可能なアイテムありません。", "NoTransfer": "融合材料を転送\n {{target}} 移動できません.", "SwitchDirection": "変更", "TransferItems": "転送" }, "Inventory": { "ClickToExpand": "(クリックして展開)", "MissingSilver": "シルバーの残高は、ゲームプレイ中のみ表示されます。" }, "Item": { "SetBonus": { "NPiece_other": "{{count}} 個" }, "ThumbsDown": "低評価", "ThumbsUp": "いいね" }, "ItemFeed": { "ClearFeed": "フィードをクリア", "Description": "アイテムフィード", "HideTagged": "タグ付けされたものを隠す", "NoNewItems": "新しいアイテムはありません", "ShowOlderItems": "古いアイテムを表示" }, "ItemMove": { "Consolidate": "{{name}} が統合されました", "Distributed": "分散 {{name}}\n {{name}} は文字間で等しく分割されました。", "MovingItem": "保管庫に転送", "MovingItem_female": "{{target}} に転送", "MovingItem_male": "{{target}} に転送", "ToStore": "{{name}} が全て {{store}} にあります。", "ToVault": "{{name}} 今は全て保管庫にあります。" }, "ItemPicker": { "ChooseItem": "アイテムを選択", "SearchPlaceholder": "アイテムを検索する" }, "ItemService": { "BucketFull": { "Guardian": "{{store}} には {{itemtype}} のスペースがありません。", "Guardian_female": "{{store}} には {{itemtype}} のスペースがありません。", "Guardian_male": "{{store}} には {{itemtype}} のスペースがありません。", "Vault": "{{store}} には {{itemtype}} のスペースがありません。" }, "Classified": "この商品は現在分類されており、現時点では譲渡できません。", "Classified2": "このアイテムは機密事項に指定されています。現時点では Bungie から情報が公開されていません。\n気になる場合はこのアイテムにメモを追加し、データ公開がされてから「notes:」検索フィルターで検索できるようにしておくことをお勧めします。", "Deequip": "{{itemname}} を取り除くために装備するアイテムが見つかりません", "ExoticError": "'{{itemname}}' は装備できません。なぜなら{{slot}} エキゾチックなものは装備できないからです。({{error}})", "NotEnoughRoom": "{{itemname}} のスペースを確保するために{{store}} から移動することはできません。", "NotEnoughRoomGeneral": "スペースが不足しているためアイテムを移動できません", "OnlyEquippedClassLevel": "これは、レベル{{level}} 以上の{{class}} にしか装備できません。", "OnlyEquippedLevel": "レベル {{level}} 以上のキャラクターのみ装備が可能です。", "PostmasterAlmostFull": "ほぼ満杯!", "PostmasterFull": "満杯!", "PreviewVendor": "{{type}} コンテンツのプレビュー", "StackFull": "{{name}} はすでに上限数まで持っているのでこれ以上取得できません。", "StoreName": "{{genderRace}} {{className}}" }, "KillType": { "ClassAbilities": "クラスアビリティ", "Finisher": "フィニッシャー", "Grenade": "グレネード", "Melee": "近接攻撃", "Precision": "精密", "Super": "スーパースキル" }, "LB": { "AddStack": "この Mod を1つスタック追加する", "AdvancedOptions": "高度なオプション", "ChooseAMod": "使用するmodを選ぶ", "ChooseASetBonus": "使用するセットボーナスを選択", "ChooseAnExotic": "使用するエキゾチックを選ぶ", "ClearLocked": "ロックを解除", "ContainsVendorItems": "装備にベンダーアイテムが含まれています。", "Current": "現在", "Equip": "{{character}}に装備する", "Exclude": "除外するアイテム", "ExcludeHelp": "特定のギアを持たずにセットを構築するには、アイテムをShift +クリック(またはこのバケットにドラッグアンドドロップ) します。", "ExistingBuildStats": "現在のビルドステータス", "ExistingBuildStatsNote": "高Tierのビルドのみ表示します。", "FilterSets": "フィルターセット", "Help": { "And": "これのパークをすべて装備した防具が使用されます (\"and\")", "ChangeNodes": "ロードアウトを作成するには知性、鍛錬、腕力の割合を表示のように変更して下さい.", "Discipline": "鍛錬はグレネードのクールダウン時間を短縮する", "DragAndDrop": "アイテムをドラッグしてロックされたバケットにドロップすると、その装備のみのセットが作成されます", "Help": "助けが必要?", "HigherTiers": "階層が高いほど良い", "Intellect": "知性はスーパースキルのクールダウン時間を短縮する", "Lock": "クリックするとパークを固定できます。", "MultiPerk": "複数のパークを持つ防具を一緒に使用するには、希望のパークをシフト+クリックします", "NoPerk": "パークが表示されない場合は、そのパークを持つ防具を所有していないことを意味します。", "Or": "これらのパークのいずれかを持つ防具アーマーが使用されます (\"or\")", "ShiftClick": "アイテムをShiftキーを押しながらクリックすると、そのギアがないセットが作成されます", "StatsIncrease": "アイテムの防御レベルが上昇するとともに、アイテムのステータス(知性/鍛錬/腕力) も上昇します。", "Strength": "腕力は近接スキルのクールダウン時間を短縮する", "Synergy": "使用する武器の種類に応じて弾薬が増加するパークがある防具を見つけてください。", "Tier11Example": "4/5/2(ティア11装備) は知性4、鍛錬5、腕力、2(4+5+2=ティア11)" }, "HideAllConfigs": "全ての設定を隠す", "HideConfigs": "設定を隠す", "IncompatibleWithOptimizer": "このアイテムは Optimizer と互換性がありません。コレクションから新しいバージョンを再入手してください。", "LB": "ロードアウト最適化", "LightMode": { "HelpCurrent": "現在の防衛レベルでの負荷を計算します。", "HelpScaled": "すべてのアイテムが350防御であるかのように負荷を計算します。", "LightMode": "ライトモード" }, "Loading": "ベストセットをロードする", "LockEquipped": "装備をロック", "LockPerk": "パークを固定する", "Locked": "ロックされたアイテム", "LockedHelp": "アイテムをドラッグ&ドロップすることでそれを固定でセット作成できます。排除するには Shift + クリック.", "Missing2": "フルセットを作るためのレア、レジェンダリー、エキゾチックの部位が足りません!", "ProcessingMode": { "Fast": "最速", "Full": "すべて", "HelpFast": "あなたの最高の装備だけを見てください。", "HelpFull": "装備が多く見つかりますが、時間がかかります。", "ProcessingMode": "処理モード" }, "RemoveStack": "この Mod をスタックから1つ外す", "Scaled": "縮尺", "SearchAMod": "Mod名または説明で検索", "SearchASetBonus": "", "SearchAnExotic": "エキゾチック名または説明で検索", "SelectExotic": "使用するエキゾチックを選ぶ", "SelectMods": "Mod を選択", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} アクティビティMod", "SelectSetBonus": "セットボーナスを選択", "SelectSubclassOptions": "サブクラスをカスタマイズ", "ShowAllConfigs": "全ての設定を表示する", "ShowConfigs": "設定を表示する", "ShowGear": "{{class}} アーマー", "Vendor": "ベンダー装備も含む" }, "Loading": { "Accounts": "Destinyのアカウント情報をロードしています...", "Code": "DIMのコードを読み込み中…", "FilterHelp": "検索ヘルプをロードしています...", "Profile": "Destinyのプロファイルをロードしています...", "Vendors": "Destinyのベンダー情報をロードしています..." }, "LoadoutAnalysis": { "Analyzed": "{{numLoadouts}} 個のロードアウトの分析が完了しました", "Analyzing": "{{numAnalyzed}}/{{numLoadouts}} 個のロードアウトを分析中", "BetterStatsAvailable": { "Description": "別のアーマーやModだと、より高いステータス値に上げられます。\"$t(Loadouts.OpenInOptimizer)\" を選択して結果を確認してください。", "Name": "より高いステータス値のビルドが作成可能" }, "BetterStatsAvailableFontNote": "注:このロードアウトは Mod「~の泉」を使用しTier10以上になっています。DIMなら不要なTier上限突破を減らして、より良いステータスを検索できるかもしれません。敢えて使用する場合は、ロードアウトの\"$t(Loadouts.IncludeRuntimeStatBenefits) \"を無効にしてください。", "DoesNotRespectExotic": { "Description": "ロードアウト最適化で指定されているエキゾチックアーマーと、このロードアウトのエキゾチックアーマーが一致しません。", "Name": "指定したエキゾチックアーマーと違います" }, "DoesNotSatisfyStatConstraints": { "Description": "このロードアウトのステータス値は、指定された最小値に届いていません。", "Name": "間違ったステータス最小値" }, "EmptyFragmentSlots": { "Description": "このサブクラスには未使用のかけらスロットがあります。", "Name": "サブクラスのかけらスロットに空き有り" }, "InvalidMods": { "Description": "このロードアウト内にある一部のModが廃止されているか、アーマーに適用できないようです。", "Name": "廃止済みMOD" }, "InvalidSearchQuery": { "Description": "このロードアウトは、最適化ツールにて実際成立しない条件で作成されています。", "Name": "無効な検索フィルター" }, "ItemsDoNotMatchSearchQuery": { "Description": "このロードアウトは最適化ツールで作成されましたが、条件外のアイテムが含まれています。", "Name": "検索条件から除外するアイテム" }, "MissingItems": { "Description": "このロードアウトに含まれる装備の一部は所持品にありません。", "Name": "アイテム不足" }, "ModsDontFit": { "Description": "このロードアウトのアーマーを強化しても、指定したMOD全てを装備できません。", "Name": "割り当てられていないMODがあります" }, "NeedsArmorUpgrades": { "Description": "このロードアウトのアーマーは、指定された全Modの装備もしくはアップグレードが必要です。", "Name": "アーマーのアップグレードが必要" }, "NotAFullArmorSet": { "Description": "このロードアウトには全部位のアーマーが含まれていないため、解析できませんでした。", "Name": "非全身アーマー" }, "TooManyFragments": { "Description": "サブクラス特性で利用可能のスロットより多くのかけらが指定されています。", "Name": "サブクラスのかけらの数が多すぎます" }, "UsesSeasonalMods": { "Description": "このロードアウトはシーズン限定のMODを使用しています。シーズン終了後、これらのMODは使用できなくなるか、アーマーのエネルギー容量を超過します。", "Name": "シーズンMODを使用" } }, "LoadoutBuilder": { "All": "全て", "AlwaysAutoMods": "常に戦略的アーマーとチューニングModは自動で選択します。", "AnyExotic": "任意のエキゾチック", "AnyExoticDescription": "指定なしで何らかのエキゾチックが必ず入るようになります。", "Artifice": "戦略的アーマー", "AssumeMasterwork": "マスターワーク済みと仮定", "AssumeMasterworkOptions": { "All": "全アーマー: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "全アーマー: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nアーマー2.0 エキゾチック: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "戦略的アーマー用ステータスMODを装着できるまで強化済みと想定。", "Current": "現在のステータスで、エネルギーは少なくとも {{minLoItemEnergy}} を想定。", "Legendary": "レジェンダリー: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nエキゾチック: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "マスターワーク済防具を、全ステータス10+と想定します。", "None": "全アーマー: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "自動的にステータス mod を追加する", "AutomaticallyPicked": "この mod はビルドのステータス値を改善するため自動的に追加されました。", "CompareLoadout": "ロードアウトを比較", "ConfirmOverwrite": "ロードアウト「{{name}}」のアーマーをこの新しいアーマーセットに置き換えてもよろしいですか?", "DecreaseStatPriority": "ステータス優先度を下げる", "DisabledByAutoStatMods": "ステータス値に関するModはロードアウト最適化機能によって自動的に選択されています。", "DisabledDueToMaintenance": "現在、Bungie API のメンテナンスによりロードアウト最適化が利用出来ません。", "EquipItems": "装備する", "ExcludeItem": "組み合わせから外すアイテムを指定", "ExcludeVendors": "ロードアウト最適化からベンダーアイテムを除外するには、\"not:vendor\"で検索してください。", "ExcludedItems": "除外するアイテム", "ExistingLoadout": "既存のロードアウト", "Exotic": "エキゾチックアーマー", "ExoticClassItemPerks": "特定のパークを探す場合は、exactperk:\"ベリティの精神\"のようなワードで検索してください。 パークをクリックするとアイテムフィルタからロードアウト最適化に追加または削除します。", "ExoticSpecialCategory": "スペシャル", "FOTLWildcardWarning": "このセットには、死者の祭りのマスクが含まれています。手動で適切なMODを選択して、希望するセットボーナスを有効にします。", "Filter": "設定", "IgnoreStat": "チェックを外すと、このステータスを無視してビルドを探索します。", "IncreaseStatPriority": "ステータス優先度を上げる", "Legendary": "レジェンダリー", "LimitToNewFeaturedGear": "新しい装備/注目の装備に限定", "LockItem": "アイテムをピン留めする", "MissingClass": "ビルド: {{className}}", "MissingClassDescription": "表示しようとしているビルドは、あなたが持っていないキャラクタークラス向けです。", "MwExotic": "エキゾチック", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "現在使用している検索フィルターで、ビルドに使える装備がありませんでした", "AllowAutoStatMods": "DIM に追加のステータス mod を自動的に含めることを許可する", "AlwaysInvalidMods": "これらの mod は所有しているどのアイテムにも適合しません :", "AssumeMasterworked": "DIM がマスターワークしているアーマーを推奨できるようにする", "AssumptionsRestricted": "DIM は防具のエネルギー変更を推奨できません :", "BadSlot": "{{bucketName}} スロットでは、許可されたアイテムのいずれもこれらの mod に対応できません :", "ExoticDoesNotExist": "選択されたエキゾチックアーマーを所持していません。", "Header": "ビルドが見つかりませんでした。見つけられなかった理由は次のとおりです:", "LowerBoundsFailed": "どの組み合わせでも指定した最小ステータス値に届きませんでした", "MaybeAllowMoreItems": "以下のアイテムが問題の原因と思われます。条件を変えてみてください:", "MaybeDecreaseLowerBounds": "最小ステータス値を下げることをおすすめします", "MaybeRemoveMods": "mod を削除することを検討してください :", "MaybeRemoveSearchQuery": "検索バーに追加しているフィルターを消すか、別のフィルターに変更してみてください", "ModAssignmentFailed": "どの所持アイテムを組み合わせても指定したModを装着できませんでした", "RemoveMods": "これらの mod を削除", "RemoveSetBonuses": "セットボーナスの指定を解除することをおすすめします", "SetBonuses": "セットボーナスを指定されていますが、条件を満たすアイテムがありませんでした。" }, "NoExotic": "エキゾチックなし", "NoExoticDescription": "検索バーで \"not:exotic\" を検索するのと同じです。エキゾチックアーマーを意図的に組み合わせから排除します。", "NoExoticPreference": "必要な時だけエキゾチックを装備", "NoExoticPreferenceDescription": "最大ステータスにできる時だけエキゾチックアーマーを選択します。", "NoLoadoutsToCompare": "比較するロードアウトはありません。", "None": "無し", "OptimizerExplanationGuide": "詳細については、ユーザーガイドまたはビデオチュートリアルをご覧ください。", "OptimizerExplanationMods": "エキゾチック、Mod、サブクラスを選択します。これらはビルドにステータス値を影響を与えますが、すでにアーマーにあるModは無視されます。", "OptimizerExplanationSearch": "検索バーを使って候補となるアーマーを絞り込みます(例: {{example}} )。条件に一致するアーマーがない場合、すべてのアイテムが対象となります。", "OptimizerExplanationStats": "マウスドラッグで最も優先したい順に上からステータスを並び替え、ビルドに不要なステータスはチェックを外します。", "OptimizerSet": "最適化セット", "PinnedItems": "ピン留めしたアイテム", "PinnedItemsFinePrint": "検索フィルターはロードアウト最適化の設定で保存されますが、ピン留めまたは除外された項目は保存されません。既存のロードアウトをチェックすると、ピンと除外した項目は無視されます。", "ProcessingSets": "最高理論値の組み合わせを検索しています…", "SaveAs": "名前をつけて保存", "SetBonus": "セットボーナス", "SpeedReport": "PCのCPUコア {{cpus}} コアを使用し、{{time}} 秒で {{combos, number}} 種類の組み合わせを探しました。", "StatConstraints": "ステータス値の優先度と範囲", "StatMax": "最大", "StatMin": "最小", "StatRangeTooltip": "現在の最小/最大の設定では、ロードアウトが存在します。ロードアウトは {{min}} から {{max}} があります。ダブルクリックして最小値を {{max}}に設定します。", "StatTotal": "合計: {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "全てのModを追加することはできません。", "UnableToAddAllModsBody": "{{mods}} を適用するのに十分なModスロットがありませんでした。", "UnlockItem": "アイテムのピン留めを解除" }, "LoadoutFilter": { "Contains": "フィルターテキストに一致するアイテムまたはMODを持つロードアウトを表示します。アイテム名にスペースが含まれる場合、引用符で囲んで検索してください。", "FashionOnly": "ファッション(シェーダーまたは装飾品)のみを含むロードアウトを表示します。", "LoadoutLight": "計算された光のレベルに基づいてロードアウトを表示します。 今シーズンのパワー制限を参照するには、パワー値の代わりに pinnaclecap または softcap キーワードを使用してください。", "ModsOnly": "アーマーModのみが搭載されたロードアウトを表示します。", "Name": "名前が完全一致 (exactname:) もしくは部分一致 (name:) するロードアウトをテキストフィルターで表示します。引用符を使用してフレーズ全体を検索してください。", "Notes": "メモフィールドでロードアウトを検索する。", "PartialMatch": "名前またはメモが、フィルターテキストと部分的に一致するロードアウトを表示する。 引用符を使用してフレーズ全体を検索する。", "Season": "シーズンごとに作成してきたロードアウトをフィルタリングします。", "Subclass": "フィルターワードに部分的に一致するサブクラス名またはダメージタイプのロードアウトを表示します。" }, "Loadouts": { "Abilities": "アビリティ", "Actions": "{{title}} をアクション", "AddEquippedItems": "設備品を追加", "AddNotes": "メモを追加する", "AddUnequippedItems": "所持品に加える", "Any": "全てのクラス", "Apply": "適用", "ApplyInGameLoadoutInGame": "ロードアウトの準備ができていますがアクティビティ中ため、ゲーム内で装備する必要があります。", "ApplyMods": "Modを適用", "ApplySearch": "検索 {{query}} を転送", "ArmorStats": "防具のステータス", "ArtifactUnlocks": "アーティファクトのロック解除", "ArtifactUnlocksDesc": "Bungie.net の仕様で DIM はアーティファクトmodをロードアウトに合わせて解除変更できません。ロードアウトを適用する前に、ゲーム内で使用するModのロック解除をする必要があります。", "ArtifactUnlocksWithSeason": "解除済のアーティファクト – シーズン{{seasonNumber}}", "BadLoadoutShare": "シェアしたロードアウトを読み込めません", "BadLoadoutShareBody": "読み込もうとしているロードアウトは無効です: {{error}}", "Before": "前 '{{name}}'", "CancelEditing": "編集をキャンセルする", "CannotCustomizeSubclass": "このサブクラスは設定することができません", "ChooseItem": "{{name}} を追加", "ClassType": "全クラスのロードアウト", "ClassTypeMismatch": "{{className}} アイテムはこのロードアウトに追加できません", "ClassTypeMissing": "ロードアウトを作成する為の {{className}} がありません", "ClassType_female": "{{className}} ロードアウト", "ClassType_male": "{{className}} ロードアウト", "Classified": "一部のアイテムは分類されており、最大出力の計算に含めることはできません。", "ClearLoadoutParameters": "ロードアウトの設定を削除", "ClearSection": "すべて削除", "ClearSpace": "他を移動する", "ClearSpaceArmor": "他の防具を移動する", "ClearSpaceWeapons": "他の武器を移動する", "ClearUnsetMods": "他の MOD を削除", "ClearingSpace": "他のアイテムを移動する", "CopyAndEdit": "名前を付けて編集", "Create": "ロードアウトを作成", "CurrentlyEquipped": "現在装備中", "Deequip": "他のキャラクターからアイテムを外す", "Delete": "削除", "DimLoadouts": "DIMロードアウト", "Edit": "ロードアウトを編集", "EditBrief": "編集", "EquipInGameLoadout": "ゲーム内ロードアウトを装備中", "EquipItems": "アイテムを装備", "EquippableDifferent1": "複数のエキゾチックアイテムを使用して最大パワーを計算しました。ゲーム内でアイテムを装備しようとしても、数値が計算通りにならない恐れがあります。", "EquippableDifferent2": "ドロップ/強力/最高峰の報酬で最大パワーを決定する際、最大パワーは通称「1つのエキゾチック」ルールによって制限されません。", "Failed": "ロードアウトを完全に適用できませんでした", "Fashion": "ファッションを選択", "FashionOnly": "装飾のみ", "FillFromEquipped": "装備中のアイテムで埋める", "FillFromInventory": "装備していないアイテムで埋める", "FilteredItems": "フィルタ処理された項目", "FindAnother": "別の {{name}} を検索する", "FromEquipped": "装備済み", "Generated": "{{statTotal}} ステータス値のロードアウト", "HashtagTip": "ヒント: ロードアウトの名前やメモに #hashtags を使用すると、ここに表示されます。", "Import": { "BadURL": "有効なロードアウト共有 URL ではありません", "Error": "ロードアウト取得中のエラー", "Error404": "このロードアウトは存在しません", "PasteHere": "ロードアウトリンクを貼り付けて、ロードアウトを開きます。" }, "ImportLoadout": "ロードアウトをインポート", "InGameActions": "ゲーム内ロードアウトアクション", "InGameLoadouts": "ゲーム内ロードアウト", "IncludeRuntimeStatBenefits": "Mod「~の泉」の反映後の数値としてカウントする", "IncludeRuntimeStatBenefitsDesc": "アーマーMod「~の泉」は、アーマーチャージの間はキャラクターのステータスを一律に上昇させます。\nこの設定を有効にすると、DIMはこれらのModがアクティブの状態での数値を、ロードアウト最適化のステータス値として計算し反映させます。", "ItemErrorSummary_other": "{{count}} 個のアイテムでエラー:", "ItemLeveling": "アイテムのレベルアップ", "LoadoutName": "ロードアウトの名前...", "LoadoutParameters": "ロードアウト最適化の設定", "LoadoutParametersExotic": "ロードアウトにエキゾチックアイテムを選んでください: {{exoticName}}", "LoadoutParametersQuery": "アイテムは検索フィルタと一必ず致させてください", "LoadoutParametersStats": "ステータス値の優先順位と最小/最大ステータス値範囲", "Loadouts": "ロードアウト", "MakeRoom": "ポストマスターのための部屋を作る", "MakeRoomDone_female_other": "{{store}} から{{movedNum}} 個のアイテムを移動して、{{count}} 個のポストマスターアイテム用のスペースを作成しました。", "MakeRoomDone_male_other": "{{store}} から{{movedNum}} 個のアイテムを移動して、{{count}} 個のポストマスターアイテム用のスペースを作成しました。", "MakeRoomDone_other": "{{store}} から{{movedNum}} 個のアイテムを移動して、{{count}} 個のポストマスターアイテム用のスペースを作成しました。", "MakeRoomError": "ポストマスターの全アイテム用のスペースを確保できませんでした: {{error}} 。", "ManageLoadouts": "ロードアウトの管理", "MaxSlots": "ロードアウトには {{slots}} {{bucketName}} しかありません。", "MaximizeLight": "最大 Light", "MaximizePower": "最大パワー", "MaximizeStat": "統計を最大化する", "MissingItemsWarning": "このロードアウトに含まれる装備の一部は所持品にありません。", "ModErrorSummary_other": "{{count}} 個のmod エラー:", "ModPlacement": { "InvalidMods": "無効な mod", "InvalidModsDesc_other": "どのアーマーにも適用できないmodが {{count}} 個見つかりました。", "ModPlacement": "Mod を入れ替え", "StackableMod": "スタック可能", "UnassignedMods": "割り当てられていないMODがあります", "UnassignedModsDesc_other": "エネルギー容量またはMODスロットが不足しているため、 {{count}} つの mod がフィットしませんでした。選択した防具にエネルギーをアップグレードしても、この問題は解決されません。", "UnstackableMod": "スタック不可", "UpgradeCosts": "アップグレードコスト", "UpgradeCostsDesc": "一部のアーマーで適用する mod に対してエネルギーが不足しています。アップグレードに必要な合計コスト:" }, "Mods": "Mod", "ModsOnly": "Modのみ", "MoveItems": "アイテムを移動しています", "NoSpace": "保管庫や他のキャラクターの空きスペースがありません。", "NoneMatch": "フィルタに一致するロードアウトはありませんでした。", "NotStarted": "他のアクションの完了、もしくは保管庫の更新による読み込みの終了を待っています。", "NotesPlaceholder": "このロードアウトに関するメモを書くか、 オリジナルの #hashtag を書くと分類しやすくなります", "NotificationTitle": "ロードアウト: {{name}}", "OnWrongCharacterAdvice": "このキャラクターの最高パワーのアイテムはこちら。", "OnWrongCharacterWarning": "このキャラクターの最も強力なアーマーは、別のキャラクターが所持しています。 パワーのドロップ、強力、最高峰の報酬にカウントするには、アーマーはこのキャラクターか保管庫に存在しなければなりません。", "OnlyItems": "装備可能なアイテム、マテリアル、消耗品のみをロードアウトに追加することができます。", "OpenInOptimizer": "防具の最適化", "OpenOnStreamDeck": "Stream Deckで開く", "PickArmor": "アーマーを選択", "PickMods": "アーマー mod を追加", "Prismatic": { "Aspect": "プリズム特性", "Grenade": "プリズムグレネード", "Melee": "プリズム近接", "Super": "スーパースキル" }, "PullFromPostmaster": "ポストマスターからアイテムを取得する", "PullFromPostmasterError": "ポストマスターから転送できません:{{error}}", "PullFromPostmasterGeneralError": "ポストマスターからすべてのアイテムを取得できません。", "PullFromPostmasterNotification_female_other": "{{count}} ポストマスターアイテムを{{store}} に引き出します。", "PullFromPostmasterNotification_male_other": "{{count}} ポストマスターアイテムを{{store}} に引き出します。", "PullFromPostmasterNotification_other": "{{count}} ポストマスターアイテムを{{store}} に引き出します。", "PullFromPostmasterPopupTitle": "ポストマスターから取得", "Random": "ランダム", "Randomize": "ロードアウトのランダム化", "RandomizeButton": "ランダム化", "RandomizeNew": "ランダムに作成", "RandomizeQueryHint": "ヒント : 最初にアイテムを検索し、ランダムに選択できるアイテムを制限する。", "RandomizeSearch": "ランダム検索", "RandomizeSearchPrompt": "検索検\"{{query}}\"から装備アイテムをランダム化しますか?", "Redo": "やり直す", "RestoreAllItems": "全アイテム", "SalvationsEdgeMods": "救済の境界Mod", "Save": "保存", "SaveAsDIM": "DIM ロードアウトとして保存", "SaveAsNew": "新規保存", "SaveAsNewTooltip": "最初の装備を、新しいロードアウトとして保存", "SaveDisabled": { "AlreadyExists": "ロードアウトの新しい名前を選択。", "Empty": "ロードアウトが空です。", "NoName": "ロードアウトには名前が必要です。" }, "SaveLoadout": "ロードアウトを保存", "Season": "シーズン{{season}}", "SetBonusesDesc": "指定したセットボーナスを必ず含む", "Share": { "Copied": "ロードアウトのリンクをクリップボードにコピーしました", "CopyButton": "リンクをコピーする", "Error": "共有リンクの取得に失敗しました", "Fashion": "ファッション (シェーダー・装飾)", "LoadoutOptimizer": "ロードアウト最適化の設定", "NativeShare": "リンクを共有する", "Notes": "メモ", "NumItems_other": "{{count}} 個 - 受信者は在庫から同等のアイテムを選択するよう求められます", "NumMods_other": "{{count}} 個のmod", "Placeholder": "共有リンクをロードしています", "Subclass": "サブクラスのカスタマイズ", "Summary": "以下を含むこのロードアウトをシェアする:", "Title": "\"{{name}}\" をシェアする" }, "ShareLoadout": "共有", "ShowModPlacement": "装備中のModを表示", "Snapshot": "ゲーム内ロードアウトとして保存", "SocketOverrides": "サブクラスオプションの変更", "SortByEditTime": "最終更新でソート", "SortByName": "名前でソート", "SubclassOptions": "{{subclass}} オプション", "SubclassOptionsSearch": "{{subclass}} オプションを検索", "Succeeded": "ロードアウトに成功", "SyncFromEquipped": "装備中から同期する", "TooManyRequested": "あなたは{{total}} {{itemname}} を持っていますが、あなたのロードアウトは{{requested}} を要求します。あなたが持っていた物をすべてを転送しました。", "TuningMods": "チューニングMod", "UnassignedModError": "Modが現在のアーマーに適用できませんでした", "Undo": "元に戻す", "Update": "変更を保存", "UpdateLoadout": "ロードアウトを更新", "VendorsCannotEquip": "これらのアイテムはありません。タップして交換を選択するか、Xをクリックして削除します。" }, "Manifest": { "Download": "最新のDestiny情報をBungieからダウンロードしています...", "Error": "Destinyのデータ読み込みに失敗しました:\n{{error}}\nリロードで再試行してください。", "Load": "Destinyデータを読み込み中…" }, "Milestone": { "Daily": "デイリーチャレンジ", "OneTime": "ワンタイムチャレンジ", "SeasonalRank": "シーズンランク {{rank}}", "Special": "スペシャルイベントチャレンジ", "Tutorial": "チュートリアルチャレンジ", "Unknown": "チャレン(挑戦)", "Weekly": "ウィークチャレンジ(週間の挑戦)" }, "Mods": { "HarmonicModDescription": "このModはコストを軽減し、装備するサブクラスで効果を変化します。" }, "MoveAmount": { "Amount": "量 : " }, "MovePopup": { "Acquired": "このアイテムはコレクションでアンロックされています。", "AcquiredMod": "このModはコレクションでアンロックされています。", "AddNote": "メモを追加", "AddToLoadout": "ロードアウト", "AddToLoadoutTitle": "このアイテムで新規ロードアウトを作成する", "All": "全て", "ArtifactBreaker": "アーティファクトパークにより、 {{breaker}} 効果が付与されています。", "CannotCurrentlyRoll": "このアイテムの現在のバージョンでは、このパークをロールできません。", "CantPullFromPostmaster": "このアイテムを取得するには、ゲーム内のポストマスターにアクセスする必要があります。", "CatalystProgress": "媒体の進行", "CommunityData": "コミュニティでの調査結果", "Consolidate": "統合する", "DistributeEvenly": "均等に割り当てる。", "EnhancementTier": "Tier {{tier}}", "Equip": "装備先:", "EquipWithName": "{{character}}に装備する", "FavoriteUnFavorite": { "Favorite": "{{itemType}} をお気に入りに追加する", "Favorited": "お気に入りに追加する", "Unfavorite": "{{itemType}} をお気に入りから解除", "Unfavorited": "お気に入りから解除" }, "Infuse": "融合する", "InfuseTitle": "融合ファインダーを開く", "IntrinsicBreaker": "{{breaker}} 内在効果を持っています。", "LoadingSockets": "このアイテムのパークとステータス値の詳細はまだ調査中です。", "LockUnlock": { "AutoLock": "ロック状態はこのアイテムのタグに同期されます", "Lock": "ロックする {{itemType}}", "Locked": "ロック中", "Unlock": "ロックを解除する {{itemType}}", "Unlocked": "ロック解錠済" }, "MissingSockets": "Bungieがサービスを更新している間は、パークとMODの詳細は利用できません。通常は数時間で、それらが完了すると戻ります。", "Notes": "メモ:", "OpenOnStreamDeck": "Stream Deckで開く", "OverviewTab": "概要", "Owned": "このアイテムはあなたの所持品にあります。", "OwnedMod": "このmodは修正インベントリにあります。", "PullItem": "{{bucket}} から{{store}} に引き出します", "PullPostmaster": "ポストマスターから取得", "ReadLore": "イシュタルコレクティブで伝承を読む", "ReadLoreLink": "伝承を読む", "Rewards": "報酬:", "SendToVault": "保管庫に送る", "Store": "転送先 :", "StoreWithName": "{{character}} に転送する", "Subtitle": { "QuestProgress": "{{questStepNum}} / {{questStepsTotal}}", "Type": "{{classType}} {{typeName}}" }, "TabList": "アイテム詳細タブ", "ToggleSidecar": "アイテムアクションを展開または折りたたむ", "TrackUntrack": { "Track": "{{itemType}} を追跡する", "Tracked": "追跡", "Untrack": "{{itemType}} を追跡しない", "Untracked": "追跡をやめる" }, "TriageTab": "厳選", "UnreliablePerkOption": "このパークは、コレクションビューにのみ表示されます。ランダムアイテムには付かない可能性があります。", "Vault": "保管庫", "WeaponLevel": "武器レベル {{level}}" }, "Notes": { "Error": "エラー!ノートは120文字が上限です。", "Help": "メモ、#hashtag、 :symbols: を追加" }, "Notification": { "Cancel": "キャンセル", "OK": "キャンセル" }, "Objectives": { "Complete": "完成", "Incomplete": "未完" }, "Organizer": { "BulkMove": "転送", "BulkMoveLoadoutName": "主催者で選択", "BulkTag": "タグ", "Columns": { "Ammo": "弾薬", "Archetype": "内在特性", "BaseStats": "基本ステータス値", "Breaker": "ブレイカー", "Crafted": "形成日", "CustomTotal": "カスタム合計", "Damage": "ダメージ", "Energy": "エネルギー", "Event": "イベント", "Featured": "新しい装備", "Foundry": "武器メーカー", "Frame": "フレーム", "Harmonizable": "共振武器", "Holofoil": "ホロフォイル", "Icon": "アイコン", "ItemTier": "レベル", "KillTracker": "倒した数", "Level": "レベル", "Loadouts": "ロードアウト", "Location": "保管場所", "Locked": "ロック中", "MasterworkStat": "MWの種類", "MasterworkTier": "MWレベル", "ModSlot": "Modスロット", "Mods": "Mod", "Name": "名前", "New": "New", "Notes": "メモ", "OriginTraits": "起源特性", "OtherPerks": "武器コンポーネント", "PercentComplete": "% 達成", "Perks": "パーク", "PerksGrid": "パーク一覧", "Power": "パワー", "Quality": "クオリティ %", "Recency": "直近", "Season": "シーズン", "Shaders": "装飾", "Source": "入手先", "StatQuality": "Statsクオリティ", "StatQualityStat": "{{stat}}%", "Stats": "補正後ステータス値", "Tag": "タグ", "TertiaryStat": "内在特性", "Tier": "レア度", "Traits": "武器パーク", "TuningStat": "チューナー", "WishList": "ウィッシュリスト", "WishListNotes": "ウィッシュリストのメモ", "Year": "年" }, "EnabledColumns": "表示項目", "Lock": "ロックする", "NoItems": "フィルタに一致するアイテムがありません。特定の検索ワードがあればそれを外して検索してみてください", "NoMobile": "オーガナイザーを使用するには画面を横向きにしてください。", "Note": "メモを設定", "OpenIn": "オーガナイザーに表示", "Organizer": "オーガナイザー", "SelectAll": "すべて選択", "SelectItem": "{{name}} を選択/解除", "ShiftTip": "ヒント: Shiftキーを押したまま、任意のセルをクリックするとフィルターを掛けることができます", "Stats": { "Aim": "照準補佐", "Airborne": "空中性能", "AmmoGeneration": "弾薬生成", "Power": "パワー", "RPM": "毎分発射数", "Recoil": "リコイル", "Reload": "リロード" }, "Unlock": "ロックを解除する" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "ポストマスターがほぼいっぱいです! ({{number}}/{{postmasterSize}})", "PostmasterFull": "ポストマスターがいっぱいです! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "バウンティ", "CatalystSource": "ソース: {{source}}", "CrucibleRank": "ランク", "Items": "クエストアイテム", "Milestones": "マイルストーンと挑戦", "NoEventChallenges": "すべてのイベントの挑戦を完了しました。", "NoTrackedTriumph": "追跡中の勝利の道のりはありません。DIM で好きなだけ追跡してください。", "PaleHeartPathfinder": "ペイルハートパスファインダー", "PercentMax": "最高まで{{pct}}%達成", "PercentPrestige": "リセットまで{{pct}}%達成", "PointsUsed_other": "{{count}} ポイント使用中", "PowerBonusHeader": "+{{powerBonus}} パワーボーナス", "PowerBonusHeaderUndefined": "その他の報酬", "Progress": "進行状況", "QueryFilteredTrackedTriumphs": "検索結果に、追跡中の勝利の道のりはありませんでした。", "QuestExpired": "期限切れ", "QuestExpires": "有効期限 ", "Quests": "クエスト", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}pts", "Resets_other": "{{count}} リセット", "RewardPassEndsIn": "報酬パス終了まで残り ", "RewardPassPrestigeRank": "プレステージランク {{rank}}", "SeasonalHub": "シーズンハブ", "StatTrackers": "追跡中ステータス", "TrackedTriumphs": "追跡中の勝利の道のり" }, "RecordBooks": { "HideCompleted": "完了した記録を非表示にする", "RecordBooks": "記録ブック" }, "Records": { "Title": "レコード", "UniversalOrnamentSetOther": "その他" }, "SearchHistory": { "Date": "最後に使用した日付", "DeleteAll": "星を付けていない検索を全て削除", "Description": "これまでに使われてきたフィルターワードの履歴一覧です。ここで履歴を削除できます。", "Item": "アイテム検索", "Link": "検索履歴を表示・編集", "Loadout": "ロードアウト検索", "Query": "フィルターで使用した検索ワード", "Title": "検索履歴", "UsageCount": "# これまでの使用回数" }, "Settings": { "Appearance": "表示設定", "ArmorArchetypeModslot": "アーマー属性/Modスロット", "AutoLockTagged": "アイテムのロック状態とタグを同期", "AutoLockTaggedExplanation": "DIMはタグに合わせて自動的にアイテムのロックとアンロックを行います。クラフトされたアイテムは、再構成できる様にロックが解除されたままになります。この設定を有効にすると、タグ付けされたアイテムのタイルにロックアイコンが表示されなくなります。", "BadgePostmaster": "現在のキャラクターにおけるポストマスターアイテム数をアプリアイコンに表示する", "BadgePostmasterExplanation": "この機能を利用するには、DIM をアプリとしてインストールし、OS がバッジの表示に対応している必要があります。", "BothDescriptions": "両方の説明", "BungieDescriptionOnly": "Bungie の説明", "CharacterOrder": "キャラ別のソート", "CharacterOrderFixed": "キャラのプレイ年数(PC環境では動作不安定です)", "CharacterOrderRecent": "最近使ったキャラ", "CharacterOrderReversed": "最近使ったキャラ(逆順)", "ColumnSize": "{{num}} 個", "ColumnSizeAuto": "オート", "CommunityData": "パーク評価", "CommunityDescriptionOnly": "コミュニティの説明", "CsvImport": "CSV の取り込み", "CustomErrorLabel": "ステータス値の名前には文字が含まれている必要があります。また、他の名前と同じものにしないでください。", "CustomErrorValues": "ステータス値の重みは正の数でなければなりません\n少なくとも2つのステータス値は1以上にしてください。", "CustomStatChooseName": "カスタムステータス名を選択", "CustomStatCreate": "新しいカスタムステータスを作成", "CustomStatDelete": "カスタムステータスを削除", "CustomStatDeleteConfirm": "このカスタムステータスを削除しますか?", "CustomStatDesc1": "好きなアーマーステータス値を選択して、カスタムの合計値を確定させます。", "CustomStatDesc3": "カスタムステータスはアイテムのポップアップ、オーガナイザー、比較で表示されます。", "CustomStatTitle": "カスタムステータス合計", "Data": "スプレッドシート", "DefaultItemSizeNote": "50pxに設定すると、画像やテキストが最もシャープに表示されます。", "DontForgetDupes": "重複アイテムは is:dupe で素早く検索でき、比較ツールやオーガナイザーを使用して関連アイテムを評価できます。", "EnableAdvancedStats": "アーマーの品質評価を表示する(D1)", "ExpandSingleCharacter": "全てのキャラクターを表示", "ExportLoadoutSS": "ロードアウトのスプレッドシート", "ExportLoadoutSSHelp": "DIMロードアウトをCSV形式でリストをダウンロードし、お好みの表計算アプリで簡単に閲覧できます。", "ExportProfile": "APIプロファイルのレスポンスデータをエクスポートする", "ExportSS": "所持品のスプレッドシート", "ExportSSHelp": "所持アイテムリストをCSV形式でダウンロードし、お好みの表計算アプリで簡単に閲覧できます。", "HidePullFromPostmaster": "\"$t(Loadouts.PullFromPostmaster)\" ボタンを隠す", "Inventory": "所持品", "InventoryColumns": "キャラクターごとの所持アイテム表示幅", "InventoryColumnsMobile": "モバイルポートレートのキャラクターの所持品の広さ", "InventoryColumnsMobileLine2": "新しい設定に合わせて表示サイズが変更されます", "InventoryNumberOfSpacesToClear": "ファーミングモードを使用する時に作成する空きスペース数", "Items": "アイテム表示", "Language": "言語", "LogOut": "ログアウト", "Masterworked": "マスターワーク済み", "MaxParallelCores": "並列タスクで使用する最大コア数", "MaxParallelCoresExplanation": "ロードアウト最適化ツールやロードアウトアナライザーで分析時に使用するCPUコア数を変更します。値を高くすると検索結果が出る時間を早めますが、システムリソースをより多く消費するため、挙動が重くなる可能性があります。", "OrnamentDisplay": "アイテムタイルに装飾を表示", "OrnamentDisplayExplanationDisabled": "アイテムに装飾を表示しません", "OrnamentDisplayExplanationEnabled": "アーマーにカーソルを重ねるか長押しすると装飾の表示を無効にします", "OrnamentDisplayExplanationHide": "アイテムにカーソルを重ねるか長押しすると装飾が非表示になります", "OrnamentDisplayExplanationShow": "アイテムにカーソルを重ねるか長押しすると装飾が表示されます", "ResetToDefault": "リセット", "RestoreVaultSide": "保管庫のアイテムを専用の列に表示", "ReverseSort": "前方 / 逆ソートの切り替え", "SetSort": "アイテムを並べ替える:", "SetVaultWeaponGrouping": "保管庫の武器を次のルールでグループ化する:", "Settings": "設定", "ShowNewItems": "新しいアイテムに赤い点を表示する", "SingleCharacter": "プレイ中のキャラクターだけを表示する", "SingleCharacterExplanation": "所持品のページで最後にプレイしたキャラクターだけを表示するよう設定します。\n非プレイのキャラクターが持っているアイテムで現在のキャラも使用できる場合、保管庫に表示されます。 \nクラス固有のアイテムは表示されません。", "SizeItem": "アイテムサイズ", "SortByAmmoType": "弾のタイプ", "SortByAmount": "所持数(消耗品のみ対象)", "SortByClassType": "クラス専用武器", "SortByCrafted": "クラフト済 (D2)", "SortByDeepsight": "ディープサイト (D2)", "SortByFeatured": "新しい装備/注目の装備 (D2)", "SortByPrimary": "パワーレベル", "SortByRarity": "レア度", "SortByRating": "防具の品質 (D1)", "SortByRecent": "最近獲得したもの (D2)", "SortBySeason": "シーズン (D2)", "SortByTag": "タグ ({{taglist}})", "SortByTier": "武器レベル(D2)", "SortByType": "種類", "SortByWeaponElement": "ダメージタイプ", "SortCustom": "カスタムソート", "SortName": "名前", "SpacesSize_other": "{{count}} スペース", "Theme": "テーマ", "Troubleshooting": "トラブルシューティング", "VaultArmorGroupingStyle": "アーマーをクラスごとに水平線で区切る", "VaultGroupingNone": "無し", "VaultUnder": "保管庫のアイテムを装備アイテムと同じ列に表示", "VaultWeaponGroupingStyle": "武器を種別ごとに水平線で区切る", "WeaponFrame": "武器フレーム", "WishlistRefreshNotificationBody": "アップデートしても変更されていない場合は、GitHubなどの元のリポジトリに変更内容がきちんと入っているかチェックしてください。", "WishlistRefreshNotificationTitle": "ウィッシュリストを更新しました" }, "Sockets": { "ApplyPerks": "パークを適用", "GridStyle": "パークをグリッドで表示", "Insert": { "Ability": "アビリティを装備する", "Aspect": "アスペクトを装着", "Fragment": "かけらを装着", "Mod": "Mod を装着する", "Ornament": "装飾を適用", "Projection": "ゴーストのプロジェクションを適用", "Shader": "シェーダーを適用", "Super": "スーパースキルを装備", "Transmat": "トランスマット効果を適用" }, "ListStyle": "パークをリストで表示", "Search": "検索名または説明文", "Select": { "Ability": "アビリティをプレビュー", "Aspect": "特性をプレビュー", "Fragment": "かけらをプレビュー", "Mod": "マスターワークのプレビュー", "Ornament": "装飾をプレビュー", "Projection": "ゴーストのプロジェクションをプレビュー", "Shader": "シェーダーをプレビュー", "Super": "スーパーをプレビュー", "Transmat": "トランスマット効果をプレビュー" }, "SelectWishlistPerks": "ウィッシュリスト推奨パークをプレビュー" }, "Stats": { "CrouchingSpeed": "しゃがみ歩き", "Custom": "カスタム合計", "CustomDesc": "Modやマスターワークを無視して、選択された基本ステータスの合計値を表示します。どのステータスが含まれているか確認するには、設定で参照してください。", "DamageResistance": "PvE ダメージ耐性", "Discipline": "鍛錬", "DropLevel": "アカウントパワー", "DropLevelExplanation1": "アカウントパワーは、報酬の増加パワーを決める基本パワーレベルです。", "DropLevelExplanation2": "アカウントパワーは、クラスやエキゾチックの縛りに関係なく、各装備スロットの最大パワーの装備を参照します。", "EquippableGear": "装備可能な装備", "FlinchResistance": "ひるみ耐性", "HP": "HP", "Intellect": "知性", "MaxGearPower": "装備可能なギアの最大出力", "MaxGearPowerAll": "各装備の最大パワー", "MaxGearPowerOneExoticRule": "装備可能な最大パワー(装備するエキゾチックアーマーは1つのみ)", "MaxTotalPower": "最大総合パワー", "MetersPerSecond": "m/s", "Milliseconds": "ミリ秒", "NoBonus": "ボーナスなし。", "NotApplicable": "N/A", "OfMaxRoll": "統計量の {{range}}", "PercentHelp": "品質の詳細については、ここをクリックしてください。", "Percentage": "%", "PowerModifier": "パワーボーナスと経験値", "Prestige": "評判レベル: {{level}}\n光の欠片5個までは後 {{exp}}xp", "Quality": "品質", "ShieldHP": "シールドHP", "StrafingSpeed": "左右後方歩行速度", "Strength": "腕力", "TierProgress": "T{{tier}} {{statName}} ({{progress}}/60 の T{{nextTier}})\n", "TierProgress_Max": "T{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "HP 全回復までの時間", "Total": "合計", "TotalHP": "最大HP", "WalkingSpeed": "通常歩行速度", "WeaponPart": "武器パーツ" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM は一度ユーザー認証すれば、その後は自動的にロードアウトやタグ、設定が異なる環境でも同期されます。DIM Sync を無効にした場合でも、設定ページからデータ出力・インポートでの手動同期が可能です。この機能は、OpenCollective の支援者によって実現しました。", "No": "今はダメだ", "Title": "DIM Sync を有効にしますか?", "Yes": "同期を有効にする" }, "AutoBackup": "万が一に備え、「dim-data.json」をダウンロードフォルダーに作成しました。", "BackUpFirst": "すべてのデータを削除する事態になった場合、万が一に備えて必ずバックアップを取ってください。", "BrowserMayClearData": "スペースに空きが無くなったり、DIMを長期使用してない場合ブラウザがこの情報を削除する場合があります。", "DataIsLocal": "タグとメモはローカル環境でのみ保存されます", "DeleteAllData": "DIM Syncサーバーからすべてのデータを削除", "DeleteAllDataConfirm": "DIM Sync から、データすべてを削除してもよろしいですか?元に戻すことはできません。", "Details": { "IndexedDBStorage": "保存内容はこのブラウザのみ有効で、ブラウザのデータ削除時にこのデータも消去されます。" }, "DimApiFinePrint": "DIMサーバーにデータをアップロードするとタグやロードアウトを保存して、異なるブラウザでも同じ内容で表示出来るようになります。", "DimSyncDown": "DIM Syncはサーバーとの通信に問題があるため、接続できません。", "DimSyncEnabled": "DIM Syncを有効にする", "DimSyncNotEnabled": "DIM Syncが無効のため、設定やタグ、ロードアウト、検索履歴はブラウザだけに保存されています。ブラウザのキャッシュを消去すると失われるため、[設定]でDIM Syncを有効にするか、定期的な手動バックアップを行うことをおすすめします。", "EnableDimApi": "DIM Sync を有効にする (推奨)", "Export": "バックアップのダウンロード", "ExportError": "DIM Sync からのバックアップのダウンロードに失敗しました。", "ExportErrorBody": "DIM Syncがダウンしているか、ネットワークに問題が発生しています。そのため、ローカルに保存されているデータを読み込みます。", "Import": "バックアップのインポート", "ImportConfirmDimApi": "現在のタグ、ロードアウト、設定をこのバージョンで上書きしても大丈夫ですか?それはあなたが持っていたものを完全に置き換えます。", "ImportExport": "バックアップとインポート", "ImportFailed": "インポートに失敗しました! {{error}}", "ImportNoFile": "ファイルが選択されていません!", "ImportNotification": { "FailedBody": "データをインポートできませんでした。 {{error}}", "FailedTitle": "インポートに失敗しました", "NoData": "バックアップにロードアウトやタグが見つかりません", "SuccessBodyForced": "バックアップから設定、 {{loadouts}} ロードアウト、 {{tags}} タグ付けされたアイテムをDIM Syncにインポートし、既にあったものを置き換えます。", "SuccessBodyLocal": "インポートされた設定、 {{loadouts}} ロードアウト、および {{tags}} タグ付けされたアイテムがバックアップからローカルストレージに保存され、既存のものが置き換えられます。 私達はローカルストレージが失われないことは保証できません。DIM Sync を有効にすることを検討してください。", "SuccessTitle": "インポート成功" }, "ImportTooManyFiles": "インポートするファイルを選んでください.", "ImportWrongFileType": "ファイルはJSONファイルではありません。DIMバックアップでない可能性があります。", "IndexedDBStorage": "ローカルブラウザストレージ", "LearnMore": "DIM Syncについて", "MenuTitle": "同期とバックアップ", "ProfileErrorBody": "DIM Syncとの通信に問題が発生し、最新の設定、タグ、ロードアウト、検索結果が反映されていない可能性があります。データはサーバー上に保存されており、ローカルの更新内容はサーバーが復旧次第、自動的に同期されます。", "ProfileErrorTitle": "DIM Sync ダウンロードエラー", "RefreshDimSync": "DIM Syncのデータを再読み込みする", "UpdateErrorBody": "DIM Syncにデータを保存できませんでした。DIMのページが開いている間、バックグラウンドで再試行を続けます。", "UpdateErrorTitle": "DIM Sync 保存エラー", "UpdateInvalid": "DIM Syncにデータを保存できませんでした", "UpdateInvalidBody": "DIM Syncに送信されたデータは無効のため保存できませんでした。", "UpdateInvalidBodyLoadout": "ロードアウト \"{{name}}\" は無効のため保存できませんでした。 別サイトからのインポートに失敗した場合は、そのサイト管理者の方へ無効なデータであることをお知らせください。", "UpdateQueueLength_other": "変更した {{count}} 個の項目は、サーバーへ再接続したときに保存されます。", "Usage": "このデバイス上でDIMが使用中の容量は {{usage, humanBytes}}です。(空き容量:{{quota, humanBytes}})\nBungie.net からダウンロードしたDestinyのアイテムデータも含みます。" }, "StreamDeck": { "Authorize": "アプリケーションに接続", "Enable": "Stream Deck プラグイン", "Error": { "Body": "Stream Deck プラグインへのデータ送信中にエラーが発生しました。プラグイン開発者に報告のご協力をお願いいたします。 {{error}}", "Title": "Stream Deck プラグイン エラー" }, "FinePrint": "DIM Stream Deck プラグインとの接続を有効にします。 このプラグインは、DIM開発チームとは別のチームによるプロジェクトです。", "Install": "プラグインをインストールする", "MissingAuthorization": "DIM に接続するには、Stream Deck アプリケーションを認証する必要があります。設定を開き、\"アプリケーションに接続\"をクリックします。", "Tooltip": { "Application": "Stream Deck アプリケーション", "AuthRequired": "このボタンをクリックするか、設定から\"アプリケーションに接続\"をクリックします。", "Error": "お使いのStream Deck プラグインはサポートが終了したため、最新バージョンにアップデートしてください。利用するには以下のバージョンが必要です:", "ErrorConnection": "最新バージョンでも利用できない場合は、ブラウザの拡張機能や設定などでブロックしていないか確認してください。", "ExtensionIssue": "拡張機能の問題", "Plugin": "プラグイン", "Title": "DIM Stream Deck プラグイン", "Version": "バージョン:" } }, "StripSockets": { "Action": "ソケットからアイテムを外す", "ArmorMods": "{{count}} 個のアイテムにアーマーModが適用されています", "Button": "{{numSockets}} 個のアイテムを外す", "Cancel": "キャンセル", "Choose": "取り外すアイテムを選択してください", "DiscountedMods": "{{count}}個のアイテムにコストが軽減されたModが適用されています", "Done": "ソケットから対象アイテムを外しました", "NoSockets": "外せるアイテムがありません", "Ok": "Ok", "Ornaments": "{{count}} 個のアイテムに装飾が適用されています", "Others": "{{count}} 個のアイテムにゴーストプロジェクションが適用されています", "Running": "ソケットからアイテムを外しています", "Shaders": "{{count}} 個のアイテムにシェーダーが適用されています", "Subclass": "{{count}} 個のアイテムにサブクラスオプションが適用されています", "WeaponMods": "{{count}} 個のアイテムに武器Modが適用されています" }, "Tags": { "Archive": "アーカイブ", "ClearTag": "タグを外す", "Favorite": "お気に入り", "Infuse": "融合する", "Junk": "ジャンク", "Keep": "キープ", "LockAll": "ロックされたアイテム", "TagItem": "タグを付ける", "UnlockAll": "アイテムのロックを解除" }, "Triage": { "AccountsForArtifice": "戦略的アーマーに+3ステータスMODを装着した場合、性能が向上するかをテストします。", "BetterArmor": "厳密に良い防具", "BetterArtificeArmor": "良い数値の戦略的アーマー", "BetterStatArmor": "良いステータスの防具", "BetterStatArtificeArmor": "良いステータス値の戦略的アーマー", "BetterWorseArmor": "より良い/悪いアーマー", "BetterWorseIncludes": "防具のピースを次から識別 : ", "HighStats": "高ステータス", "InLoadouts": "以下ロードアウトで使用", "OwnedCount": "所持", "PerkBetterArmorDesc": "同じかそれ以上の、固有パークや特別 mod スロット", "PerkWorseArmorDesc": "同一の固有パーク、か否か。", "SimilarItems": "類似アイテム", "StatBetterArmorDesc": "すべてのステータスが同等以上で、かつ少なくとも1つが優れている。", "StatNotPerkArmorDesc": "ステータスのみテストします。下位のピースでも、特別な mod スロットや固有パークを持つ場合があります。", "StatWorseArmorDesc": "より良ステータスは無く、少なくとも1つは悪いステータス。", "ThisItem": "共通する特徴から比較", "WorseArmor": "厳密に悪い防具", "WorseArtificeArmor": "数値の低い非戦略的アーマー", "WorseStatArmor": "悪いステータスの防具", "WorseStatArtificeArmor": "低いステータス値の非戦略的アーマー", "YourBestItem": "最高のアイテム" }, "Triumphs": { "GildingTriumph": "金の勝利", "HideCompleted": "完了した勝利の道のりを隠す", "RevealRedacted": "改定された勝利の道のりを表示する", "SortRecords": "完了順に勝利の道のりを並べ替える" }, "Vendors": { "Collections": "コレクション", "Engram": "ランク", "FilterToUnacquired": "未収集アイテムのみ表示", "HideSilverItems": "シルバーでの販売アイテムを隠す", "NoItems": "このベンダーは現在、アイテムを提供していません。", "RefreshTime": "ベンダーアイテムがリセットされるまで残り:", "Vendors": "ベンダー" }, "Views": { "About": { "APIHistory": "DIMと他のDestinyアプリが実行した全操作の履歴を確認する", "BungieCopyright": "掲載されているすべての画像およびコンテンツの権利はBungieに帰属します。", "CommunityInsight": "コミュニティのパーク調査結果は {{clarityLink}} の提供によるものです。情報に誤りを見つけた場合や質問がある場合は {{clarityDiscordLink}} に参加してください。", "Discord": "Discord", "DiscordHelp": "Discord内のチャンネルで、質問・フィードバック・サポートを行っています。お気軽にご参加ください。", "FAQ": "よくある質問", "FAQAccess": "DIMはどのようにして私のゲームデータにアクセスしているのですか?", "FAQAccessAnswer": "DIMはBungieのコンパニオンアプリと同じ認証でアクセスして、アイテムの確認や移動をしています。DIMがユーザー名やパスワードを見ることができません。", "FAQKeyboard": "DIMでショートカットキーは使えますか?", "FAQKeyboardAnswer": "はい、対応しています。「?」キーでショートカットの一覧を確認できます。", "FAQLogout": "どうやってDIMをログアウトできますか?", "FAQLogoutAnswer": "左上のアイコンからメニューを開き、「ログアウト」を選択します", "FAQLostItem": "ツールを使っていたらアイテムが消えました!", "FAQLostItemAnswer": "Bungieでは、アプリからアイテムを削除することは許可されていません(公式アプリでも同様です)。ほとんどの場合、転送に失敗してアイテムが保管庫や他のキャラクターに残っている可能性があります。まずはアイテムを検索してみてください。それでも見つからない場合は、ページを再読み込みしてください。{{link}}やゲーム内でアイテムが残っているかも確認してみましょう。きっとまだ存在しています。", "FAQMobile": "DIMはスマートフォンでも使えますか?専用アプリはありますか?", "FAQMobileAnswer": "DIMはスマホやタブレットでも動作し、ホーム画面に追加すればアプリ感覚でご利用いただけます。", "GitHub": "GitHub", "GitHubHelp": "プロジェクトへの参加に関心がある方は、{{link}}をご確認ください。", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIMは、Bungie.netやDestinyコンパニオンアプリと同じサービスを利用して動作する、コミュニティ開発の無料オープンソースアプリです。", "Schedule": { "beta": "DIMのベータ版はコードの変更と同時に更新され、最新の機能や修正がすぐに使えます。ただし新しいバグも入りやすいのでご注意ください。", "release": "DIMのバージョンは週に1度、日本時間で日曜日のPM 17: 00 頃に更新されます。" }, "Translation": "翻訳チームに参加してください!", "TranslationText": "翻訳を効率的に行うために{{link}}を使用しています。DIMの翻訳を改善したい方は、ぜひチームにご参加ください。", "Version": "バージョン {{version}} ({{flavor}}), built on {{date}}", "Wiki": "DIMユーザーガイド", "WikiHelp": "DIMの使い方を確認する" }, "Login": { "Auth": "Bungie.net で認証する", "EnableDimSyncWarning": "現在、DIM Sync を無効にしています。有効にすると、ローカルデータはDIM Syncの過去データに上書きされます。有効化前にローカルバックアップを取り、必要に応じて[設定]から復元してください。", "Explanation": "DIMによるキャラクターや保管庫の操作には、アクセス認証が必要です。", "LearnMore": "アカウントとログインについて", "NewAccount": "別の Bungie.net アカウントでログイン", "Permission": "ログインが必要" }, "Support": { "BackersDetail": "開発を継続するため、1回または月額のご寄付によるご支援をお願いいたします。", "FreeToDownload": "DIMは無料でダウンロードしてご利用できるプロダクトです。ソースコードはオープンソースで、誰でも自由に改良に参加できます。DIMに広告が表示されることは決してありません。これは私たちのポリシーです。", "OpenCollective": "このプロジェクトに尽力している開発者への報酬のため、{{link}}を利用しています。", "Store": "ロゴなどをデザインしたグッズを{{link}} にて販売しています", "Support": "DIMを支援する" } }, "WishListRoll": { "BestRatedTip_other": "ウィッシュリストにある推奨ロールと完全一致しています。", "Clear": "ウィッシュリストをクリア", "CopiedLine": "ウィッシュリストのロールをクリップボードにコピーしました", "CopyLine": "選択中のパークをウィッシュリストのロールとしてコピー", "DupeRolls": " (+{{num, number}} 個の重複を無視しました)", "ExternalSource": "別のウィッシュリストに追加しました", "ExternalSourcePlaceholder": "追加するウィッシュリストのURLをここにペーストしてください。", "Header": "ウィッシュリスト", "Import": "ウィッシュリストにあるロールをロードする", "ImportError": "「{{url}}」からウィッシュリストを読み込む際にエラーが発生しました: {{error}}", "ImportFailed": "ウィッシュリスト内に有効なロールはありません。", "ImportNoFile": "ファイルが選択されていません。", "InvalidExternalSource": "外部ウィッシュリストソースの有効なURLを入力してください。 URLは次から始まる必要があります:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "最終更新: {{lastUpdatedDate}} at {{lastUpdatedTime}}", "Num": "{{num, number}} 個のロールが現在のウイッシュリストに入っています", "NumRolls": "{{num, number}} 個のロール", "Refresh": "ウィッシュリストを更新", "SourceAlreadyAdded": "このウィッシュリストは既に追加されています。", "UpdateExternalSource": "ウィッシュリストに追加", "Voltron": "voltron (標準)", "WishListNotes": "ウィッシュリストのメモ:", "WorstRatedTip_other": "ごみリストにある非推奨パークと完全一致しています。" }, "no-space": "空きスペースなし", "wrong-level": "レベル不正" } ================================================ FILE: src/locale/ko.json ================================================ { "AWA": { "ConfirmDescription": "데스티니 가디언즈 컴패니언 앱을 실행하여 DIM이 아이템을 수정할 수 있도록 허용해주세요.", "ConfirmTitle": "작업 확인", "Error": "개조 부품이나 특성 교체 실패", "ErrorMessage": "{{item}}에 {{plug}}을(를) 장착할 수 없습니다.\n\n{{error}}", "FailedToken": "아이템을 변경할 권한이 없음", "IrreversiblePlugging": "{{plug}}을(를) 소지하고 있지 않으므로 덮어쓰지 않습니다." }, "Accounts": { "Choose": "{{bungieName}} 프로필", "ErrorLoadInventory": "데스티니 {{version}} 캐릭터와 소지품 불러오기 실패", "ErrorLoadManifest": "Bungie에서 데스티니 정보 데이터베이스 불러오기 실패", "ErrorLoading": "Bungie.net에서 데스티니 계정 불러오기 실패", "MissingAccountWarning": "여기에 계정이 보이지 않는다면 잘못된 Bungie.net 계정으로 로그인하였거나 Bungie.net이 점검 중일 수 있습니다.", "MissingDescription": "지금 확인하려는 계정은 Bungie.net 프로필과 연결된 계정이 아닙니다. 아래에서 본인의 계정을 선택하세요.", "MissingTitle": "계정을 찾을 수 없습니다", "NoCharacters": "이 Bungie.net 계정에 연동된 데스티니 캐릭터가 없습니다. 다른 계정으로 접속해보세요.", "NoCharactersTitle": "캐릭터를 찾을 수 없음", "SwitchAccounts": "헤더의 메뉴에서 계정을 전환할 수 있습니다.", "Title": "계정" }, "Activities": { "Activities": "활동", "Hard": "어려움", "Nightfall": "황혼전", "Normal": "보통", "WeeklyHeroic": "주간 영웅 공격전" }, "Armory": { "AlternateItems": "다른 버전", "Armory": "무기고", "DifferentSeason": "다른 시즌에서 재출시됨", "NoNotes": "메모 없음", "OpenInArmory": "무기고에서 보기", "Season": "{{season}}시즌, {{year}}년차", "TrashlistedRolls_other": "{{count, number}}개의 쓰레기 목록에 추가된 특성 조합", "Unknown": "알 수 없는 아이템", "UnknownPerkHash": "이 희망 아이템 목록은 아이템에 존재하지 않는 특성 해시 {{hash}} ({{perkName}})를 지정하였으며, 유효하지 않습니다. 수정을 위해 희망 아이템 목록 작성자에게 문의하세요. 희망 아이템 목록은 강화되지 않은 특성만을 지정해야 합니다.", "WishlistedRolls_other": "{{count, number}}개의 희망 아이템 목록에 추가된 특성 조합", "YourItems": "내 아이템" }, "Browsercheck": { "Samsung": "삼성 인터넷은 다크 모드가 활성화된 경우 사이트를 너무 어둡게 만들 수 있습니다. 설정 > 실험실 > 웹사이트의 다크 테마를 사용하거나 다른 브라우저를 사용하세요.", "Steam": "스팀 오버레이의 브라우저는 매우 오래되어 DIM의 일부 혹은 모든 기능이 동작하지 않을 수 있습니다. 이에 대한 지원은 제공되지 않습니다.", "Unsupported": "DIM 팀은 이 브라우저를 지원하지 않습니다. DIM의 일부나 전체 기능이 동작하지 않을 수 있습니다." }, "Bucket": { "Armor": "방어구", "Class": "하위직업", "General": "일반", "Ghost": "고스트", "Inventory": "소지품", "Postmaster": "우편 담당자", "Progress": "진척도", "Reputation": "평판", "Unknown": "알 수 없음", "Vault": "금고", "Weapons": "무기" }, "BulkNote": { "Append": "메모에 추가 / #해시태그 추가", "Confirm": "메모 업데이트", "Remove": "메모에서 제거 / #해시태그 제거", "Replace": "메모 교체", "Title_other": "아이템 {{count}}개의 메모 변경" }, "BungieAlert": { "Title": "Bungie의 메시지:" }, "BungieService": { "AppNotPermitted": "DIM이 이 작업을 수행할 권한이 없습니다.", "DestinyCannotPerformActionAtThisLocation": "활동 중에는 아이템을 장착하거나 개조 부품을 변경할 수 없습니다. 궤도나 소셜 구역으로 이동해보세요. DIM이 아닌 Bungie.net API의 한계입니다.", "DestinyItemUnequippable": "이 아이템을 장착할 수 없습니다. 캐릭터의 마지막 활동에서 장비가 잠긴 경우 캐릭터에 다시 로그인해보세요.", "DestinyLegacyPlatform": "Bungie의 서비스에는 이전 세대의 콘솔에서 데스티니 1을 플레이하였을 경우 DIM이 데스티니 2 정보를 불러오지 못하게 막는 버그가 있습니다. Bungie가 이 버그를 곧 고칠 것이지만, 그전에 당신의 정보에 접근하기 위해서는 현세대 콘솔에서 데스티니 1을 플레이하셔야 합니다.", "DevVersion": "DIM의 개발 버전을 실행 중이신가요? Bungie.net에 크롬 확장 프로그램을 등록하셔야 합니다.", "Difficulties": "현재 Bungie.net에 문제가 있습니다.", "ErrorTitle": "Bungie.net 오류", "ItemUniquenessExplanation": "캐릭터는 '{{name}}'을(를) 하나만 소지할 수 있습니다.", "Maintenance": "Bungie.net 서버가 점검 중입니다.", "MissingInventory": "Bungie.net에서 소지품을 반환하지 않았으며, 개인 정보 보호 설정에 의해 차단되었을 수 있습니다. 로그아웃 후 다시 로그인해 보십시오.", "NetworkError": "통신 오류 - {{status}} {{statusText}}", "NoAccount": "데스티니 계정을 찾을 수 없습니다. 올바른 플랫폼을 선택하셨나요?", "NoAccountForPlatform": "{{platform}}에서 데스티니 계정을 찾지 못했습니다.", "NotConnected": "인터넷에 연결되어 있지 않을 수 있습니다.", "NotConnectedOrBlocked": "인터넷에 연결되지 않았거나, 광고 차단 혹은 개인정보 관련 확장 프로그램이 Bungie.net을 차단하고 있을 수 있습니다.", "NotLoggedIn": "이 앱을 사용하려면 DIM에 권한을 부여해주세요.", "Slow": "Bungie.net이 느림", "SlowDetails": "Bungie.net이 정보를 반환하는 시간이 오래 걸립니다. 너무 많은 플레이어가 한 번에 접속 중이거나, Bungie.net에 문제가 있을 경우 이와 같은 일이 일어날 수 있습니다. 인터넷 연결 문제가 있을 수도 있습니다. 저희는 계속 응답을 기다리고 있겠습니다.", "SlowResponse": "Bungie.net의 응답이 너무 느립니다.", "Throttled": "Bungie.net이 DIM의 요청의 수를 제한하고 있습니다.", "Twitter": "상태 업데이트 받기:", "UnknownError": "Bungie.net 메시지: {{message}}", "VendorNotFound": "상인의 정보가 없습니다." }, "Compare": { "Archetype": "유형", "AssumeMasterworked": "걸작 가정", "AssumeMasterworkedDescription": "현재 개조 부품 제외, 걸작 기준 능력치", "BaseStatsDescription": "개조 부품 및 걸작 단계를 제외한 기본 능력치", "Button": "비교", "ButtonHelp": "아이템 비교", "CompareBaseStats": "기본 능력치 표시", "CurrentStats": "현재 능력치", "CurrentStatsDescription": "개조 부품 및 걸작 단계를 포함한 현재 능력치", "Error": { "Invalid": "비교 가능한 아이템이 없습니다.", "Unmatched": "비교되는 아이템의 유형과 일치하지 않습니다." }, "InitialItem": "비교 도구가 실행된 아이템입니다.", "IsVendorItem": "이 아이템은 소지품에 없으며, {{vendorName}}이(가) 판매하고 있습니다.", "NoModArmor": "구형 방어구" }, "Cooldown": { "Grenade": "수류탄 재사용 대기시간: {{cooldown}}", "Melee": "근접 공격 재사용 대기시간: {{cooldown}}", "Super": "궁극기 재사용 대기시간: {{cooldown}}" }, "Countdown": { "Days_compact_other": "{{count}}일", "Days_other": "{{count}}일" }, "Csv": { "EmptyFile": "파일에 행이 없습니다.", "ImportConfirm": "정말로 CSV에서 태그와 메모를 가져오시겠습니까? 스프레드시트에 포함된 모든 아이템의 태그와 메모를 덮어씁니다.", "ImportFailed": "CSV에서 태그와 메모 가져오기 실패: {{error}}", "ImportSuccess_other": "{{count}}개의 아이템에 대한 태그와 메모를 불러왔습니다.", "ImportWrongFileType": "이 파일은 CSV 파일이 아닙니다.", "WrongFields": "CSV는 'Id', 'Notes', 'Tag', 'Hash' 열을 포함해야 합니다." }, "Dialog": { "Cancel": "취소", "OK": "확인" }, "EnergyMeter": { "Energy": "에너지", "Unused": "사용되지 않음", "UpgradeNeeded": "이 아이템의 현재 에너지 수용량은 {{energyCapacity}} 입니다. 선택된 개조 부품을 장착하려면 {{energyUsed}}의 에너지 수용량이 필요합니다.", "Used": "사용됨" }, "ErrorBoundary": { "Title": "문제가 발생했습니다" }, "ErrorPanel": { "BrowserTooOld": "브라우저의 버전이 너무 낮아 DIM을 사용할 수 없습니다. 브라우저를 최신 버전으로 업데이트해 주세요.", "BrowserTooOldTitle": "호환되지 않는 브라우저", "Description": "Destiny 2 컴패니언 앱에서 소지품을 불러와서 Bungie.net 서버 상태를 확인해 보세요.", "ReadTheGuide": "문제가 발생한 경우 사용 설명서(메뉴의 링크)를 읽어보세요.", "SystemDown": "이 문제는 모든 데스티니 앱에 해당되며, DIM 팀이 고치거나 우회할 수 없습니다.", "Troubleshooting": "문제 해결 가이드" }, "FarmingMode": { "D2Desc_female_other": "DIM은 {{store}}의 아이템 종류별로 {{count}}개의 빈 공간을 만듦으로써 아이템이 우편 담당자에게 보내지는 것을 막고 있습니다.", "D2Desc_male_other": "DIM은 {{store}}의 아이템 종류별로 {{count}}개의 빈 공간을 만듦으로써 아이템이 우편 담당자에게 보내지는 것을 막고 있습니다.", "D2Desc_other": "DIM은 {{store}}의 아이템 종류별로 {{count}}개의 빈 공간을 만듦으로써 아이템이 우편 담당자에게 보내지는 것을 막고 있습니다.", "Desc_female_other": "DIM이 우편 담당자에게 아이템이 보내지는 것을 막기 위해 {{store}}의 엔그램과 미광체 아이템을 금고로 옮기고 아이템 종류별로 {{count}}개의 빈 공간을 확보하고 있습니다.", "Desc_male_other": "DIM이 우편 담당자에게 아이템이 보내지는 것을 막기 위해 {{store}}의 엔그램과 미광체 아이템을 금고로 옮기고 아이템 종류별로 {{count}}개의 빈 공간을 확보하고 있습니다.", "Desc_other": "DIM이 우편 담당자에게 아이템이 보내지는 것을 막기 위해 {{store}}의 엔그램과 미광체 아이템을 금고로 옮기고 아이템 종류별로 {{count}}개의 빈 공간을 확보하고 있습니다.", "FarmingMode": "파밍 모드", "FarmingModeNote": "(획득할 아이템을 위해 공간 관리)", "MakeRoom": { "Desc": "DIM이 우편 담당자에게 아이템이 보내지는 것을 막기 위해 {{store}}의 엔그램과 미광체 아이템을 금고나 다른 캐릭터에게 이동시키고 있습니다.", "Desc_female": "DIM이 우편 담당자에게 아이템이 보내지는 것을 막기 위해 {{store}}의 엔그램과 미광체 아이템을 금고나 다른 캐릭터에게 이동시키고 있습니다.", "Desc_male": "DIM이 우편 담당자에게 아이템이 보내지는 것을 막기 위해 {{store}}의 엔그램과 미광체 아이템을 금고나 다른 캐릭터에게 이동시키고 있습니다.", "MakeRoom": "장비를 이동시켜 아이템을 얻을 공간을 확보", "Tooltip": "체크하면 DIM이 금고 내 엔그램의 공간 확보를 위해서 무기와 방어구를 옮길 것입니다." }, "OutOfRoom": "{{character}}에게서 아이템을 이동할 공간이 없습니다. 쓰레기를 청소할 때가 됐습니다!", "OutOfRoomTitle": "공간 부족", "Stop": "중지", "Vault": "공간 확보를 위해 아이템을 금고로 옮깁니다." }, "FashionDrawer": { "Accept": "패션 저장", "CannotFitOrnament": "이 아이템은 장식 소켓이 없거나 해당하는 장식을 소지하고 있지 않습니다.", "CannotFitShader": "이 아이템에 안료를 적용할 수 없음", "ClearOrnaments": "장식 초기화", "ClearOrnamentsTitle": "모든 장식을 \"선택 안 함\"으로 초기화", "ClearShaders": "안료 초기화", "ClearShadersTitle": "모든 안료를 \"선택 안 함\"으로 초기화", "NoPreference": "선택 안 함 - 이 소켓은 변경되지 않습니다", "Reset": "패션 초기화", "Sync": "동기화", "SyncOrnaments": "장식 동기화", "SyncOrnamentsTitle": "잠금 해제된 경우 모든 아이템에 동일한 세트의 장식을 사용합니다.", "SyncShaders": "안료 동기화", "SyncShadersTitle": "모든 아이템에 동일한 안료 사용", "Title": "안료와 장식 선택", "UseEquipped": "장착된 패션 사용" }, "FileUpload": { "Instructions": "클릭 또는 파일 끌어서 놓기" }, "Filter": { "Adept": "\\(숙련자\\)", "AmmoType": "탄약 유형에 따라 아이템을 표시합니다.", "Armor": "방어구인 아이템을 표시합니다.", "Armor3": "운명의 경계에서 도입된 방어구 3.0 능력치 시스템을 사용하는 아이템을 표시합니다.", "ArmorCategory": "방어구의 종류를 기준으로 표시합니다.", "ArmorIntrinsic": "계략 방어구와 같이 본질 특성이 있는 전설 방어구를 표시합니다.", "Artifice": "계략 방어구를 표시합니다.", "Ascended": "승천 노드가 있으며 승천이 완료된 아이템을 표시합니다.", "Breaker": "용사 차단기나 용사 종류를 기준으로 표시합니다. breaker:instrinsic을 사용하여 용사 차단기 본질 특성이 있는 아이템을 표시합니다.", "BulkClear_other": "{{count}}개의 아이템에서 태그를 제거하였습니다.", "BulkRevert_other": "{{count}}개의 아이템의 태그를 되돌렸습니다.", "BulkTag_other": "{{count}}개의 선택된 아이템에 {{tag}} 태그를 지정하였습니다.", "Catalyst": "촉매제를 현재 상태에 따라 표시합니다. catalyst:complete는 완료하고 장착된 촉매제를 표시하며, catalyst:incomplete는 잠금 해제하였으나 완료하지 않았거나 장착하지 않은 촉매제를 표시하며, catalyst:missing은 촉매제를 장착할 수 있으나 아직 찾지 못한 아이템을 표시합니다.", "Class": "직업을 기준으로 아이템을 표시합니다.", "Combine": "필터는 \"{{example}}\"와 같이 괄호, \"or\" 및 \"and\"로 결합하거나 그룹화하여 검색 범위를 좁힐 수 있습니다.", "ContributePower": "전투력이 있으며, 캐릭터의 전투력에 기여할 수 있는 아이템을 표시합니다.", "Cosmetic": "장식 및 기타 아이템을 표시합니다.", "Craftable": "제작 가능한 아이템을 표시합니다.", "CraftedDupe": "중복된 무기 중 하나 이상이 제작된 무기인 경우 표시합니다.", "Curated": "고정 특성 조합이 있는 아이템을 표시합니다.", "CurrentClass": "현재 접속한 수호자에게 장착할 수 있는 아이템을 표시합니다.", "CustomStatLower": "해당 직업의 사용자 정의 총합 능력치만 고려하여 동일한 종류의 다른 방어구보다 능력치가 확실히 낮은 방어구를 표시합니다.", "DamageType": "공격 속성에 따라 아이템을 표시합니다.", "Deepsight": "심안 공명에서 패턴을 추출할 수 있거나 심안 융화기를 사용하여 심안 공명을 활성화할 수 있는 무기를 표시합니다.", "Deprecated": "이 필터는 더 이상 지원되지 않습니다.", "Description": "설명", "DescriptionFilter": "설명이 필터와 부분적으로 일치하는 아이템을 표시합니다. 따옴표를 사용하여 전체 문구를 검색합니다.", "DisabledModSlot": "비활성화된 개조 부품이 포함된 아이템을 표시합니다.", "Dupe": "재출시를 포함하여 중복된 아이템을 표시합니다.", "DupeArchetype": "동일한 능력치 유형을 가진 방어구를 묶습니다.", "DupeCount": "특정 개수만큼 중복된 아이템 표시", "DupeLower": "재출시된 아이템을 포함하여 가장 높은 전투력이 아닌 중복된 아이템을 표시합니다. 하나의 중복된 아이템만 가장 높은 전투력으로 선택되며, 나머지 아이템은 낮은 전투력으로 간주됩니다.", "DupePerks": "동일한 유형의 아이템에서 특성이 일치하거나 일부만 포함된 아이템을 표시합니다.", "DupeSetBonus": "동일한 세트 보너스를 가진 방어구를 묶습니다.", "DupeStats": "계략 및 조율 등 능력치 조정 개조 부품을 포함하여, 동일한 기본 능력치를 가진 방어구를 표시합니다.", "DupeTertiary": "동일한 3차 능력치를 가진 방어구를 묶습니다.", "DupeTraits": "동일한 유형의 무기에서 특성이 일치하거나 일부만 포함된 무기를 표시합니다.", "DupeTunedStat": "조정 능력치가 동일한 방어구를 묶습니다.", "DupeUntunedStats": "능력치 개조 부품을 제외하고 동일한 기본 능력치를 가진 방어구를 묶습니다.", "DupeZeroStats": "세 가지 0이 아닌 기본 능력치가 동일한 방어구를 묶습니다.", "Energy": "섀도우킵에서 도입된 방어구 2.0 개조 부품 시스템을 사용하는 아이템을 표시합니다.", "EnergyCapacity": "현재 에너지 수용량에 따라 아이템을 표시합니다.", "Engrams": "엔그램을 표시합니다.", "Enhanceable": "강화 가능한 무기를 표시합니다.", "Enhanced": "무기의 강화 등급을 기준으로 표시합니다.", "EnhancedPerk": "특정 개수의 강화된 특성을 지닌 아이템을 표시합니다.", "EnhancementReady": "특성을 강화할 수 있는 레벨에 도달한 무기를 표시합니다.", "Equipment": "장착 가능한 아이템", "Equipped": "캐릭터가 착용하고 있는 아이템", "Event": "데스티니 2의 특정 이벤트에서 나온 아이템을 표시합니다.", "ExtraPerk": "추가로 선택 가능한 특성이 있는 무작위 특성의 전설 무기를 표시합니다.", "Featured": "현재 시즌에서 \"새 장비\"나 \"추천 아이템\"에 해당되는 아이템", "Filter": "필터", "FilterWith": "필터 조건:", "Focusable": "상인에게서 집중 가능한 아이템을 표시합니다.", "Foundry": "제조사에 따라 아이템을 표시합니다.", "Glimmer": "미광체 수급에 관련된 소모품을 표시합니다.", "Harrowed": "\\(고뇌\\)", "HasNotes": "메모가 추가된 아이템 표시", "HasOrnament": "장식이 적용된 아이템을 표시합니다.", "HasShader": "안료가 적용된 아이템을 표시합니다.", "Holofoil": "홀로그램 장식 무기를 표시합니다.", "InDimLoadout": "is:indimloadout은 DIM 로드아웃에 포함된 아이템을 표시합니다.", "InInGameLoadout": "is:iningameloadout은 게임 내 로드아웃에 포함된 아이템을 표시합니다.", "InInventory": "소지품에 한 개 이상 보유 중인 아이템을 표시합니다. 상점이나 기록 화면에서만 유용합니다.", "InLoadout": "\"is:inloadout\"은 로드아웃에 포함된 아이템을 표시합니다. \"inloadout:\"을 사용하여 검색하면 이름이 일치하는 로드아웃의 아이템을 표시합니다. \"inloadout:\"이 해시태그와 같이 사용되는 경우, 이름이나 메모에 해시태그가 포함된 로드아웃의 아이템을 표시합니다. 숫자 범위와 함께 사용되는 경우, 해당 개수만큼 로드아웃에 포함된 아이템을 표시합니다.", "Infusable": "주입할 수 있는 아이템을 표시합니다.", "InfusionFodder": "더 낮은 전투력의 동일한 아이템에 미광체만을 사용하여 주입할 수 있는 아이템을 표시합니다.", "IsAdept": "숙련자 개조 부품과 호환되는 무기를 표시합니다.", "IsCrafted": "제작된 무기를 표시합니다.", "ItemHash": "소지품 아이템의 해시와 일치하는 아이템을 표시합니다. 고급 사용자용.", "ItemId": "소지품 아이템의 ID와 일치하는 아이템을 표시합니다. 고급 사용자용.", "Leveling": { "Complete": "{{term}} - 모든 업그레이드가 잠금 해제된 아이템을 표시합니다.", "Incomplete": "{{term}} - 잠긴 업그레이드가 있는 아이템을 표시합니다.", "NeedsXP": "{{term}} - 여전히 경험치를 줄 수 있는 아이템 표시", "Upgraded": "{{term}} - 충분한 경험치가 있지만 일부 노드가 잠금 해제되지 않은 아이템을 표시합니다.", "XPComplete": "{{term}} - (업그레이드 잠금 해제 유무와 상관없이) 경험치를 주입할 수 없는 아이템을 표시합니다." }, "Location": "앱 내 위치를 기준으로 아이템을 표시합니다. left/middle/right는 캐릭터의 시각적인 위치이며, inleftchar는 항상 작동하지만 나머지 두 개는 소지하고 있는 캐릭터 수를 기준으로 합니다. current는 최근/현재 로그인한 캐릭터(노란색 삼각형으로 표시됨)입니다.", "LockAllFailed": "아이템 잠금 실패", "LockAllSuccess": "아이템 {{num}}개 잠김", "Locked": "잠금 여부를 기준으로 아이템을 표시합니다.", "Masterwork": "걸작 능력치나 단계에 따라 아이템을 표시합니다.", "MasterworkKills": "걸작 처치 추적기 수치에 따라 아이템을 표시합니다.", "MaxPower": "각 슬롯에서 가장 높은 전투력의 아이템을 표시합니다.", "MaxPowerLoadout": "로드아웃에서 각 직업의 전투력을 최대화할 수 있는 아이템을 표시합니다.", "Memento": "유품 소켓이 있는 무기를 표시합니다.", "ModSlot": "특정 개조 부품 슬롯이 있는 방어구를 표시합니다.", "Mods": { "Y3": "개조 부품이 장착된 아이템을 표시합니다." }, "Name": "필터와 일치(exactname:)하거나 부분적으로 일치(name:)하는 아이템을 표시합니다. 따옴표를 사용하여 전체 문구를 검색합니다.", "NamedStat": "특정 능력치가 있는 아이템을 표시합니다.", "Negate": "검색 결과를 제외하려면 {{notexample}}나 {{notexample2}}와 같이 검색어 앞에 마이너스 기호나 \"not\"을 붙이세요.", "NewItems": "새로운 아이템을 표시합니다.", "Notes": "메모가 입력된 아이템을 검색합니다.", "OriginTrait": "기원 속성이 포함된 무기를 표시합니다.", "Ornament": "현재 상태에 대한 필터와 장식이 있는 아이템을 표시합니다.", "PartialMatch": "이름, 설명, 특성이나 개조 부품이 필터와 부분적으로 일치하는 아이템을 표시합니다. 따옴표를 사용하여 전체 문구를 검색합니다.", "PatternUnlocked": "형성 여부에 관계없이 무기 형성 패턴이 잠금 해제된 아이템을 표시합니다.", "Perk": "특성이나 개조 부품의 이름이나 설명이 필터와 부분적으로 일치하는 아이템을 표시합니다. 전체 구문을 검색하시려면 따옴표를 사용하세요.", "PerkName": "특성이나 개조 부품의 이름이 필터와 일치(exactperk:)하거나 부분적으로 일치(perkname:)하는 아이템을 표시합니다. 따옴표를 사용하여 전체 문구를 검색합니다.", "PinnacleReward": "최고급 보상을 주는 이정표를 표시합니다.", "Postmaster": "우편 담당자가 소지하고 있는 아이템", "PowerKeywords": "현재 시즌의 전투력 제한 값 대신 pinnaclecap이나 softcap 단어를 사용합니다.", "PowerLevel": "전투력을 기준으로 아이템을 표시합니다. $t(Filter.PowerKeywords)", "PowerfulReward": "강력한 보상을 주는 이정표를 표시합니다.", "PrismaticDamageType": "빛, 어둠 피해 유형에 따라 아이템을 표시합니다. 빛 유형은 전기, 태양, 공허입니다. 어둠 유형은 시공, 초월입니다.", "Quality": "전체 능력치의 품질 비율에 따라 아이템을 표시합니다. '{{percentage}}'는 '{{quality}}'의 단축어입니다.", "RandomRoll": "무작위 특성의 아이템을 표시합니다.", "RarityTier": "희귀도 등급에 따라 아이템을 표시합니다.", "Reforgeable": "총제작자가 재련할 수 있는 아이템을 표시합니다.", "Release": "특정 버전이나 이벤트에서 제공되는 아이템을 표시합니다.", "RequiredLevel": "필요 레벨에 따라 아이템을 표시합니다.", "RetiredPerk": "더 이상 얻을 수 없는 특성이 포함된 아이템을 표시합니다.", "SearchPrompt": "사용 가능한 필터 명령어 검색", "Season": "데스티니 2의 특정 시즌에서 나온 아이템을 표시합니다.", "StackFull": "스택 당 최대 소지 제한에 도달한 아이템 표시 (강화 코어, 이상한 동전, 총기 제작 재료 등)", "StackLevel": "아이템의 개수를 기준으로 표시합니다.", "Stackable": "한 칸에 중첩 가능한 아이템(탄약 신스, 이상한 동전 등)을 표시합니다.", "StatLower": "동일한 종류의 다른 방어구보다 능력치가 확실하게 낮은 방어구를 표시합니다.", "Stats": "특정 능력치의 값에 따라 아이템을 표시합니다. $t(Filter.StatsExtras)", "StatsBase": "방어구를 장착된 개조 부품과 걸작을 제외한 기본 능력치 값으로 필터링. $t(Filter.StatsExtras)", "StatsExtras": "여러 능력치 이름을 +, & 기호로 연결하여 능력치를 더할 수 있습니다. highest, secondhighest, thirdhighest 등의 특수 키워드가 있으며, 아이템의 능력치 순위를 기준으로 합니다. 사용자 정의 능력치 또한 검색어가 있으며, 사용자 정의 능력치 설정에서 확인할 수 있습니다.", "StatsLoadout": "특정 능력치를 최대한 올릴 수 있는 아이템 조합을 찾습니다.", "StatsMax": "특정 능력치가 가장 높은 방어구를 찾습니다. 최대치를 가진 모든 방어구가 포함됩니다.", "StatsOrdinal": "특정 능력치를 기반으로 3.0 방어구를 찾습니다.", "Tags": { "Tag": "특정 태그가 있는 아이템을 표시합니다.", "Tagged": "태그가 있는 아이템을 표시합니다." }, "Tier": "0~5 등급에 따라 아이템을 표시합니다.", "Timelost": "\\(잃어버린 시간\\)", "Tracked": "퀘스트/현상금을 추적 여부에 따라 표시합니다.", "Transferable": "캐릭터 간 이동 가능한 아이템", "Trashlist": "희망 아이템 목록의 쓰레기 목록에 포함된 아이템을 표시합니다.", "TunedStat": "특정 능력치에 대한 개조 부품이 장착된 아이템을 표시합니다.", "Unascended": "승천 노드가 있으나 승천을 하지 않은 아이템을 표시합니다.", "Undo": "되돌리기", "UnlockAllFailed": "아이템 잠금 해제 실패", "UnlockAllSuccess": "아이템 {{num}}개 잠금 해제됨", "Vendor": "특정 상인에게서 얻을 수 있는 아이템", "VendorItem": "소지품이 아닌 상인에게서 가져온 아이템입니다. 로드아웃 최적화에서 상인 아이템을 제외하는데 유용하게 쓰입니다.", "Weapon": "무기인 아이템을 표시합니다.", "WeaponLevel": "무기의 전투력을 기준으로 표시합니다.", "WeaponType": "무기의 공격 속성을 기준으로 표시합니다.", "Wishlist": "희망 아이템 목록에 포함된 아이템을 표시합니다.", "WishlistDupe": "중복된 아이템 중 하나 이상이 희망 아이템 목록에 포함된 아이템을 표시합니다.", "WishlistEnabled": "희망 아이템 목록 조합으로 나올 수 있는 아이템을 표시합니다.", "WishlistNotes": "메모가 일치하는 희망 아이템 목록의 아이템을 표시합니다", "WishlistUnknown": "불러온 희망 아이템 목록에 추천 특성 조합이 없는 아이템을 표시합니다.", "Year": "데스티니의 특정 연도에서 나온 아이템을 표시합니다." }, "General": { "ClickForDetails": "자세한 내용은 클릭", "Close": "닫기", "Confirm": "확인?", "UserGuideLink": "사용 설명서" }, "Glyphs": { "Axe": "도끼", "DarkAbility": "어둠 능력", "Gilded": "금박", "Harmonic": "조화", "HiveSword": "군체 검", "LightAbility": "빛 능력", "LightLevel": "빛 레벨", "Misadventure": "사고사", "Missing": "누락", "OpenSymbolsPicker": "기호 선택기 열기", "Prismatic": "프리즘", "Quickfall": "빠른 착지", "RespawnRestricted": "부활 제한", "ScorchCannon": "소각 대포", "SearchSymbols": "기호 검색...", "Smoke": "연막" }, "Header": { "About": "DIM 정보", "AutoRefresh": "플레이하는 동안 DIM을 자동으로 새로고침합니다.", "BulkTag": "아이템 대량으로 태그하기", "BungieNetAlert": "Bungie 알림", "Clear": "검색 필터 초기화", "CompareMatching": "아이템 비교", "DeleteSearch": "검색 삭제", "FilterHelp": "아이템/특성, {{example}} 등 검색", "FilterHelpBrief": "아이템 검색", "FilterHelpLoadouts": "로드아웃 이름 및 메모 검색", "FilterHelpMenuItem": "필터 도움말...", "FilterHelpOptimizer": "빌드에 포함된 방어구 필터링 (예: {{example}})", "FilterHelpProgress": "이정표와 현상금 검색", "FilterHelpRecords": "업적과 수집품 검색", "FilterMatchCount_other": "아이템 {{count}}개", "Filters": "필터", "InstallDIM": "앱으로 설치", "InstallDIMBanner": "DIM을 홈 화면에 앱으로 설치", "Inventory": "소지품", "IosPwaPrompt": "사파리에서는 공유 아이콘(하단 중앙 버튼)을 누르고 \"홈 화면에 추가\"를 선택하세요.", "KeyboardShortcuts": "키보드 단축키", "LaunchDIMAlone": "다른 창", "MaterialCounts": "재료 수", "Menu": "메뉴", "ProfileAge": "데스티니 서버에서 마지막으로 업데이트된 데이터를 {{age}} 전에 보냈습니다.\nDIM을 새로고침하면 새로운 데이터를 받을 수 있으나, 캐시된 정보를 다시 보낼 수도 있습니다.", "Refresh": "데스티니 데이터 새로고침 [R]", "ReloadApp": "앱 새로고침", "ReportBug": "버그 제보", "SaveSearch": "검색 저장", "SearchActions": "검색 액션 열기", "SearchResults": "아이템 표시", "Shop": "상점", "TagAs": "'{{tag}}'(으)로 태그", "UpgradeDIM": "DIM 업데이트", "WhatsNew": "새로운 기능" }, "Help": { "CannotMove": "그 아이템은 해당 캐릭터에서 옮길 수 없습니다.", "NoStorage": "DIM이 데이터를 저장할 수 없음", "NoStorageMessage": "DIM이 브라우저에 데이터를 저장할 수 없습니다. 브라우저를 시크릿 모드로 사용하거나, 디스크 용량이 부족하거나, 브라우저 버그로 인해 이와 같은 문제가 발생할 수 있습니다. 컴퓨터를 재시작해 보시길 바랍니다! 문제가 해결될 때까지 DIM에 로그인하거나 사용할 수 없습니다." }, "Hotkey": { "Armory": "아이템을 무기고에서 표시", "CheatSheetTitle": "키보드 단축키:", "ClearDialog": "대화 상자 숨기기", "ClearNewItems": "새 아이템 초기화", "Enter": "엔터", "ItemPopupTab": "아이템 세부정보 탭 전환", "LockUnlock": "아이템 잠금 또는 잠금 해제", "MarkItemAs": "아이템을 '{{tag}}'로 표시하기", "Menu": "메뉴 표시 전환", "Note": "메모 입력", "Pull": "아이템을 활성화된 캐릭터로 이동", "RefreshInventory": "소지품 새로고침", "ShowHotkeys": "키보드 단축키 표시", "StartSearch": "검색 시작", "StartSearchClear": "새로운 검색 시작", "Tab": "탭", "Vault": "아이템을 금고로 이동" }, "InGameLoadout": { "ClearSlot": "슬롯 {{index}} 초기화", "Create": "로드아웃 생성", "CreateTitle": "현재 장비에서 게임 내 로드아웃 생성", "CurrentlyEquipped": "현재 착용 중인 아이템", "DeleteFailed": "로드아웃 삭제 실패", "Deleted": "로드아웃 삭제됨", "DeletedBody": "게임 내 로드아웃 {{index}}번 슬롯 삭제됨", "EditFailed": "로드아웃 업데이트 실패", "EditIdentifiers": "식별자 변경", "EditTitle": "로드아웃 이름 및 아이콘 변경", "EquipNotReady": "게임 내 장착 준비 안 됨", "EquipReady": "게임 내 장착 준비 완료", "LoadoutDetails": "로드아웃 설명", "MatchingLoadouts": "일치하는 로드아웃:", "PrepareEquip": "장착 준비", "Replace": "로드아웃 {{index}} 교체", "Save": "로드아웃 업데이트", "SaveIdentifiers": "식별자 업데이트", "SnapshotFailed": "장착된 로드아웃 스냅샷 생성 실패" }, "Infusion": { "Filter": "아이템 필터", "InfuseSource": "{{name}}을(를) 주입할 아이템 선택", "InfuseTarget": "{{name}}에 주입할 아이템 선택", "InfusionMaterials": "주입 재료", "NoItems": "주입할 수 있는 아이템이 없음.", "NoTransfer": "주입 재료 이동\n{{target}}을(를) 옮길 수 없습니다.", "SwitchDirection": "전환", "TransferItems": "이동" }, "Inventory": { "ClickToExpand": "(클릭하여 확장)", "MissingSilver": "실버 잔액은 게임을 플레이하는 동안만 사용할 수 있습니다." }, "Item": { "SetBonus": { "NPiece_other": "{{count}}개" }, "ThumbsDown": "반대", "ThumbsUp": "추천" }, "ItemFeed": { "ClearFeed": "피드 지우기", "Description": "최근 아이템", "HideTagged": "태그된 아이템 숨기기", "NoNewItems": "새 아이템 없음", "ShowOlderItems": "오래된 아이템 표시" }, "ItemMove": { "Consolidate": "{{name}} 합쳐짐", "Distributed": "{{name}}을(를) 분배함\n{{name}}을(를) 모든 캐릭터에게 공평하게 나누었습니다.", "MovingItem": "금고로 이동", "MovingItem_female": "{{target}}에게 이동", "MovingItem_male": "{{target}}에게 이동", "ToStore": "이제 모든 {{name}}은(는) {{store}}에 있습니다.", "ToVault": "이제 모든 {{name}}은(는) 금고에 있습니다." }, "ItemPicker": { "ChooseItem": "아이템 선택:", "SearchPlaceholder": "아이템 검색" }, "ItemService": { "BucketFull": { "Guardian": "{{store}}에 '{{itemtype}}' 아이템이 너무 많습니다.", "Guardian_female": "{{store}}에 '{{itemtype}}' 아이템이 너무 많습니다.", "Guardian_male": "{{store}}에 '{{itemtype}}' 아이템이 너무 많습니다.", "Vault": "{{store}}에 '{{itemtype}}' 아이템이 너무 많습니다." }, "Classified": "이 아이템은 비공개 상태이며 옮길 수 없습니다.", "Classified2": "비공개 아이템. Bungie가 이 아이템의 정보를 제공하지 않았습니다. 메모를 추가하고 \"notes:\" 필터를 이용하여 검색하세요.", "Deequip": "{{itemname}} 대신 장착할 다른 아이템을 찾을 수 없음", "ExoticError": "{{slot}} 슬롯에 있는 경의 등급의 아이템이 장착 해제 될 수 없어 {{itemname}}을(를) 장착하실 수 없습니다. {{error}}", "NotEnoughRoom": "{{store}}에서 {{itemname}}을(를) 옮길 공간을 만들기 위해 움직일 수 있는 아이템이 없습니다.", "NotEnoughRoomGeneral": "이 아이템을 이동할 공간이 없습니다.", "OnlyEquippedClassLevel": "이 아이템은 {{class}}이고 {{level}}레벨 이상인 캐릭터만 장착할 수 있습니다.", "OnlyEquippedLevel": "이 아이템은 {{level}}레벨 이상인 캐릭터만 장착할 수 있습니다.", "PostmasterAlmostFull": "거의 가득 참!", "PostmasterFull": "꽉 참!", "PreviewVendor": "{{type}} 콘텐츠 미리보기", "StackFull": "이미 최대 개수의 {{name}}을(를) 가지고 있습니다.", "StoreName": "{{genderRace}} {{className}}" }, "KillType": { "ClassAbilities": "직업 능력", "Finisher": "필살기", "Grenade": "수류탄", "Melee": "근접 공격", "Precision": "정밀 처치", "Super": "궁극기" }, "LB": { "AddStack": "동일한 개조 부품 추가", "AdvancedOptions": "고급 옵션", "ChooseAMod": "개조 부품 선택", "ChooseASetBonus": "세트 보너스를 선택하세요", "ChooseAnExotic": "경이 선택", "ClearLocked": "잠금 해제", "ContainsVendorItems": "이 로드아웃은 상인의 아이템을 포함합니다", "Current": "현재", "Equip": "{{character}}에게 장착", "Exclude": "제외된 아이템", "ExcludeHelp": "아이템을 쉬프트 + 클릭(또는 이 바구니에 끌어서 놓기)하여 특정 장비를 제외한 세트를 만듭니다.", "ExistingBuildStats": "기존 빌드 능력치", "ExistingBuildStatsNote": "엄격하게 더 높은 능력치를 가진 빌드만 표시합니다.", "FilterSets": "장비 조합 필터", "Help": { "And": "이 특성을 가진 모든 방어구들은 (\"그리고\") 를 사용 할 것입니다", "ChangeNodes": "로드아웃을 생성하려면 표기된 값으로 게임 내에서 지능, 힘, 규율 스탯을 변경하세요.", "Discipline": "규율 스탯은 수류탄 재충전 속도를 올려줍니다.", "DragAndDrop": "잠긴 바구니에 아이템을 끌어다 놓아 해당 장비로만 이루어진 세트를 만드세요", "Help": "도움이 필요하신가요?", "HigherTiers": "높은 등급이 더 좋습니다.", "Intellect": "지능 스탯은 궁극기의 재충전 시간을 줄여줍니다.", "Lock": "여러 개의 특성을 잠그려면 잠금 바구니를 누르고 특성을 선택하세요.", "MultiPerk": "한 방어구에 여러 특성를 쓰기 위해 원하는 특성을 쉬프트 + 클릭하세요", "NoPerk": "특성이 보이지 않는 것은 그 특성을 가진 방어구가 없다는 것을 의미합니다", "Or": "이 특성들을 가진 모든 방어구들은 (\"그리고\") 를 사용 할 것입니다", "ShiftClick": "해당 장비를 제외한 세트를 만들려면 아이템을 쉬프트 클릭하세요.", "StatsIncrease": "아이템의 방어 수준이 올라가면, 그 아이템의 스탯(힘/지능/규율) 또한 올라 갈 것입니다", "Strength": "힘 스탯은 근접공격 재충전 시간을 줄입니다", "Synergy": "당신이 쓰는 무기 유형을 위한 탄약 증가 특성이 달린 방어구를 찾아보세요", "Tier11Example": "4/5/2 (11등급 빌드) 는 4의 지능 스탯, 5의 규율 스탯, 2의 힘 스탯입니다. (4+5+2 = 11등급)" }, "HideAllConfigs": "모든 설정 숨기기", "HideConfigs": "설정 숨기기", "IncompatibleWithOptimizer": "이 아이템은 로드아웃 최적화와 호환되지 않습니다. 수집품에서 새로운 버전을 다시 획득하세요.", "LB": "로드아웃 최적화", "LightMode": { "HelpCurrent": "현재 방어 레벨에 따른 로드아웃 계산하기", "HelpScaled": "모든 장비가 350 방어라는 가정 하에 로드아웃 계산", "LightMode": "전투력 모드" }, "Loading": "가장 좋은 장비 조합 불러오는 중", "LockEquipped": "장착된 아이템 잠금", "LockPerk": "특성 잠금", "Locked": "잠긴 아이템", "LockedHelp": "바구니에 아이템을 끌어다 놓아 해당 장비가 포함된 조합을 만드세요. 아이템을 제외하려면 쉬프트 + 클릭하세요.", "Missing2": "완전한 세트를 만들기 위해 필요한 희귀, 전설, 경이 등급의 장비가 부족합니다!", "ProcessingMode": { "Fast": "빠름", "Full": "최대", "HelpFast": "가장 좋은 장비만 표시", "HelpFull": "더 많은 장비들을 보지만, 더 오래걸림", "ProcessingMode": "처리 모드" }, "RemoveStack": "동일한 개조 부품 제거", "Scaled": "확대 됨", "SearchAMod": "개조 부품 이름이나 설명 검색", "SearchASetBonus": "", "SearchAnExotic": "경이 이름이나 설명 검색", "SelectExotic": "경이 선택", "SelectMods": "개조 부품 선택", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} 활동 개조 부품", "SelectSetBonus": "세트 보너스 선택", "SelectSubclassOptions": "하위직업 사용자 정의", "ShowAllConfigs": "모든 설정 표시", "ShowConfigs": "설정 표시", "ShowGear": "{{class}} 방어구", "Vendor": "상인 아이템 포함" }, "Loading": { "Accounts": "데스티니 계정 불러오는 중...", "Code": "DIM 코드 불러오는 중...", "FilterHelp": "검색 도움말 불러오는 중...", "Profile": "데스티니 프로필 불러오는 중...", "Vendors": "데스티니 상인 불러오는 중..." }, "LoadoutAnalysis": { "Analyzed": "{{numLoadouts}}개의 로드아웃 분석됨", "Analyzing": "{{numAnalyzed}}/{{numLoadouts}} 로드아웃 분석하는 중", "BetterStatsAvailable": { "Description": "이 로드아웃에서 다른 방어구나 개조 부품을 선택하면 더 높은 능력치에 도달할 수 있습니다. 더 나은 빌드를 보려면 \"$t(Loadouts.OpenInOptimizer)\"를 선택하세요.", "Name": "더 좋은 능력치 사용 가능" }, "BetterStatsAvailableFontNote": "메모: 이 로드아웃은 \"... 샘\" 개조 부품을 사용하며 능력치가 200을 넘길 수 있습니다. DIM이 초과되는 능력치를 줄여서 더 나은 능력치를 찾을 수 있습니다. 이러한 과정을 무시하려면 로드아웃에서 \"$t(Loadouts.IncludeRuntimeStatBenefits)\"을 비활성화합니다.", "DoesNotRespectExotic": { "Description": "이 로드아웃의 로드아웃 최적화 설정은 특정 경이 아이템을 지정하고 있지만, 로드아웃에 일치하는 경이 아이템이 없습니다.", "Name": "잘못된 경이 아이템" }, "DoesNotSatisfyStatConstraints": { "Description": "이 로드아웃의 로드아웃 최적화 설정은 특정 능력치의 최솟값을 지정하고 있지만, 로드아웃이 그 등급을 달성하지 않습니다.", "Name": "잘못된 능력치 최솟값" }, "EmptyFragmentSlots": { "Description": "하위직업에 빈 조각 슬롯이 있습니다.", "Name": "빈 조각 슬롯" }, "InvalidMods": { "Description": "이 로드아웃의 일부 개조 부품이 만료되었거나 어떤 방어구에도 장착할 수 없습니다.", "Name": "만료된 개조 부품" }, "InvalidSearchQuery": { "Description": "이 로드아웃은 로드아웃 최적화에서 유효하지 않은 검색어를 사용하여 생성되었습니다.", "Name": "잘못된 검색어" }, "ItemsDoNotMatchSearchQuery": { "Description": "이 로드아웃은 로드아웃 최적화에서 검색어로 생성되었으며, 로드아웃에서 한 개 이상의 아이템을 제외합니다.", "Name": "제외된 아이템 검색" }, "MissingItems": { "Description": "이 로드아웃의 일부 아이템이 더 이상 소지품에 없습니다.", "Name": "누락된 아이템" }, "ModsDontFit": { "Description": "방어구를 업그레이드하여도 로드아웃의 모든 개조 부품을 장착할 수 없습니다.", "Name": "할당되지 않은 개조 부품" }, "NeedsArmorUpgrades": { "Description": "이 로드아웃의 방어구를 업그레이드해야 모든 개조 부품이나 특정 능력치를 달성할 수 있습니다.", "Name": "방어구 업그레이드 필요" }, "NotAFullArmorSet": { "Description": "이 로드아웃은 모든 방어구를 포함하지 않아 더 이상 분석할 수 없습니다.", "Name": "전체 방어구 세트 아님" }, "TooManyFragments": { "Description": "하위직업의 상에서 제공한 조각의 수를 초과하였습니다.", "Name": "조각이 너무 많음" }, "UsesSeasonalMods": { "Description": "이 로드아웃은 특정 시즌에서만 사용 가능한 개조 부품에 의존합니다. 시즌이 종료되면 일부 개조 부품을 사용할 수 없거나 방어구 에너지 수용량을 초과할 수 있습니다.", "Name": "시즌 개조 부품 사용" } }, "LoadoutBuilder": { "All": "전체", "AlwaysAutoMods": "계략 및 개조 부품이 항상 자동으로 선택됩니다.", "AnyExotic": "모든 경이", "AnyExoticDescription": "경이 아이템이 세트에 반드시 포함되어야 하지만, 어떤 경이 아이템이라도 상관없습니다.", "Artifice": "계략", "AssumeMasterwork": "걸작 가정", "AssumeMasterworkOptions": { "All": "모든 방어구: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "모든 방어구: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\n방어구 2.0 경이: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "계략 능력치 개조 부품을 수용할 수 있도록 강화되었습니다.", "Current": "현재 능력치, 최소 에너지 레벨 {{minLoItemEnergy}} 가정", "Legendary": "전설: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\n경이: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "걸작 능력치 보너스, 최소 에너지 레벨 10 가정.", "None": "모든 방어구: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "능력치 개조 부품 자동 추가", "AutomaticallyPicked": "이 개조 부품은 빌드의 능력치를 향상시키기 위해 자동으로 추가되었습니다.", "CompareLoadout": "로드아웃 비교", "ConfirmOverwrite": "로드아웃 \"{{name}}\"의 방어구를 새로운 방어구 조합으로 변경하시겠습니까?", "DecreaseStatPriority": "능력치 우선도 감소", "DisabledByAutoStatMods": "능력치 개조 부품은 로드아웃 최적화에서 자동으로 선택됩니다.", "DisabledDueToMaintenance": "Bungie API 점검으로 인하여 로드아웃 최적화가 비활성화되었습니다.", "EquipItems": "장착", "ExcludeItem": "아이템 제외", "ExcludeVendors": "\"not:vendor\"를 검색하여 로드아웃 최적화에서 상인 아이템을 제외합니다.", "ExcludedItems": "제외된 아이템", "ExistingLoadout": "기존 로드아웃", "Exotic": "경이 방어구", "ExoticClassItemPerks": "특정 특성을 원하시면, exactperk:\"진리의 정신\"과 같이 검색해보세요. 로드아웃 최적화에서 특성을 클릭하여 아이템 필터에 추가하거나 제거할 수 있습니다.", "ExoticSpecialCategory": "특수", "FOTLWildcardWarning": "이 세트는 가면 축제의 가면을 포함하고 있습니다. 원하는 세트 보너스를 활성화하려면 수동으로 개조 부품을 적용하세요.", "Filter": "설정", "IgnoreStat": "체크가 해제된 경우 로드아웃 최적화가 장비 조합을 구성할 때 이 능력치를 무시합니다.", "IncreaseStatPriority": "능력치 우선도 증가", "Legendary": "전설", "LimitToNewFeaturedGear": "새로운/추천 장비로 제한", "LockItem": "아이템 잠금", "MissingClass": "빌드 대상: {{className}}", "MissingClassDescription": "소지하지 않은 직업의 빌드입니다.", "MwExotic": "경이", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "활성화된 검색어가 DIM이 빌드에 포함할 수 있는 아이템을 제한하고 있습니다", "AllowAutoStatMods": "DIM이 능력치 개조 부품을 추가할 수 있도록 허용", "AlwaysInvalidMods": "이 개조 부품이 소지한 아이템에 맞지 않음:", "AssumeMasterworked": "DIM이 방어구 걸작을 추천하도록 허용", "AssumptionsRestricted": "DIM이 방어구 에너지 유형 변경을 추천할 수 없음:", "BadSlot": "{{bucketName}} 슬롯에서 이 모드를 장착할 수 있는 허용된 아이템이 없음:", "ExoticDoesNotExist": "소지품에 선택한 경이 방어구가 없습니다.", "Header": "빌드를 찾을 수 없습니다. DIM에서 빌드를 찾을 수 없는 이유는 다음과 같습니다:", "LowerBoundsFailed": "대부분의 조합이 최소 능력치 요구사항을 충족하지 않음", "MaybeAllowMoreItems": "다른 아이템을 허용하는 것을 고려해보세요:", "MaybeDecreaseLowerBounds": "최소 능력치 요구사항을 줄이는 것을 고려해보세요", "MaybeRemoveMods": "일부 개조 부품을 제거해보세요:", "MaybeRemoveSearchQuery": "검색 창에서 필터를 지우거나 변경해 보세요", "ModAssignmentFailed": "대부분의 조합이 요청한 개조 부품을 장착할 수 없음", "RemoveMods": "이 개조 부품 제거", "RemoveSetBonuses": "세트 보너스를 제거해보세요", "SetBonuses": "선택된 세트 보너스의 활성화에 필요한 아이템이 없을 수 있습니다." }, "NoExotic": "경이 없음", "NoExoticDescription": "검색 창에서 \"not:exotic\"를 검색하는 것과 동일함 - 장비 조합이 경이 방어구를 사용하지 않음.", "NoExoticPreference": "경이 장비가 선택되지 않음", "NoExoticPreferenceDescription": "능력치를 최대화하기 위해 경이 방어구가 사용됩니다.", "NoLoadoutsToCompare": "비교할 로드아웃 없음", "None": "없음", "OptimizerExplanationGuide": "사용 설명서에서 더 자세한 정보나 비디오 튜토리얼을 찾을 수 있습니다.", "OptimizerExplanationMods": "경이 아이템, 개조 부품, 하위직업을 선택하세요. 이들은 빌드에 능력치를 추가하며, 방어구에 이미 장착된 개조 부품은 무시됩니다.", "OptimizerExplanationSearch": "검색 창을 사용하여 고려할 방어구를 좁혀보세요 (예: {{example}}). 검색 조건과 일치하는 방어구가 없는 슬롯은 모든 아이템을 대상으로 간주됩니다.", "OptimizerExplanationStats": "가장 중요한 능력치를 위로 끌어서 놓고, 최대화하고 싶지 않은 능력치는 선택 해제하세요.", "OptimizerSet": "최적화 조합", "PinnedItems": "고정된 아이템", "PinnedItemsFinePrint": "검색 필터는 로드아웃 최적화 설정과 같이 저장되지만, 고정된 아이템과 제외된 아이템은 저장되지 않습니다. 고정된 아이템과 제외된 아이템은 DIM이 더 좋은 능력치의 빌드를 탐색할 때는 무시됩니다.", "ProcessingSets": "가장 높은 능력치의 조합 찾는 중...", "SaveAs": "다른 이름으로 저장", "SetBonus": "세트 보너스", "SpeedReport": "{{cpus}}개의 CPU 코어로 {{time}}초 동안 {{combos, number}}개 조합을 계산하였습니다.", "StatConstraints": "능력치 우선도 & 범위", "StatMax": "최대", "StatMin": "최소", "StatRangeTooltip": "현재 최소/최대 설정에서, 이 능력치가 {{min}}에서 {{max}} 사이인 로드아웃이 존재합니다. 더블 클릭하면 최솟값을 {{max}}로 설정합니다.", "StatTotal": "합계: {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "모든 개조 부품을 추가할 수 없습니다.", "UnableToAddAllModsBody": "{{mods}}개의 개조 부품을 장착할 수 있는 공간이 없습니다.", "UnlockItem": "아이템 고정 해제" }, "LoadoutFilter": { "Contains": "아이템이나 개조 부품이 필터와 일치하는 로드아웃을 표시합니다. 따옴표를 사용하여 이름에 공백이 포함된 아이템을 검색합니다.", "FashionOnly": "패션 아이템(안료나 장식)만 포함된 로드아웃을 표시합니다.", "LoadoutLight": "계산된 전투력을 기반으로 로드아웃을 표시합니다. 현재 시즌의 최대 전투력 제한 값 대신 pinnaclecap이나 softcap 키워드를 사용하세요.", "ModsOnly": "방어구 개조 부품만 포함된 로드아웃을 표시합니다.", "Name": "필터와 일치(exactname:)하거나 부분적으로 일치(name:)하는 로드아웃을 표시합니다. 따옴표를 사용하여 전체 문구를 검색합니다.", "Notes": "메모로 로드아웃을 검색합니다.", "PartialMatch": "이름이나 메모가 필터와 부분적으로 일치하는 로드아웃을 표시합니다. 따옴표를 사용하여 전체 문구를 검색합니다.", "Season": "마지막으로 수정된 데스티니 2의 시즌에 따라 로드아웃을 표시합니다.", "Subclass": "하위직업 이름이나 피해 유형이 필터와 부분적으로 일치하는 로드아웃을 표시합니다." }, "Loadouts": { "Abilities": "능력", "Actions": "{{title}} 액션", "AddEquippedItems": "장착된 아이템 추가", "AddNotes": "메모 추가", "AddUnequippedItems": "장착하지 않은 아이템 추가", "Any": "모든 직업", "Apply": "적용", "ApplyInGameLoadoutInGame": "로드아웃을 장착할 준비가 되었으나, 활동에 참가 중인 경우 게임 내에서 장착해야 합니다.", "ApplyMods": "개조 부품 적용 중", "ApplySearch": "\"{{query}}\"로 검색된 아이템 이동", "ArmorStats": "방어구 능력치", "ArtifactUnlocks": "유물 잠금 해제", "ArtifactUnlocksDesc": "Bungie.net의 제한으로 인해 DIM이 유물을 자동으로 관리할 수 없습니다. 로드아웃을 적용하기 전에 게임 내에서 유물을 잠금 해제해야 합니다.", "ArtifactUnlocksWithSeason": "유물 잠금 해제 – S{{seasonNumber}}", "BadLoadoutShare": "공유된 로드아웃 불러오기 실패", "BadLoadoutShareBody": "불러오려는 로드아웃이 잘못되었습니다: {{error}}", "Before": "'{{name}}' 이전", "CancelEditing": "변경 취소", "CannotCustomizeSubclass": "이 하위직업의 설정을 변경할 수 없음", "ChooseItem": "{{name}} 추가", "ClassType": "아무 직업 로드아웃", "ClassTypeMismatch": "{{className}} 아이템을 이 로드아웃에 추가할 수 없습니다.", "ClassTypeMissing": "로드아웃을 생성할 {{className}}이(가) 없습니다", "ClassType_female": "{{className}} 로드아웃", "ClassType_male": "{{className}} 로드아웃", "Classified": "일부 아이템이 비공개 상태이며 최대 전투력 계산에 포함되지 않았습니다.", "ClearLoadoutParameters": "로드아웃 최적화 설정 제거", "ClearSection": "모두 제거", "ClearSpace": "다른 아이템 이동", "ClearSpaceArmor": "다른 방어구 이동", "ClearSpaceWeapons": "다른 무기 이동", "ClearUnsetMods": "다른 개조 부품 제거", "ClearingSpace": "다른 아이템 이동 중", "CopyAndEdit": "사본 편집", "Create": "로드아웃 생성", "CurrentlyEquipped": "현재 착용 중인 아이템", "Deequip": "다른 캐릭터의 아이템 장착 해제 중", "Delete": "삭제", "DimLoadouts": "DIM 로드아웃", "Edit": "로드아웃 편집", "EditBrief": "수정", "EquipInGameLoadout": "게임 내 로드아웃 장착", "EquipItems": "장비 장착 중", "EquippableDifferent1": "최대 전투력을 계산할 때 여러 개의 경이 아이템이 사용되었으므로, 게임 내 아이템을 장착할 때 표시된 숫자를 달성하지 못할 수 있습니다.", "EquippableDifferent2": "최대 전투력은 획득하는 아이템, 강력한 장비, 최고급 보상의 전투력을 결정할 때 \"하나의 경이\" 규칙에 의해 제한되지 않습니다.", "Failed": "로드아웃이 완전히 적용되지 않음", "Fashion": "패션 선택", "FashionOnly": "장식만", "FillFromEquipped": "장착한 아이템으로 채우기", "FillFromInventory": "장착하지 않은 아이템으로 채우기", "FilteredItems": "필터링된 아이템", "FindAnother": "다른 {{name}} 검색", "FromEquipped": "현재 장비", "Generated": "{{statTotal}} 능력치 로드아웃", "HashtagTip": "팁: 로드아웃 이름이나 메모에 #해시태그를 사용하면 이곳에 표시됩니다.", "Import": { "BadURL": "로드아웃 공유 URL이 유효하지 않음.", "Error": "로드아웃 가져오기 실패:", "Error404": "로드아웃이 존재하지 않습니다.", "PasteHere": "로드아웃을 열려면 로드아웃 주소를 붙여넣으세요." }, "ImportLoadout": "로드아웃 가져오기", "InGameActions": "게임 내 로드아웃 액션", "InGameLoadouts": "게임 내 로드아웃", "IncludeRuntimeStatBenefits": "샘 개조 부품 능력치 포함", "IncludeRuntimeStatBenefitsDesc": "\"... 샘\" 방어구 개조 부품은 방어구 충전이 있는 동안 캐릭터 능력치를 일정하게 향상시킵니다.\n\n이 설정이 켜진 경우, DIM은 이 개조 부품이 활성화된 것으로 간주하고 로드아웃의 능력치 계산과 최적화에 사용합니다.", "ItemErrorSummary_other": "{{count}}개의 아이템 오류:", "ItemLeveling": "아이템 레벨 올리기", "LoadoutName": "로드아웃 이름", "LoadoutParameters": "로드아웃 최적화 설정", "LoadoutParametersExotic": "로드아웃이 이 경이 아이템을 포함해야 함: {{exoticName}}", "LoadoutParametersQuery": "아이템이 이 검색 필터와 일치해야 함", "LoadoutParametersStats": "능력치 선호도와 최소/최대 능력치 범위", "Loadouts": "로드아웃", "MakeRoom": "우편 담당자를 위한 공간 생성", "MakeRoomDone_female_other": "{{store}}에서 {{movedNum}}개의 아이템을 옮겨서 우편 담당자 아이템 {{count}}개를 위한 공간을 만들었습니다.", "MakeRoomDone_male_other": "{{store}}에서 {{movedNum}}개의 아이템을 옮겨서 우편 담당자 아이템 {{count}}개를 위한 공간을 만들었습니다.", "MakeRoomDone_other": "{{store}}에서 {{movedNum}}개의 아이템을 옮겨서 우편 담당자 아이템 {{count}}개를 위한 공간을 만들었습니다.", "MakeRoomError": "모든 우편 담당자 아이템을 위한 공간을 만들 수 없습니다: {{error}}", "ManageLoadouts": "로드아웃 관리", "MaxSlots": "로드아웃에는 최대 {{slots}}개의 {{bucketName}}을(를) 포함할 수 있습니다.", "MaximizeLight": "최대 전투력", "MaximizePower": "최대 전투력", "MaximizeStat": "능력치 최대화", "MissingItemsWarning": "이 로드아웃의 일부 아이템이 더 이상 소지품에 없습니다.", "ModErrorSummary_other": "{{count}}개의 개조 부품 오류:", "ModPlacement": { "InvalidMods": "잘못된 개조 부품", "InvalidModsDesc_other": "{{count}}개의 개조 부품을 방어구에 장착할 수 없습니다.", "ModPlacement": "개조 부품 배치", "StackableMod": "중첩 가능", "UnassignedMods": "할당되지 않은 개조 부품", "UnassignedModsDesc_other": "{{count}}개의 개조 부품이 에너지 수용량이 부족하거나 개조 부품 슬롯이 부족하여 장착할 수 없습니다. 선택된 방어구의 에너지를 업그레이드하여도 해결되지 않습니다.", "UnstackableMod": "중첩 불가", "UpgradeCosts": "업그레이드 비용", "UpgradeCostsDesc": "요청한 개조 부품을 장착하기 위해서 일부 방어구의 에너지 수용량을 업그레이드해야 합니다. 총 업그레이드 비용:" }, "Mods": "개조 부품", "ModsOnly": "개조 부품만", "MoveItems": "아이템 이동 중", "NoSpace": "금고와 다른 캐릭터의 빈 공간이 없습니다.", "NoneMatch": "필터와 일치하는 로드아웃이 없습니다.", "NotStarted": "다른 작업이 완료되거나 소지품을 새로고침하여 불러오기를 기다리는 중", "NotesPlaceholder": "이 로드아웃에 메모를 작성하거나 #해시태그를 사용하여 그룹화합니다.", "NotificationTitle": "로드아웃: {{name}}", "OnWrongCharacterAdvice": "이 캐릭터에서 전투력이 가장 높은 아이템을 찾으려면 여기를 클릭하세요.", "OnWrongCharacterWarning": "이 캐릭터에서 전투력이 가장 높은 방어구가 다른 캐릭터에 있습니다. 떨어지는 아이템, 강력한 보상, 최고급 보상의 전투력 계산에 포함되려면 방어구가 이 캐릭터나 금고에 있어야 합니다.", "OnlyItems": "오직 장착이 가능한 아이템, 재료, 소모품들만이 로드아웃에 추가될 수 있습니다.", "OpenInOptimizer": "방어구 최적화", "OpenOnStreamDeck": "스트림덱에서 열기", "PickArmor": "방어구 선택", "PickMods": "방어구 개조 부품 추가", "Prismatic": { "Aspect": "프리즘 상", "Grenade": "프리즘 수류탄", "Melee": "프리즘 근접 공격", "Super": "궁극기 능력" }, "PullFromPostmaster": "우편 담당자에게서 수령", "PullFromPostmasterError": "우편 담당자로부터 가져올 수 없습니다: {{error}}", "PullFromPostmasterGeneralError": "우편 담당자에게서 모든 아이템을 가져올 수 없습니다.", "PullFromPostmasterNotification_female_other": "{{count}}개의 아이템을 우편 담당자에게서 {{store}}에게 이동하였습니다.", "PullFromPostmasterNotification_male_other": "{{count}}개의 아이템을 우편 담당자에게서 {{store}}에게 이동하였습니다.", "PullFromPostmasterNotification_other": "{{count}}개의 아이템을 우편 담당자에게서 {{store}}에게 이동하였습니다.", "PullFromPostmasterPopupTitle": "우편 담당자에게서 가져오기", "Random": "무작위", "Randomize": "무작위 로드아웃", "RandomizeButton": "무작위", "RandomizeNew": "무작위 생성", "RandomizeQueryHint": "팁: 먼저 아이템을 검색하여 무작위로 선택될 수 있는 아이템을 제한하세요.", "RandomizeSearch": "검색 결과에서 무작위화", "RandomizeSearchPrompt": "\"{{query}}\"로 검색된 결과에서 무작위로 장착하시겠습니까?", "Redo": "다시 실행", "RestoreAllItems": "모든 아이템", "SalvationsEdgeMods": "구원의 경계 개조 부품", "Save": "저장", "SaveAsDIM": "DIM 로드아웃으로 저장", "SaveAsNew": "새로 저장", "SaveAsNewTooltip": "기존 로드아웃을 유지하고 새로운 로드아웃으로 저장", "SaveDisabled": { "AlreadyExists": "새 로드아웃 이름을 입력해주세요.", "Empty": "로드아웃이 비어있습니다.", "NoName": "로드아웃의 이름이 필요합니다." }, "SaveLoadout": "로드아웃 저장", "Season": "시즌 {{season}}", "SetBonusesDesc": "필요한 세트 보너스", "Share": { "Copied": "클립보드로 로드아웃 주소가 복사됨", "CopyButton": "주소 복사", "Error": "공유 주소 가져오기 실패", "Fashion": "패션 (안료 & 장식)", "LoadoutOptimizer": "로드아웃 최적화 설정", "NativeShare": "주소 공유", "Notes": "메모", "NumItems_other": "아이템 {{count}}개 - 받는 사람은 소지품에서 유사한 아이템을 선택하라는 메시지가 표시됩니다", "NumMods_other": "개조 부품 {{count}}개", "Placeholder": "공유 주소 가져오는 중", "Subclass": "하위직업 사용자 정의", "Summary": "다음을 포함하는 로드아웃 공유:", "Title": "\"{{name}}\" 공유" }, "ShareLoadout": "공유", "ShowModPlacement": "개조 부품 배치 표시", "Snapshot": "게임 내 로드아웃으로 저장", "SocketOverrides": "하위직업 옵션 변경 중", "SortByEditTime": "수정한 날짜순 정렬", "SortByName": "이름순 정렬", "SubclassOptions": "{{subclass}} 옵션", "SubclassOptionsSearch": "{{subclass}} 옵션 검색", "Succeeded": "로드아웃 성공", "SyncFromEquipped": "장착한 아이템 동기화", "TooManyRequested": "{{total}}개의 {{itemname}}을(를) 가지고 있으나 로드아웃이 {{requested}}개를 필요로 합니다. 가지고 있는 모든 것을 옮겼습니다.", "TuningMods": "개조 부품", "UnassignedModError": "개조 부품이 현재 방어구에 맞지 않음", "Undo": "되돌리기", "Update": "변경사항 저장", "UpdateLoadout": "로드아웃 업데이트", "VendorsCannotEquip": "이 아이템을 소유하고 있지 않습니다. 클릭하여 대체품을 선택하거나 X를 눌러 제거합니다:" }, "Manifest": { "Download": "Bungie에서 최신 데스티니 정보 데이터베이스 다운로드하는 중...", "Error": "데스티니 정보 데이터베이스 불러오기 실패:\n{{error}}\n재시도하시려면 새로고침하십시오.", "Load": "데스티니 정보 데이터베이스 불러오는 중..." }, "Milestone": { "Daily": "일일 도전", "OneTime": "일회성 도전", "SeasonalRank": "시즌 등급 {{rank}}", "Special": "특별 이벤트 도전", "Tutorial": "튜토리얼 도전", "Unknown": "도전", "Weekly": "주간 도전" }, "Mods": { "HarmonicModDescription": "이 개조 부품의 효과는 낮은 비용으로 제공되며 장착된 하위 직업에 따라 속성이 변화됩니다." }, "MoveAmount": { "Amount": "수량:" }, "MovePopup": { "Acquired": "이 아이템은 수집품에서 잠금 해제되었습니다.", "AcquiredMod": "이 개조 부품은 수집품에서 잠금 해제되었습니다.", "AddNote": "메모 추가", "AddToLoadout": "로드아웃", "AddToLoadoutTitle": "로드아웃에 추가", "All": "전체", "ArtifactBreaker": "이 무기는 잠금 해제된 유물 특성으로 인해 {{breaker}}을(를) 가지고 있습니다.", "CannotCurrentlyRoll": "이 특성은 현재 버전의 아이템에서 얻을 수 없습니다.", "CantPullFromPostmaster": "이 아이템을 수령하려면 게임 내에서 우편 담당자를 방문해야 합니다.", "CatalystProgress": "촉매제 진척도", "CommunityData": "커뮤니티 인사이트", "Consolidate": "모으기", "DistributeEvenly": "균등하게 분배", "EnhancementTier": "{{tier}}등급", "Equip": "장착:", "EquipWithName": "{{character}}에게 장착", "FavoriteUnFavorite": { "Favorite": "{{itemType}} 즐겨찾기", "Favorited": "즐겨찾기 추가됨", "Unfavorite": "{{itemType}} 즐겨찾기 해제", "Unfavorited": "즐겨찾기 해제됨" }, "Infuse": "주입", "InfuseTitle": "주입 재료 탐색기 열기", "IntrinsicBreaker": "이 무기는 본질적으로 {{breaker}}을(를) 가지고 있습니다.", "LoadingSockets": "이 아이템의 특성과 능력치 정보가 아직 로드되지 않았습니다.", "LockUnlock": { "AutoLock": "잠금 상태가 아이템의 태그와 동기화됨", "Lock": "{{itemType}} 잠그기", "Locked": "잠금", "Unlock": "{{itemType}} 잠금 해제하기", "Unlocked": "잠금 해제됨" }, "MissingSockets": "Bungie가 서비스를 업데이트하는 도중에는 특성과 개조 부품 정보를 확인할 수 없습니다. 일반적으로 몇 시간 후 업데이트가 끝나면 다시 확인할 수 있습니다.", "Notes": "메모:", "OpenOnStreamDeck": "스트림덱에서 열기", "OverviewTab": "개요", "Owned": "이 아이템은 소지품에 있습니다.", "OwnedMod": "이 개조 부품은 소지품의 개조 부품 항목에 있습니다.", "PullItem": "{{bucket}}에서 {{store}}(으)로 이동", "PullPostmaster": "우편 담당자에게서 가져오기", "ReadLore": "Ishtar Collective에서 지식 읽기", "ReadLoreLink": "지식 읽기", "Rewards": "보상:", "SendToVault": "금고로 이동", "Store": "이동:", "StoreWithName": "{{character}}에게 이동", "Subtitle": { "QuestProgress": "{{questStepNum}} / {{questStepsTotal}} 단계", "Type": "{{classType}} {{typeName}}" }, "TabList": "아이템 세부정보 탭", "ToggleSidecar": "아이템 액션 확장 또는 축소", "TrackUntrack": { "Track": "{{itemType}} 추적", "Tracked": "추적됨", "Untrack": "{{itemType}} 추적 해제", "Untracked": "추적 안됨" }, "TriageTab": "분류", "UnreliablePerkOption": "이 특성은 수집품에서만 표시되며, 이 아이템에서 얻지 못하는 특성일 수 있습니다.", "Vault": "금고", "WeaponLevel": "무기 레벨 {{level}}" }, "Notes": { "Error": "에러! 메모는 최대 120자 까지 쓰실 수 있습니다.", "Help": "메모, #해시태그, :기호: 추가" }, "Notification": { "Cancel": "취소", "OK": "지우기" }, "Objectives": { "Complete": "완료", "Incomplete": "미완료" }, "Organizer": { "BulkMove": "이동", "BulkMoveLoadoutName": "정리 도구에서 선택됨", "BulkTag": "태그", "Columns": { "Ammo": "탄약", "Archetype": "유형", "BaseStats": "기본 능력치", "Breaker": "용사 차단기", "Crafted": "제작일", "CustomTotal": "사용자 정의 총합", "Damage": "피해 유형", "Energy": "에너지", "Event": "이벤트", "Featured": "새 장비", "Foundry": "제조사", "Frame": "프레임", "Harmonizable": "조율 가능", "Holofoil": "홀로그램 장식", "Icon": "아이콘", "ItemTier": "등급", "KillTracker": "킬 수", "Level": "레벨", "Loadouts": "로드아웃", "Location": "위치", "Locked": "잠금", "MasterworkStat": "걸작 능력치", "MasterworkTier": "걸작 등급", "ModSlot": "개조 부품 소켓", "Mods": "개조 부품", "Name": "이름", "New": "신규", "Notes": "메모", "OriginTraits": "기원 속성", "OtherPerks": "무기 부품", "PercentComplete": "% 완료", "Perks": "특성", "PerksGrid": "특성 그리드", "Power": "전투력", "Quality": "품질 %", "Recency": "최신", "Season": "시즌", "Shaders": "장식", "Source": "출처", "StatQuality": "능력치 품질", "StatQualityStat": "{{stat}}%", "Stats": "능력치", "Tag": "태그", "TertiaryStat": "3차 능력치", "Tier": "희귀도", "Traits": "무기 특성", "TuningStat": "조율", "WishList": "희망 아이템 목록", "WishListNotes": "희망 아이템 목록 메모", "Year": "연도" }, "EnabledColumns": "활성화된 열", "Lock": "잠금", "NoItems": "필터와 일치하는 아이템이 없습니다. 검색어가 있다면 지워보세요.", "NoMobile": "정리 도구를 사용하시려면 핸드폰을 가로로 돌려주세요.", "Note": "메모 설정", "OpenIn": "정리 도구에서 열기", "Organizer": "정리 도구", "SelectAll": "모두 선택", "SelectItem": "{{name}} 선택 혹은 선택 해제", "ShiftTip": "팁: 아이템을 필터링하시려면 쉬프트 키를 누르고 셀을 클릭하세요", "Stats": { "Aim": "조준", "Airborne": "공중 효율", "AmmoGeneration": "탄약 생성", "Power": "전투력", "RPM": "RPM", "Recoil": "반동", "Reload": "재장전" }, "Unlock": "잠금 해제" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "우편함이 거의 꽉 찼습니다! ({{number}}/{{postmasterSize}})", "PostmasterFull": "우편함이 꽉 찼습니다! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "현상금", "CatalystSource": "출처: {{source}}", "CrucibleRank": "등급", "Items": "퀘스트 아이템", "Milestones": "이정표 & 도전", "NoEventChallenges": "모든 이벤트 도전 완료", "NoTrackedTriumph": "추적 중인 업적이 없습니다. DIM에서 원하는 만큼 추적하세요.", "PaleHeartPathfinder": "창백한 심장 길잡이", "PercentMax": "최대까지 {{pct}}%", "PercentPrestige": "초기화까지 {{pct}}%", "PointsUsed_other": "{{count}} 포인트 사용됨", "PowerBonusHeader": "+{{powerBonus}} 전투력 보상", "PowerBonusHeaderUndefined": "다른 보상", "Progress": "진척도", "QueryFilteredTrackedTriumphs": "검색된 추적 중인 업적이 없습니다.", "QuestExpired": "만료됨", "QuestExpires": "만료일까지 ", "Quests": "퀘스트", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}점", "Resets_other": "초기화 {{count}}회", "RewardPassEndsIn": "보상 패스 종료: ", "RewardPassPrestigeRank": "명예 등급 {{rank}}", "SeasonalHub": "시즌 허브", "StatTrackers": "통계 추적기", "TrackedTriumphs": "추적 중인 업적" }, "RecordBooks": { "HideCompleted": "완료한 기록 숨기기", "RecordBooks": "기록 모음집" }, "Records": { "Title": "기록", "UniversalOrnamentSetOther": "기타" }, "SearchHistory": { "Date": "최근 사용", "DeleteAll": "저장되지 않은 검색 기록 삭제", "Description": "이전에 저장된 검색 기록입니다. 이곳에서 삭제할 수 있습니다.", "Item": "아이템 검색", "Link": "검색 기록 보기 및 편집", "Loadout": "로드아웃 검색", "Query": "검색", "Title": "검색 기록", "UsageCount": "사용 횟수" }, "Settings": { "Appearance": "모양", "ArmorArchetypeModslot": "방어구 유형 / 개조 부품 슬롯", "AutoLockTagged": "태그와 아이템 잠금 상태 동기화", "AutoLockTaggedExplanation": "DIM이 자동으로 아이템을 태그에 따라 잠그거나 해제합니다. 제작된 아이템은 재형성을 위해 잠금 해제된 상태로 유지됩니다. 이 설정이 활성화되면 태그된 아이템에 잠금 아이콘이 표시되지 않습니다.", "BadgePostmaster": "앱 아이콘에 현재 캐릭터의 우편 담당자 아이템 수 표시", "BadgePostmasterExplanation": "이 기능을 사용하려면 DIM을 앱으로 설치하고 OS가 배지 표시를 지원해야 합니다", "BothDescriptions": "두 설명 모두", "BungieDescriptionOnly": "Bungie 설명", "CharacterOrder": "캐릭터 정렬 방법", "CharacterOrderFixed": "캐릭터 나이 (PC에서 오류가 있음)", "CharacterOrderRecent": "최근 캐릭터", "CharacterOrderReversed": "최근 캐릭터 (역순)", "ColumnSize": "아이템 {{num}}개", "ColumnSizeAuto": "자동", "CommunityData": "커뮤니티 특성 인사이트", "CommunityDescriptionOnly": "커뮤니티 설명", "CsvImport": "CSV 가져오기", "CustomErrorLabel": "능력 이름은 글자만을 포함해야 하며, 수호자의 능력치 이름과 달라야 합니다.", "CustomErrorValues": "능력치 가중치는 양수여야 합니다.\n적어도 두 개의 능력치 가중치가 0보다 커야 합니다.", "CustomStatChooseName": "사용자 정의 능력치 이름 선택", "CustomStatCreate": "새 사용자 정의 능력치 만들기", "CustomStatDelete": "이 사용자 정의 능력치 삭제", "CustomStatDeleteConfirm": "이 사용자 정의 능력치를 삭제할까요?", "CustomStatDesc1": "사용자 정의 총합 능력치를 생성하기 위해 원하는 방어구 능력치를 선택하세요.", "CustomStatDesc3": "사용자 정의 능력치는 아이템 팝업, 정리 도구, 비교 화면에 표시됩니다.", "CustomStatTitle": "사용자 정의 총합 능력치", "Data": "스프레드시트", "DefaultItemSizeNote": "아이템 크기를 50px로 하면 아이템 그림이나 텍스트가 흐려지지 않고 가장 선명하게 보입니다.", "DontForgetDupes": "중복된 아이템을 빠르게 찾기 위해 is:dupe를 검색할 수 있으며, 비교 도구나 정리 도구를 사용하여 유사한 아이템을 비교 평가할 수 있습니다.", "EnableAdvancedStats": "(데스티니 1) 방어구 스탯 품질 표시", "ExpandSingleCharacter": "모든 캐릭터 표시", "ExportLoadoutSS": "로드아웃 스프레드시트", "ExportLoadoutSSHelp": "내 로드아웃을 다양한 스프레드시트 앱에서 쉽게 볼 수 있는 CSV로 다운로드합니다.", "ExportProfile": "API 프로필 응답 내보내기", "ExportSS": "소지품 스프레드시트", "ExportSSHelp": "내 아이템 리스트를 다양한 스프레드시트 앱에서 쉽게 볼 수 있는 CSV로 다운로드합니다.", "HidePullFromPostmaster": "\"$t(Loadouts.PullFromPostmaster)\" 버튼 숨기기", "Inventory": "소지품 표시", "InventoryColumns": "캐릭터 소지품 너비", "InventoryColumnsMobile": "모바일 세로 모드에서의 캐릭터 소지품 폭", "InventoryColumnsMobileLine2": "새 설정에 맞추기 위해 아이템의 크기가 조절될 것입니다.", "InventoryNumberOfSpacesToClear": "파밍 모드를 사용할 때 만들 빈 공간의 수", "Items": "아이템 표시", "Language": "언어", "LogOut": "로그아웃", "Masterworked": "걸작", "MaxParallelCores": "병렬 작업 최대 코어 수", "MaxParallelCoresExplanation": "로드아웃 최적화나 로드아웃 분석기와 같이 연산이 많은 작업에서 DIM이 사용할 CPU 코어 수를 설정합니다. 값을 높이면 성능이 향상되지만 시스템 자원 사용량도 늘어납니다.", "OrnamentDisplay": "아이템 타일에 장식 표시", "OrnamentDisplayExplanationDisabled": "아이템의 장식이 표시되지 않습니다", "OrnamentDisplayExplanationEnabled": "방어구를 가리키거나 길게 누르면 장식이 숨겨집니다", "OrnamentDisplayExplanationHide": "아이템을 가리키거나 길게 누르면 장식이 숨겨집니다", "OrnamentDisplayExplanationShow": "아이템을 가리키거나 길게 누르면 장식이 표시됩니다", "ResetToDefault": "초기화", "RestoreVaultSide": "금고 아이템을 독립된 열에 표시", "ReverseSort": "오름차순/내림차순 정렬 전환", "SetSort": "아이템 정렬 방법:", "SetVaultWeaponGrouping": "금고 무기 정렬 방법:", "Settings": "설정", "ShowNewItems": "새 아이템에 빨간색 점 표시", "SingleCharacter": "단일 캐릭터 보기", "SingleCharacterExplanation": "DIM에서 가장 최근에 플레이한 캐릭터만 표시합니다.\n숨겨진 캐릭터가 소지한 아이템은 현재 캐릭터가 사용할 수 있는 경우 금고에 표시됩니다.\n다른 직업의 아이템은 완전히 숨겨집니다.", "SizeItem": "아이템 크기", "SortByAmmoType": "탄약 유형", "SortByAmount": "수량", "SortByClassType": "필요한 직업", "SortByCrafted": "제작됨 (D2)", "SortByDeepsight": "심안 (D2)", "SortByFeatured": "새 장비 / 추천 (D2)", "SortByPrimary": "전투력", "SortByRarity": "희귀도", "SortByRating": "방어구 품질 (D1)", "SortByRecent": "최근 획득 (D2)", "SortBySeason": "시즌 (D2)", "SortByTag": "태그 ({{taglist}})", "SortByTier": "등급 (D2)", "SortByType": "유형", "SortByWeaponElement": "피해 유형", "SortCustom": "사용자 지정 정렬", "SortName": "이름", "SpacesSize_other": "{{count}}개의 공간", "Theme": "테마", "Troubleshooting": "문제 해결", "VaultArmorGroupingStyle": "직업별로 다른 줄에 방어구 표시", "VaultGroupingNone": "없음", "VaultUnder": "금고 아이템을 장착된 아이템 아래에 표시", "VaultWeaponGroupingStyle": "무기 그룹마다 다른 줄로 표시", "WeaponFrame": "무기 프레임", "WishlistRefreshNotificationBody": "업데이트가 보이지 않는 경우, 소스(GitHub 등)가 업데이트를 반영하는지 확인해 보세요!", "WishlistRefreshNotificationTitle": "희망 아이템 목록 갱신됨" }, "Sockets": { "ApplyPerks": "특성 적용", "GridStyle": "특성을 격자 모양으로 표시", "Insert": { "Ability": "능력 장착", "Aspect": "상 삽입", "Fragment": "조각 삽입", "Mod": "개조 부품 삽입", "Ornament": "장식 적용", "Projection": "고스트 투영 적용", "Shader": "안료 적용", "Super": "궁극기 장착", "Transmat": "전송 효과 적용" }, "ListStyle": "특성을 목록으로 표시", "Search": "이름 또는 설명 검색", "Select": { "Ability": "능력 미리보기", "Aspect": "상 미리보기", "Fragment": "조각 미리보기", "Mod": "개조 부품 미리보기", "Ornament": "장식 미리보기", "Projection": "고스트 투영 미리보기", "Shader": "안료 미리보기", "Super": "궁극기 미리보기", "Transmat": "전송 효과 미리보기" }, "SelectWishlistPerks": "희망 아이템 목록 특성 미리보기" }, "Stats": { "CrouchingSpeed": "웅크리기", "Custom": "사용자 정의 총합", "CustomDesc": "개조 부품이나 걸작을 제외한 선택된 기본 능력치의 사용자 정의 총합입니다. 설정에서 어떤 능력치를 포함할지 선택하세요.", "DamageResistance": "PvE 피해 저항", "Discipline": "규율", "DropLevel": "계정 전투력", "DropLevelExplanation1": "계정 전투력은 보상의 레벨을 계산할 때 기준으로 사용됩니다.", "DropLevelExplanation2": "계정 전투력은 직업 제한이나 \"하나의 경이\" 규칙을 무시하고 각 장비 슬롯마다 가장 높은 전투력의 아이템이 사용됩니다.", "EquippableGear": "장착 가능한 장비", "FlinchResistance": "피격 반동 저항", "HP": "체력", "Intellect": "지능", "MaxGearPower": "장착 가능한 최대 전투력", "MaxGearPowerAll": "모든 장비의 최대 전투력", "MaxGearPowerOneExoticRule": "장착 가능한 장비의 최대 전투력\n(하나의 경이 방어구만 장착)", "MaxTotalPower": "최대 전투력", "MetersPerSecond": "m/s", "Milliseconds": "ms", "NoBonus": "보너스 없음", "NotApplicable": "해당 없음", "OfMaxRoll": "최대로 나올 수 있는 {{range}}", "PercentHelp": "클릭하셔서 품질에 대한 자세한 내용을 확인하세요.", "Percentage": "%", "PowerModifier": "시즌 경험치 획득으로 주어진 전투력", "Prestige": "명성 레벨: {{level}}\n5개의 빛의 티끌까지 {{exp}}xp", "Quality": "능력치 품질", "ShieldHP": "보호막 체력", "StrafingSpeed": "횡이동", "Strength": "힘", "TierProgress": "{{tier}}등급 {{statName}} ({{nextTier}}등급 까지 {{progress}}/60)", "TierProgress_Max": "{{tier}}등급 {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "체력 완전 회복 시간", "Total": "총", "TotalHP": "최대 체력", "WalkingSpeed": "걷기", "WeaponPart": "무기 부품" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM은 별도의 로그인 없이 태그, 로드아웃, 설정을 서버에 저장하고 다른 버전의 DIM과 데이터를 동기화할 수 있습니다. 이전에 DIM 동기화를 활성화하지 않았으면 설정에서 기존 데이터를 가져올 수 있습니다. OpenCollective 후원자의 지원으로 가능했습니다!", "No": "나중에", "Title": "DIM 동기화를 활성화합니까?", "Yes": "동기화 사용" }, "AutoBackup": "만일을 대비해서 데이터를 다운로드 폴더의 dim-data.json 파일에 백업했습니다.", "BackUpFirst": "만일을 대비해서 모든 데이터를 삭제하기 전에 백업하세요.", "BrowserMayClearData": "저장 공간이 부족하거나 DIM을 꾸준히 방문하시지 않으면 브라우저에서 이 정보를 지울 수도 있습니다", "DataIsLocal": "태그와 메모 데이터가 로컬에서만 사용 가능", "DeleteAllData": "DIM 동기화 서버의 모든 데이터 삭제", "DeleteAllDataConfirm": "정말로 DIM 동기화에서 모든 계정의 모든 데이터를 삭제하시겠습니까? 되돌릴 수 없습니다.", "Details": { "IndexedDBStorage": "로컬 저장소는 정보를 이 브라우저에만 저장할 것입니다. 브라우저 데이터를 삭제하면 이 정보도 같이 삭제됩니다." }, "DimApiFinePrint": "DIM이 태그, 로드아웃, 설정을 DIM 서버에 저장하고 다른 버전의 DIM과 동기화합니다.", "DimSyncDown": "서버와 통신하는 도중 오류가 발생하여 DIM 동기화가 연결되지 않았습니다.", "DimSyncEnabled": "DIM 동기화 활성화됨", "DimSyncNotEnabled": "DIM 동기화가 활성화되지 않았으며, 설정, 태그, 로드아웃, 검색 기록이 로컬에만 저장되고 브라우저 저장소를 비울 경우 삭제됩니다. DIM 동기화를 활성화하여 데이터를 자동으로 백업하거나, 정기적으로 데이터를 수동으로 백업하세요.", "EnableDimApi": "DIM 동기화 활성화 (권장)", "Export": "데이터 백업 다운로드", "ExportError": "DIM 동기화에서 백업된 파일을 다운로드하지 못하였습니다.", "ExportErrorBody": "DIM 동기화 서버가 비활성화되었거나, 연결에 문제가 있을 수 있습니다. 대신 로컬에 저장된 데이터를 다운로드합니다.", "Import": "데이터 백업 불러오기", "ImportConfirmDimApi": "정말로 현재 태그, 로드아웃, 설정을 이 버전으로 덮어쓰시겠습니까? 기존 데이터를 완전히 대체할 것입니다.", "ImportExport": "백업 & 복원", "ImportFailed": "불러오기 실패! {{error}}", "ImportNoFile": "선택된 파일이 없습니다!", "ImportNotification": { "FailedBody": "데이터를 가져올 수 없습니다. {{error}}", "FailedTitle": "가져오기 실패", "NoData": "백업에서 로드아웃이나 태그를 찾을 수 없음", "SuccessBodyForced": "백업에서 DIM 동기화로 설정, {{loadouts}}개의 로드아웃, {{tags}}개의 태그된 아이템을 가져와서 기존 데이터를 대체했습니다.", "SuccessBodyLocal": "설정, {{loadouts}}개의 로드아웃, {{tags}}개의 태그된 아이템을 백업에서 가져왔으며, 로컬 저장소에 있던 데이터를 대체했습니다. 로컬 저장소가 손실되지 않을 것을 보장할 수 없습니다 - DIM 동기화 사용을 고려하십시오.", "SuccessTitle": "가져오기 성공" }, "ImportTooManyFiles": "불러올 파일을 하나만 선택하세요.", "ImportWrongFileType": "이 파일은 JSON 파일이 아닙니다. DIM 백업 파일이 아닐 수 있습니다.", "IndexedDBStorage": "로컬 브라우저 저장소", "LearnMore": "DIM 동기화에 대해 더 알아보기", "MenuTitle": "동기화 & 백업", "ProfileErrorBody": "DIM 동기화와 통신하는 도중 문제가 발생하였습니다. 최신 설정, 태그, 로드아웃, 검색 기록이 표시되지 않을 수 있습니다. 데이터는 여전히 서버에 있으며, 로컬에서 수정된 내역은 서버에 다시 연결되었을 때 저장됩니다. DIM이 열려있는 동안 계속 시도하겠습니다.", "ProfileErrorTitle": "DIM 동기화 다운로드 오류", "RefreshDimSync": "DIM 동기화에서 데이터 다시 가져오기", "UpdateErrorBody": "DIM 동기화에 데이터를 저장하는 도중 문제가 발생하였습니다. DIM이 열려있는 동안 계속 시도하겠습니다.", "UpdateErrorTitle": "DIM 동기화 저장 오류", "UpdateInvalid": "DIM 동기화에 데이터 저장 실패", "UpdateInvalidBody": "DIM 동기화에 비정상적인 데이터가 전송되었으며 저장되지 않았습니다.", "UpdateInvalidBodyLoadout": "\"{{name}}\" 로드아웃에 문제가 있으며 저장되지 않습니다. 다른 사이트에서 가져왔다면, 해당 사이트에 잘못된 로드아웃이라고 알려주시기 바랍니다.", "UpdateQueueLength_other": "서버에 다시 연결되었을 때 {{count}}개의 새로운 변경 사항이 저장됩니다.", "Usage": "DIM은 이 기기에서 {{quota, humanBytes}} 중 {{usage, humanBytes}}를 사용하고 있습니다. 이는 Bungie.net에서 다운로드한 데스티니 아이템 데이터베이스를 포함하고 있습니다." }, "StreamDeck": { "Authorize": "앱 연결", "Enable": "스트림덱 플러그인", "Error": { "Body": "스트림덱 플러그인에 데이터를 전송하는 도중 오류가 발생하였습니다. 플러그인 개발자에게 문의하세요. {{error}}", "Title": "스트림덱 플러그인 오류" }, "FinePrint": "DIM 스트림덱 플러그인으로 연결을 활성화합니다. 이 플러그인은 DIM 팀에 의해 작성되거나 지원되지 않는 별개의 프로젝트입니다.", "Install": "플러그인 설치", "MissingAuthorization": "스트림덱 앱에서 인증해야 DIM에 연결할 수 있습니다. 설정으로 이동해서 \"앱 연결\"을 클릭하세요.", "Tooltip": { "Application": "스트림덱 앱", "AuthRequired": "이 버튼을 누르거나 설정으로 이동하여 \"앱 연결\"을 클릭하세요.", "Error": "스트림덱 플러그인이 더 이상 지원되지 않습니다. 최신 버전으로 업데이트하세요. 플러그인 최소 요구 사항:", "ErrorConnection": "이미 최신 버전을 사용 중이라면, 브라우저 확장 프로그램이 연결을 차단하고 있는지 확인하세요.", "ExtensionIssue": "확장 프로그램 문제", "Plugin": "플러그인", "Title": "DIM 스트림덱 플러그인", "Version": "버전:" } }, "StripSockets": { "Action": "소켓 해제", "ArmorMods": "{{count}}x 방어구 개조 부품", "Button": "{{numSockets}}개의 소켓 해제", "Cancel": "취소", "Choose": "해제할 소켓 선택", "DiscountedMods": "{{count}}x 할인된 개조 부품", "Done": "소켓 해제 완료", "NoSockets": "해제할 소켓 없음", "Ok": "확인", "Ornaments": "{{count}}x 장식", "Others": "{{count}}x 고스트 투영", "Running": "소켓 해제 중", "Shaders": "{{count}}x 안료", "Subclass": "{{count}}x 하위직업 옵션", "WeaponMods": "{{count}}x 무기 개조 부품" }, "Tags": { "Archive": "보관", "ClearTag": "태그 삭제", "Favorite": "즐겨찾기", "Infuse": "주입", "Junk": "쓰레기", "Keep": "유지", "LockAll": "아이템 잠금", "TagItem": "태그 추가", "UnlockAll": "아이템 잠금 해제" }, "Triage": { "AccountsForArtifice": "+3 능력치 개조 부품이 사용되었을 때 계략 방어구가 더 나은지 확인합니다.", "BetterArmor": "엄격하게 더 나은 방어구", "BetterArtificeArmor": "더 나은 계략 방어구", "BetterStatArmor": "더 나은 능력치의 방어구", "BetterStatArtificeArmor": "더 나은 능력치의 계략 방어구", "BetterWorseArmor": "더 나은/나쁜 방어구", "BetterWorseIncludes": "다음을 사용하여 방어구 분석:", "HighStats": "높은 능력치", "InLoadouts": "로드아웃", "OwnedCount": "# 소지 수", "PerkBetterArmorDesc": "본질 특성이나 특수한 개조 부품 슬롯이 동일하거나 더 많습니다.", "PerkWorseArmorDesc": "본질 특성이 동일하거나 없습니다.", "SimilarItems": "유사한 아이템", "StatBetterArmorDesc": "모든 능력치가 최소한 동일하며, 적어도 하나의 능력치가 더 좋습니다.", "StatNotPerkArmorDesc": "이 옵션은 능력치만 확인합니다. 더 나쁜 방어구에 특수한 개조 부품 슬롯이나 본질 특성이 있을 수 있습니다.", "StatWorseArmorDesc": "더 좋은 능력치가 없으며, 적어도 하나의 능력치가 더 낮습니다.", "ThisItem": "이 아이템", "WorseArmor": "엄격하게 더 나쁜 방어구", "WorseArtificeArmor": "더 나쁜 일반 방어구", "WorseStatArmor": "더 나쁜 능력치의 방어구", "WorseStatArtificeArmor": "더 나쁜 능력치의 일반 방어구", "YourBestItem": "가장 좋은 아이템" }, "Triumphs": { "GildingTriumph": "금박 업적", "HideCompleted": "완료한 업적 숨기기", "RevealRedacted": "삭제된 업적 표시", "SortRecords": "완료 여부에 따라 업적 정렬" }, "Vendors": { "Collections": "수집품", "Engram": "등급", "FilterToUnacquired": "얻지 못한 아이템만 표시", "HideSilverItems": "실버 아이템 숨기기", "NoItems": "이 상인은 현재 어떤 아이템도 판매하지 않습니다.", "RefreshTime": "소지품 새로고침:", "Vendors": "상인" }, "Views": { "About": { "APIHistory": "DIM과 다른 데스티니 앱에서 수행된 모든 작업의 기록 보기", "BungieCopyright": "모든 이미지와 컨텐츠는 Bungie의 자산입니다.", "CommunityInsight": "특성과 캐릭터 능력치에 대한 커뮤니티 인사이트는 {{clarityLink}}에서 제공됩니다. 부정확한 정보를 발견하거나 질문이 있는 경우 {{clarityDiscordLink}}에 방문하세요.", "Discord": "Discord", "DiscordHelp": "저희의 디스코드 채널에서 질문하고, 피드백을 주고, 도움을 받으세요.", "FAQ": "자주 묻는 질문", "FAQAccess": "DIM이 어떻게 제 데스티니 데이터에 접근하나요?", "FAQAccessAnswer": "우리는 Bungie의 앱 인증을 사용하여 DIM이 당신의 아이템을 보고 이동할 권한을 얻습니다. DIM은 사용자 이름이나 비밀번호를 알지 못합니다. 이 방법은 데스티니 컴패니언 앱의 작동 원리와 같습니다.", "FAQKeyboard": "DIM이 키보드 단축키를 지원하나요?", "FAQKeyboardAnswer": "네! \"?\" 키를 눌러 사용할 수 있는 단축키를 볼 수 있습니다.", "FAQLogout": "DIM에서 로그아웃하려면 어떻게 해야 하나요?", "FAQLogoutAnswer": "화면 좌측 상단의 아이콘을 눌러 메뉴를 열고 \"로그아웃\"을 누르세요.", "FAQLostItem": "이 앱을 사용하다가 아이템이 사라졌어요!", "FAQLostItemAnswer": "Bungie는 앱이 아이템을 삭제하는 것을 허용하지 않습니다 (Bungie의 앱에서도 불가능합니다!). 이동 실패보다 금고나 캐릭터에 남아있을 확률이 높습니다. 아이템을 검색하실 수도 있습니다. 여전히 보이지 않는다면 새로고침을 해보세요. {{link}} 혹은 게임 안에서 아이템이 있는지 확인하세요. 저희는 여전히 있을 거라고 확신합니다.", "FAQMobile": "DIM이 모바일을 지원하나요? 앱이 있나요?", "FAQMobileAnswer": "DIM 웹사이트는 핸드폰과 태블릿에서 불러올 수 있으며, 홈 화면에 추가하여 앱처럼 사용하실 수 있습니다.", "GitHub": "GitHub", "GitHubHelp": "이 프로젝트에 기여하는데 관심이 있으시다면, 저희 프로젝트 페이지인 {{link}}에 방문해보세요.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM은 무료이며 커뮤니티 개발자들이 Bungie.net, 데스티니 가디언즈 컴패니언 앱과 동일한 서비스를 사용하여 만든 오픈 소스 앱입니다.", "Schedule": { "beta": "베타 버전의 DIM은 코드를 변경할 때마다 업데이트됩니다. 최신 기능과 수정 사항은 물론 최신 버그도 얻을 수 있습니다!", "release": "이 버전의 DIM은 태평양 표준시 기준으로 매주 일요일 자정쯤에 업데이트됩니다." }, "Translation": "번역 팀에 참여하세요!", "TranslationText": "간편한 번역을 위해 {{link}}을 사용합니다. DIM의 번역을 개선하고 싶으시다면 팀에 참여하세요.", "Version": "버전 {{version}} {{flavor}}, {{date}}에 빌드됨", "Wiki": "DIM 사용 설명서", "WikiHelp": "DIM의 기능을 사용하는 방법을 배워보세요." }, "Login": { "Auth": "Bungie.net으로 인증", "EnableDimSyncWarning": "이전에 DIM 동기화를 비활성화하고 로컬 데이터 저장소만 사용하였습니다. DIM 동기화를 활성화하면 로컬 데이터를 DIM 동기화의 데이터로 덮어쓰게 됩니다. DIM 동기화를 활성화하기 전에 데이터를 백업해야 합니다. 설정에서 백업을 복원할 수 있습니다.", "Explanation": "DIM이 데스티니 캐릭터, 금고, 진척도를 확인하고 수정할 수 있도록 허용해주세요.", "LearnMore": "계정과 로그인에 대해 더 알아보기", "NewAccount": "다른 Bungie.net 계정으로 로그인", "Permission": "당신의 허락이 필요합니다..." }, "Support": { "BackersDetail": "일회성 혹은 월간 기부를 하여 저희가 적극적인 개발을 계속할 수 있도록 도와주세요.", "FreeToDownload": "DIM은 무료로 다운로드하고 사용할 수 있는 제품입니다. DIM의 소스 코드는 오픈소스로, 누구나 개선 가능합니다. DIM에서 광고를 보지 못할 것입니다. 이것이 우리의 약속입니다.", "OpenCollective": "{{link}} 서비스를 통해 이 프로젝트에 사용된 개발자의 헌신과 시간에 대한 보상을 제공하고 있습니다.", "Store": "우리의 로고와 다른 디자인의 상품을 {{link}}에서 판매하고 있습니다.", "Support": "DIM 지원" } }, "WishListRoll": { "BestRatedTip_other": "희망 아이템 목록에 있는 무기의 특성 조합과 일치합니다.", "Clear": "희망 아이템 목록 비우기", "CopiedLine": "희망 아이템 목록 특성 조합이 클립보드에 복사됨", "CopyLine": "선택한 특성을 희망 아이템 목록 특성 조합으로 복사", "DupeRolls": " (+{{num, number}}개의 중복 무시됨)", "ExternalSource": "다른 희망 아이템 목록 추가", "ExternalSourcePlaceholder": "희망 아이템 목록 URL 입력", "Header": "희망 아이템 목록", "Import": "희망 아이템 목록 불러오기", "ImportError": "\"{{url}}\"에서 희망 아이템 목록 불러오는 중 오류 발생: {{error}}", "ImportFailed": "희망 아이템 목록에 유효한 조합이 없습니다.", "ImportNoFile": "선택된 파일이 없습니다.", "InvalidExternalSource": "유효한 외부 희망 아이템 목록 URL을 입력하세요. URL은 다음 중 하나로 시작해야 합니다:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "마지막 업데이트: {{lastUpdatedDate}} {{lastUpdatedTime}}", "Num": "{{num, number}}개의 희망 아이템 목록", "NumRolls": "{{num, number}}개의 조합", "Refresh": "희망 아이템 목록 새로고침", "SourceAlreadyAdded": "희망 아이템 목록 이미 추가됨", "UpdateExternalSource": "희망 아이템 목록 추가", "Voltron": "voltron (기본값)", "WishListNotes": "희망 아이템 목록 메모:", "WorstRatedTip_other": "쓰레기 아이템 목록에 있는 무기의 특성 조합과 일치합니다." }, "no-space": "no-space", "wrong-level": "wrong-level" } ================================================ FILE: src/locale/locales.test.ts ================================================ import i18next from 'i18next'; import { setupi18n } from 'testing/test-utils'; import { getCopyWithCount } from 'testing/utils/i18next'; setupi18n(); const locales = [ 'en', 'de', 'es', 'es-mx', 'fr', 'it', 'ja', 'ko', 'pl', 'pt-br', 'ru', 'zh-chs', 'zh-cht', ]; const keysWithCounts = [ 'Armory.TrashlistedRolls', 'Armory.WishlistedRolls', 'Armory.WishlistedRolls', 'BulkNote.Title', 'Countdown.Days', 'Countdown.Days_compact', 'Csv.ImportSuccess', 'FarmingMode.D2Desc', 'FarmingMode.D2Desc_female', 'FarmingMode.D2Desc_male', 'FarmingMode.Desc', 'FarmingMode.Desc_female', 'FarmingMode.Desc_male', 'Filter.BulkClear', 'Filter.BulkRevert', 'Filter.BulkTag', 'Header.FilterMatchCount', 'Loadouts.ItemErrorSummary', 'Loadouts.MakeRoomDone_female', 'Loadouts.MakeRoomDone_male', 'Loadouts.MakeRoomDone', 'Loadouts.ModErrorSummary', 'Loadouts.ModPlacement.InvalidModsDesc', 'Loadouts.ModPlacement.UnassignedModsDesc', 'Loadouts.PullFromPostmasterNotification_female', 'Loadouts.PullFromPostmasterNotification_male', 'Loadouts.PullFromPostmasterNotification', 'Loadouts.Share.NumItems', 'Loadouts.Share.NumMods', 'Progress.PointsUsed', 'Progress.Resets', 'Storage.UpdateQueueLength', 'WishListRoll.BestRatedTip', 'WishListRoll.WorstRatedTip', ]; describe.each(locales)('locale %s', (locale) => { describe.each(keysWithCounts)('i18n key %s', (i18nKey) => { describe.each([0, 1, 4, 51])('with count %s', (count) => { const t = i18next.getFixedT(locale, 'translation'); test.each` copy | expected ${t(i18nKey, { count, defaultValue: i18nKey })} | ${getCopyWithCount(i18nKey, locale, count)} `('returns: $copy', ({ copy, expected }) => { expect(copy).toStrictEqual(expected); }); }); }); }); ================================================ FILE: src/locale/pl.json ================================================ { "AWA": { "ConfirmDescription": "Proszę użyć aplikacji Destiny 2 Companion aby zezwolić DIM na modyfikację Twoich przedmiotów.", "ConfirmTitle": "Potwierdź działanie", "Error": "Błąd podczas zmiany modyfikacji lub cech", "ErrorMessage": "Nie mogliśmy włożyć {{plug}} w {{item}}.\n\n{{error}}", "FailedToken": "Nie można uzyskać pozwolenia na zmianę elementu", "IrreversiblePlugging": "Nie posiadasz {{plug}}, więc nie nadpiszemy jej." }, "Accounts": { "Choose": "Profile dla {{bungieName}}", "ErrorLoadInventory": "Nie można załadować Twoich postaci Destiny {{version}} oraz ekwipunku", "ErrorLoadManifest": "Nie można załadować bazy danych informacji Destiny od Bungie", "ErrorLoading": "Nie można załadować kont Destiny z Bungie.net", "MissingAccountWarning": "Jeśli nie widzisz tutaj swojego konta, prawdopodobnie nie zalogowałeś się na odpowiednie konto Bungie.net lub Bungie.net może być wyłączony na czas prac technicznych.", "MissingDescription": "Konto, które próbujesz wyświetlić nie jest kontem połączonym z Twoim profilem Bungie.net. Wybierz jedno z poniższych kont.", "MissingTitle": "Nie znaleziono konta", "NoCharacters": "Nie posiadasz żadnych postaci z Destiny powiązanych z tym kontem Bungie.net. Spróbuj zalogować się na inne konto.", "NoCharactersTitle": "Nie znaleziono postaci", "SwitchAccounts": "Konta można przełączać później z menu w nagłówku.", "Title": "Konta" }, "Activities": { "Activities": "Aktywności", "Hard": "Trudne", "Nightfall": "Nocny Szturm", "Normal": "Normalne", "WeeklyHeroic": "Cotygodniowy Heroiczny Szturm" }, "Armory": { "AlternateItems": "Alternate Versions", "Armory": "Zbrojownia", "DifferentSeason": "Reissue from a different season", "NoNotes": "Brak notatek", "OpenInArmory": "zobacz w Zbrojowni", "Season": "Sezon {{season}}, rok {{year}}", "TrashlistedRolls_few": "{{count, number}} Rolli z listy śmieci", "TrashlistedRolls_many": "{{count, number}} Rolli z listy śmieci", "TrashlistedRolls_one": "Roll z listy śmieci", "TrashlistedRolls_other": "{{count, number}} Rolli z listy śmieci", "Unknown": "Nieznany przedmiot", "UnknownPerkHash": "The perk hash {{hash}} ({{perkName}}) does not appear on this item, so this wish list roll is invalid. Please contact the wish list author to correct this. Note that wish lists should always specify the non-enhanced version of perks.", "WishlistedRolls_few": "{{count, number}} Rolli z listy życzeń", "WishlistedRolls_many": "{{count, number}} Rolli z listy życzeń", "WishlistedRolls_one": "Roll z listy życzeń", "WishlistedRolls_other": "{{count, number}} Rolli z listy życzeń", "YourItems": "Twoje przedmioty" }, "Browsercheck": { "Samsung": "Aplikacja Samsung Internet może spowodować, że strony będą wyglądały zbyt ciemno, gdy włączony jest tryb ciemny. Wejdź w i włącz Ustawienia > Labs > Użyj ciemnego motywu witr. WWW lub przełącz się na inną przeglądarkę.", "Steam": "Przeglądarka nakładki Steam jest bardzo stara i część funkcji lub wszystkie funkcje DIM mogą nie działać. Nie możemy zapewnić wsparcia w tym zakresie.", "Unsupported": "Zespół DIM nie obsługuje tej przeglądarki. Niektóre lub wszystkie funkcje DIM mogą nie działać." }, "Bucket": { "Armor": "Zbroja", "Class": "Podklasa", "General": "Ogólne", "Ghost": "Duch", "Inventory": "Ekwipunek", "Postmaster": "Kurier", "Progress": "Postęp", "Reputation": "Reputacja", "Unknown": "Nieznany", "Vault": "Schowek", "Weapons": "Broń" }, "BulkNote": { "Append": "Dodaj do notatek / dodaj #hasztagi", "Confirm": "Zaktualizuj notatki", "Remove": "Usuń z notatek / usuń #hasztagi", "Replace": "Podmień notatki", "Title_few": "Zmieniono notatki dla {{count}} przedmiotów", "Title_many": "Zmieniono notatki dla {{count}} przedmiotów", "Title_one": "Zmieniono notatki dla jednego przedmiotu", "Title_other": "Zmieniono notatki dla {{count}} przedmiotów" }, "BungieAlert": { "Title": "Wiadomość od Bungie:" }, "BungieService": { "AppNotPermitted": "DIM nie ma uprawnień do wykonania tej czynności.", "DestinyCannotPerformActionAtThisLocation": "Nie możesz wyposażyć przedmiotów ani zmieniać modyfikacji podczas aktywności. Spróbuj wrócić na orbitę lub do przestrzeni społecznej. To jest ograniczenie API Bungie.net, a nie DIM.", "DestinyItemUnequippable": "Nie możesz wyposażyć tego przedmiotu. Jeśli ostatnia aktywność tej postaci zablokowała swoje wyposażenie, spróbuj zalogować się ponownie do postaci.", "DestinyLegacyPlatform": "Usługi Bungie posiadają błąd, który uniemożliwia DIM odczytywać informacji z twojego konta Destiny 2 jeśli grałeś w Destiny 1 na konsoli poprzedniej generacji. Bungie wkrótce to naprawi, ale do tego czasu musisz grać Destiny 1 na konsoli obecnej generacji aby mieć dostęp do swoich informacji.", "DevVersion": "Czy korzystasz z wersji developerskiej DIM? Musisz zarejestrować swoje rozszerzenie na Bungie.net.", "Difficulties": "Bungie.net obecnie ma problemy.", "ErrorTitle": "Błąd Bungie.net", "ItemUniquenessExplanation": "Postać może mieć tylko jeden z '{{name}}' na nim.", "Maintenance": "Serwery Bungie.net są wyłączone w celu przeprowadzenia konserwacji.", "MissingInventory": "Bungie.net nie zwrócił twojego ekwipunku, prawdopodobnie dlatego, że uniemożliwiają to ustawienia prywatności. Spróbuj się wylogować i zalogować.", "NetworkError": "Błąd sieci - {{status}}{{statusText}}", "NoAccount": "Nie znaleziono konta Destiny. Czy wybrałeś poprawną platformę?", "NoAccountForPlatform": "Nie znaleziono konta Destiny na platformie {{platform}}.", "NotConnected": "Możesz nie być podłączony do internetu.", "NotConnectedOrBlocked": "Możesz być niepodłączony do internetu, bądź bloker reklam lub rozszerzenie prywatności mogą blokować Bungie.net.", "NotLoggedIn": "Proszę autoryzuj DIM aby móc używać tej aplikacji.", "Slow": "Bungie.net jest teraz powolny", "SlowDetails": "Bungie.net zajmuje dużo czasu, aby zwrócić Twoje informacje. Może się to zdarzyć, gdy wielu graczy jest w grze jednocześnie, lub jeśli Bungie.net ma problemy. Możesz również mieć problem z połączeniem Internetowym. Będziemy czekać na odpowiedź.", "SlowResponse": "Bungie.net było za wolne aby odpowiedzieć.", "Throttled": "Bungie.net limituje ilość żądań, jakich DIM może wykonać.", "Twitter": "Uzyskaj aktualizacje statusu na:", "UnknownError": "Wiadomość Bungie.net: {{message}}", "VendorNotFound": "Dane sprzedawcy są niedostępne." }, "Compare": { "Archetype": "Archetyp", "AssumeMasterworked": "Przyjmij, że posiada Mistrzowskie Ulepszenie", "AssumeMasterworkedDescription": "Stats if fully Masterworked, without current Mods", "BaseStatsDescription": "Base stats, without Masterwork or Mods", "Button": "Porównaj", "ButtonHelp": "Porównaj Przedmioty", "CompareBaseStats": "Pokaż podstawowe statystyki", "CurrentStats": "Current Stats", "CurrentStatsDescription": "Current stats, including Mods and Masterwork level", "Error": { "Invalid": "Nie ma żadnych odpowiednich przedmiotów do porównania.", "Unmatched": "Ten przedmiot nie pasuje do typu porównywanych przedmiotów." }, "InitialItem": "To przedmiot dla którego uruchomiono narzędzie porównania", "IsVendorItem": "Ten przedmiot nie znajduje się w Twoim ekwipunku, jednak sprzedaje go {{vendorName}}.", "NoModArmor": "Pre-mods" }, "Cooldown": { "Grenade": "Odnowienie granatu: {{cooldown}}", "Melee": "Odnowienie ataku wręcz: {{cooldown}}", "Super": "Odnowienie supera: {{cooldown}}" }, "Countdown": { "Days_compact_few": "{{count}}d", "Days_compact_many": "{{count}}d", "Days_compact_one": "{{count}}d", "Days_compact_other": "{{count}}d", "Days_few": "{{count}} Dni", "Days_many": "{{count}} Dni", "Days_one": "1 Dzień", "Days_other": "{{count}} Dni" }, "Csv": { "EmptyFile": "W tym pliku nie było rzędów.", "ImportConfirm": "Czy na pewno chcesz zaimportować tagi/notatki z CSV? Nadpiszą one tagi/notatki wszystkich przedmiotów zawartych w twoim arkuszu.", "ImportFailed": "Nie udało się zaimportować tagów/notatek z CSV: {{error}}", "ImportSuccess_few": "Tagi/notatki wczytane dla {{count}} przedmiotów.", "ImportSuccess_many": "Tagi/notatki wczytane dla {{count}} przedmiotów.", "ImportSuccess_one": "Tagi/notatki wczytane dla jednego przedmiotu.", "ImportSuccess_other": "Tagi/notatki wczytane dla {{count}} przedmiotów.", "ImportWrongFileType": "Plik nie jest jest plikiem CSV.", "WrongFields": "CSV musi posiadać kolumny 'Id', 'Notes', 'Tag' i 'Hash'." }, "Dialog": { "Cancel": "Anuluj", "OK": "OK" }, "EnergyMeter": { "Energy": "Energia", "Unused": "Nieużywane", "UpgradeNeeded": "Obecna pojemność energii tego przedmiotu wynosi {{energyCapacity}}. Aby dopasować wybrane modyfikacje, jego pojemność energii musi wynosić {{energyUsed}}.", "Used": "Używane" }, "ErrorBoundary": { "Title": "Coś poszło nie tak" }, "ErrorPanel": { "BrowserTooOld": "Your browser is too old to use DIM. Please update your browser to the latest version.", "BrowserTooOldTitle": "Incompatible browser", "Description": "Spróbuj załadować swój ekwipunek w aplikacji Destiny 2 Companion, aby sprawdzić, czy Bungie.net jest wyłączony.", "ReadTheGuide": "Przeczytaj nasz podręcznik użytkownika (link w menu), aby uzyskać informacje na temat rozwiązywania problemów.", "SystemDown": "Ma to wpływ na wszystkie aplikacje Destiny, a zespół DIM nie może tego naprawić ani obejść.", "Troubleshooting": "Przewodnik rozwiązywania problemów" }, "FarmingMode": { "D2Desc_female_few": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest {{count}} wolnych miejsc na dany typ przedmiotu.", "D2Desc_female_many": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest {{count}} wolnych miejsc na dany typ przedmiotu.", "D2Desc_female_one": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest jedno miejsce wolne na dany typ przedmiotu.", "D2Desc_female_other": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest {{count}} wolnych miejsc na dany typ przedmiotu.", "D2Desc_few": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest {{count}} wolnych miejsc na dany typ przedmiotu.", "D2Desc_male_few": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest {{count}} wolnych miejsc na dany typ przedmiotu.", "D2Desc_male_many": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest {{count}} wolnych miejsc na dany typ przedmiotu.", "D2Desc_male_one": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest jedno miejsce wolne na dany typ przedmiotu.", "D2Desc_male_other": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest {{count}} wolnych miejsc na dany typ przedmiotu.", "D2Desc_many": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest {{count}} wolnych miejsc na dany typ przedmiotu.", "D2Desc_one": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest jedno miejsce wolne na dany typ przedmiotu.", "D2Desc_other": "DIM zapobiega przedmiotom dostawanie się do Kuriera poprzez upewnianie się, że na {{store}} zawsze jest {{count}} wolnych miejsc na dany typ przedmiotu.", "Desc_female_few": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia {{count}} miejsc wolnych na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_female_many": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia {{count}} miejsc wolnych na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_female_one": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia jedno miejsce wolne na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_female_other": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia {{count}} miejsc wolnych na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_few": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia {{count}} miejsc wolnych na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_male_few": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia {{count}} miejsc wolnych na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_male_many": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia {{count}} miejsc wolnych na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_male_one": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia jedno miejsce wolne na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_male_other": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia {{count}} miejsc wolnych na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_many": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia {{count}} miejsc wolnych na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_one": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia jedno miejsce wolne na dany typ przedmiotu aby nic nie poszło do Kuriera.", "Desc_other": "DIM przenosi Engramowe i Migotowe przedmioty z {{store}} do schowka i zostawia {{count}} miejsc wolnych na dany typ przedmiotu aby nic nie poszło do Kuriera.", "FarmingMode": "Tryb farmienia", "FarmingModeNote": "(zachowaj miejsce na zdobyte przedmioty)", "MakeRoom": { "Desc": "DIM przenosi tylko Engramowe i Migotowe przedmioty z {{store}} do schowka albo do innych postaci aby nic nie poszło do Kuriera.", "Desc_female": "DIM przenosi tylko Engramowe i Migotowe przedmioty z {{store}} do schowka albo do innych postaci aby nic nie poszło do Kuriera.", "Desc_male": "DIM przenosi tylko Engramowe i Migotowe przedmioty z {{store}} do schowka albo do innych postaci aby nic nie poszło do Kuriera.", "MakeRoom": "Zrób miejsce do podnoszenia przedmiotów przenosząc wyposażenie", "Tooltip": "Jeśli zaznaczone, DIM przeniesie broń i wyposażenie tak by zrobić miejsce w schowku na engramy." }, "OutOfRoom": "Brakuje Ci miejsca na przeniesienie przedmiotów z {{character}}. Czas usunąć śmieci!", "OutOfRoomTitle": "Brak wolnego miejsca", "Stop": "Zatrzymaj", "Vault": "Przeniesie przedmioty do schowka aby zrobić miejsce." }, "FashionDrawer": { "Accept": "Zapisz modę", "CannotFitOrnament": "Ten przedmiot nie posiada gniazda na zdobienia lub nie posiadasz żadnego zdobienia do tego przedmiotu.", "CannotFitShader": "Ten przedmiot nie może posiadać barw", "ClearOrnaments": "Wyczyść ozdoby", "ClearOrnamentsTitle": "Zresetuj wszystkie ozdoby do \"brak preferencji\"", "ClearShaders": "Wyczyść barwy", "ClearShadersTitle": "Zresetuj wszystkie barwy do \"brak preferencji\"", "NoPreference": "Brak preferencji - to gniazdo nie zostanie zmienione", "Reset": "Wyczyść modę", "Sync": "Zsynchronizuj", "SyncOrnaments": "Zsynchronizuj ozdoby", "SyncOrnamentsTitle": "Użyj ozdób z tego samego zestawu na wszystkich przedmiotach, jeśli są odblokowane", "SyncShaders": "Zsynchronizuj barwy", "SyncShadersTitle": "Użyj tej samej barwy na wszystkich przedmiotach", "Title": "Wybierz barwy i ozdoby", "UseEquipped": "Użyj wyposażonej mody" }, "FileUpload": { "Instructions": "Kliknij lub przeciągnij pliki" }, "Filter": { "Adept": "\\(adept\\)", "AmmoType": "Pokazuje przedmioty na podstawie rodzaju amunicji.", "Armor": "Pokazuje przedmioty, które są pancerzem.", "Armor3": "Shows items that use the Armor 3.0 stat system introduced in Edge of Fate.", "ArmorCategory": "Pokazuje zbroje na podstawie ich kategorii.", "ArmorIntrinsic": "Pokazuje legendarny pancerz, który ma Cechy Pancerza, takie jak Pancerz Pomysłowości.", "Artifice": "Shows Artifice armor.", "Ascended": "Pokazuje przedmioty, które posiadają węzeł wstępujący i zostały wstąpione.", "Breaker": "Filter by breaker type or corresponding champion type. breaker:instrinsic shows items with intrinsic breaker ability.", "BulkClear_few": "Usunięto tagi z {{count}} przedmiotów.", "BulkClear_many": "Usunięto tagi z {{count}} przedmiotów.", "BulkClear_one": "Usunięto tag z 1 przedmiotu.", "BulkClear_other": "Usunięto tagi z {{count}} przedmiotów.", "BulkRevert_few": "Przywrócono tagi na {{count}} przedmiotach.", "BulkRevert_many": "Przywrócono tagi na {{count}} przedmiotach.", "BulkRevert_one": "Przywrócono tag na 1 przedmiocie.", "BulkRevert_other": "Przywrócono tagi na {{count}} przedmiotach.", "BulkTag_few": "Otagowano {{count}} przedmiotów jako {{tag}}.", "BulkTag_many": "Otagowano {{count}} przedmiotów jako {{tag}}.", "BulkTag_one": "Otagowano wybrany przedmiot jako {{tag}}.", "BulkTag_other": "Otagowano {{count}} przedmiotów jako {{tag}}.", "Catalyst": "Pokazuje katalizatory na podstawie ich statusu. catalyst:complete pokazuje katalizatory, które ukończyłeś i nałożyłeś, catalyst:incomplete pokazuje odblokowane katalizatory, których cel nie został ukończony lub nie są nałożone, i catalyst:missing pokazuje przedmioty, które mogą posiadać katalizator, ale jeszcze nie został znaleziony.", "Class": "Pokazuje przedmioty na podstawie ich podobieństwu do klasy.", "Combine": "Filtry mogą być połączone lub pogrupowane z nawiasami, \"or\" i \"and\" w celu zawężenia wyszukiwania, na przykład \"{{example}}\".", "ContributePower": "Pokazuje przedmioty, które mają moc i mogą przyczynić się do twojego poziomu mocy.", "Cosmetic": "Pokazuje przedmioty, które są znaczkami lub kosmetykami.", "Craftable": "Pokazuje przedmioty, które można stworzyć.", "CraftedDupe": "Pokazuje zduplikowane bronie, gdzie co najmniej jeden duplikat jest zmodelowany.", "Curated": "Pokazuje przedmioty, które są kurowanym rollem.", "CurrentClass": "Pokazuje przedmioty, które można wyposażyć na aktualnie zalogowanym strażniku.", "CustomStatLower": "Pokazuje zbroję, której statystyki są niższe niż inna tego samego typu zbroi, tylko biorąc pod uwagę statystyki na liście niestandardowych sum statystyk dla tej klasy.", "DamageType": "Pokazuje przedmioty na podstawie rodzaju obrażeń.", "Deepsight": "Pokazuje bronie Rezonans Głębokiego Wglądu, które mogą mieć wydobyty wzorzec lub mogą mieć dodany Rezonans Głębokiego Wglądu używając\nHarmonizera Głębokiego Wglądu.", "Deprecated": "Ten filtr nie jest już wspierany.", "Description": "Opis", "DescriptionFilter": "Pokazuje przedmioty, których opis jest częściowo zgodny z treścią filtra. Możliwość wyszukiwania całych fraz przy użyciu cudzysłowów.", "DisabledModSlot": "Shows items with a disabled mod.", "Dupe": "Pokazuje zduplikowane przedmioty, włącznie z powtórzeniami", "DupeArchetype": "Groups armor with the same stat Archetype.", "DupeCount": "Przedmioty, które mają określoną liczbę duplikatów.", "DupeLower": "Zduplikowane przedmioty, włącznie z powtórzeniami, które nie są duplikatami najwyższego poziomu mocy. Wybierany jest tylko jeden duplikat jako najwyższy, a reszta jest uważana za niższe.", "DupePerks": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "DupeSetBonus": "Groups armor with the same set bonus.", "DupeStats": "Shows armor with identical base stats, and matching stat adjustment mods like Artifice or Tuners.", "DupeTertiary": "Groups armor with the same tertiary stat.", "DupeTraits": "Weapons whose traits are either a duplicate of, or a subset of, another weapon of the same type.", "DupeTunedStat": "Groups armor with the same Tuned stat.", "DupeUntunedStats": "Groups armor with identical base stats, ignoring stat adjustment mods.", "DupeZeroStats": "Groups armor with the same 3 non-zero base stats.", "Energy": "Shows items that use the Armor 2.0 mod system introduced in Shadowkeep.", "EnergyCapacity": "Shows items based on their current energy capacity.", "Engrams": "Pokazuje engramy.", "Enhanceable": "Shows weapons that can be enhanced.", "Enhanced": "Shows weapons based on their enhancement tier.", "EnhancedPerk": "Shows weapons that have the specified number of enhanced perk columns.", "EnhancementReady": "Shows weapons that have reached level thresholds for perk enhancement.", "Equipment": "Przedmioty, które można wyposażyć.", "Equipped": "Przedmioty aktualnie wyposażone na postaci.", "Event": "Pokazuje przedmioty na podstawie którego zdarzenia z Destiny 2 pochodzą.", "ExtraPerk": "Pokazuje losowo wylosowane legendarne bronie z dodatkowym, wybieralną cechą.", "Featured": "Items that count as one of the \"New Gear\" or \"Featured Items\" in the current season.", "Filter": "Filtr", "FilterWith": "Filter with:", "Focusable": "Shows items that can be focused at a vendor", "Foundry": "Pokazuje przedmioty na podstawie koncernów, które je utworzyły.", "Glimmer": "Pokazuje przedmioty konsumpcyjne, które są powiązane ze zyskiwaniem migotu.", "Harrowed": "\\(Spustoszenie\\)", "HasNotes": "Pokaż przedmioty z notatkami.", "HasOrnament": "Pokazuje przedmioty, które mają zastosowaną ozdobę.", "HasShader": "Pokazuje przedmioty z zastosowaną barwą.", "Holofoil": "Shows holofoil weapons.", "InDimLoadout": "is:indimloadout shows items that are included in any DIM loadout.", "InInGameLoadout": "is:iningameloadout shows items that are included in any in-game loadout.", "InInventory": "Shows items that you have at least one copy of in your inventory. Only really useful in the Vendors and Records screens.", "InLoadout": "is:inloadout shows items that are included in any loadout. Searching with inloadout: shows items that are included in loadouts with matching titles. When used with a hashtag, inloadout: shows items whose loadouts have the hashtag in the title or notes. When used with a range, it shows items that are in that many loadouts.", "Infusable": "Pokazuje przedmioty, które można nasycić.", "InfusionFodder": "Pokazuje przedmioty, które mogą być użyte do nasycenia przedmiotu tego samego typu o niższej mocy tylko za migot.", "IsAdept": "Shows weapons compatible with Adept mods.", "IsCrafted": "Pokazuje bronie, które zostały stworzone.", "ItemHash": "Pokazuje przedmioty z podanym hashem elementu ekwipunku. Dla zaawansowanych użytkowników.", "ItemId": "Pokazuje przedmiot z podanym ID elementu ekwipunku. Dla zaawansowanych użytkowników.", "Leveling": { "Complete": "{{term}} - pokazuje kompletne przedmioty - każde ulepszenie odblokowane.", "Incomplete": "{{term}} - pokazuje niekompletne przedmioty - wciąż można odblokować co najmniej jedno ulepszenie.", "NeedsXP": "{{term}} - pokazuje przedmioty, w które wciąż można włożyć doświadczenie.", "Upgraded": "{{term}} - pokazuje przedmioty, które mają wystarczającą ilość doświadczenia aby odblokować wszystkie ich węzły, lecz niektóre węzły nie zostały jeszcze odblokowane.", "XPComplete": "{{term}} - pokazuje przedmioty, w które już nie można włożyć doświadczenia (nawet jeśli ich ulepszenia zostały odblokowane albo nie)." }, "Location": "Pokazuje przedmioty na podstawie ich położeniu w aplikacji. left/middle/right (lewa/środkowa/prawa) char to wizualna lokacja postaci i chociaż gdy inleftchar zawsze będzie działać, to dwa inne są oparte na ilości postaci jakich masz. current to twoja ostatnio/aktualnie używana postać (która jest zaznaczona żółtym trójkątem).", "LockAllFailed": "Nie udało się zablokować przedmiotów", "LockAllSuccess": "Zablokowano {{num}} przedmiotów", "Locked": "Pokazuje przedmioty na podstawie ich zablokowania.", "Masterwork": "Pokazuje przedmioty na podstawie ich mistrzowskich statystyk lub poziomu mistrzostwa.", "MasterworkKills": "Pokazuje przedmioty na podstawie ich mistrzowskiej ilości zabójstw.", "MaxPower": "Pokazuje przedmioty o najwyższej mocy na dany slot.", "MaxPowerLoadout": "Pokazuje przedmioty w uzbrojeniu, które by zmaksymalizowały Twój Poziom Mocy dla każdej klasy postaci.", "Memento": "Shows weapons that have a memento socket.", "ModSlot": "Shows armor with a specific mod type slot.", "Mods": { "Y3": "Shows items with any mods applied." }, "Name": "Shows items whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.", "NamedStat": "Pokazuje pancerz zawierający punkty w podanej statystyce.", "Negate": "Aby negować wyszukiwanie, poprzedź wyszukiwane hasło znakiem minusa lub słowem \"not\", na przykład \"{{notexample}}\" lub \"{{notexample2}}\".", "NewItems": "Pokazuje nowe przedmioty.", "Notes": "Szukaj przedmioty z nałożonymi przez ciebie notatkami.", "OriginTrait": "Shows weapons that have an origin trait perk.", "Ornament": "Pokazuje przedmioty z ozdobami i filtruje po ich status.", "PartialMatch": "Pokazuje elementy, w których ich nazwa, opis, dowolny atut lub dowolne modyfikacje są częściowo dopasowane do tekstu filtra. Wyszukaj całe frazy za pomocą cytatów.", "PatternUnlocked": "Pokazuje przedmioty, które mają odblokowane wzorce, nawet jeśli nie są stworzone.", "Perk": "Pokazuje przedmioty, w których jedna z ich cech lub modyfikacji jest częściowo dopasowana do treści filtra w ich nazwie lub opisie. Możliwość wyszukiwania całych fraz przy użyciu cudzysłowów.", "PerkName": "Shows items with a perk or mod whose name matches (exactperk:) or partially matches (perkname:) the filter text. Search for entire phrases using quotes.", "PinnacleReward": "Pokazuje zadania, które dają szczytowe nagrody.", "Postmaster": "Przedmioty znajdujące się u Kuriera.", "PowerKeywords": "Użyj słowa kluczowego pinnaclecap lub softcap, zamiast liczby, aby odnieść się do limitów mocy bieżącego sezonu.", "PowerLevel": "Pokazuje przedmioty na podstawie ich poziomu mocy. $t(Filter.PowerKeywords)", "PowerfulReward": "Pokazuje zadania, które dają potężne nagrody.", "PrismaticDamageType": "Shows items based on if they are a light or darkness damage type. Light types are arc, solar, and void. Darkness types are stasis and strand.", "Quality": "Pokazuje przedmioty na podstawie ich zsumowanego odsetku jakości statystyki. '{{percentage}}' to alias do '{{quality}}'.", "RandomRoll": "Pokazuje przedmioty, które wypadają z losowymi rollami.", "RarityTier": "Pokazuje przedmioty na podstawie ich poziomie rzadkości.", "Reforgeable": "Pokazuje przedmioty, które można przekuć u Rusznikarza.", "Release": "Shows items available from a specific release or event.", "RequiredLevel": "Pokazuje przedmioty na podstawie wymaganego przez nich poziomu.", "RetiredPerk": "Pokazuje broń z cechami, które nie są już dostępne.", "SearchPrompt": "Wyszukaj dostępne komendy filtrujące", "Season": "Pokazuje przedmioty na podstawie którego sezonu z Destiny 2 pochodzą.", "StackFull": "Pokaż, które przedmioty są wypełnione dla ich stosu (rdzenie wzmacniające, dziwne monety, materiały rusznikarza itp.)", "StackLevel": "Pokazuje przedmioty na podstawie ilości przedmiotów w ich stosie.", "Stackable": "Pokazuje przedmioty, które można stertować (syntezatory amunicji, dziwne monety, itd)", "StatLower": "Pokazuje zbroję, której statystyki są niższe niż inna tego samego typu zbroi.", "Stats": "Pokazuje przedmioty na podstawie określonej wartości statystyki. $t(Filter.StatsExtras)", "StatsBase": "Filtruje pancerz w oparciu o jego podstawową wartość statyczną, nie licząc dołączonych modyfikacji lub mistrzostwa $t(Filter.StatsExtras)", "StatsExtras": "Supports stat addition by connecting multiple stat names with the + or & symbol. There are also special keywords highest, secondhighest, thirdhighest, etc. which match stats based on their rank within an item's stats. Each custom stats also has its own search term, shown in the Custom Stats settings.", "StatsLoadout": "Znajduje zestaw przedmiotów do wyposażenia, aby maksymalizować całkowitą wartość określonej statystyki.", "StatsMax": "Znajduje pancerz o najwyższym numerze dla określonej statystyki. Obejmuje wszystkie przedmioty z najwyższą statystyką.", "StatsOrdinal": "Finds armor 3.0 with the specified stat focusing.", "Tags": { "Tag": "Pokazuje przedmioty, które mają konkretny tag.", "Tagged": "Pokazuje przedmioty, które mają dowolny tag." }, "Tier": "Shows items based on their tier from 0-5.", "Timelost": "\\(zagubion(y|a) w czasie\\)", "Tracked": "Pokazuje zadania/zlecenia na podstawie ich śledzonego stanu.", "Transferable": "Przedmioty, które można przenosić między postaciami.", "Trashlist": "Pokazuje przedmioty pasujące do listy śmieci z twojej listy życzeń.", "TunedStat": "Shows items with tuning mods for the specified stat.", "Unascended": "Pokazuje przedmioty, które posiadają węzeł wstępujący, lecz nie zostały wstąpione.", "Undo": "Cofnij", "UnlockAllFailed": "Nie udało się odblokować przedmiotów", "UnlockAllSuccess": "Odblokowano {{num}} przedmiotów", "Vendor": "Przedmiot jest dostępny od określonego sprzedawcy.", "VendorItem": "Item is from a vendor, not in your inventory. Useful for excluding vendor items from Loadout Optimizer.", "Weapon": "Pokazuje przedmioty, które są bronią.", "WeaponLevel": "Pokazuje bronie na podstawie ich Poziomu Broni.", "WeaponType": "Pokazuje bronie na podstawie rodzaju broni.", "Wishlist": "Pokazuje przedmioty pasujące do twojej listy życzeń.", "WishlistDupe": "Pokazuje zduplikowane przedmioty, gdzie co najmniej jeden duplikat jest na twojej liście życzeń.", "WishlistEnabled": "Shows items that are eligible to have wish list rolls.", "WishlistNotes": "Pokazuje przedmioty listy życzeń, których notatki pasują do wyszukiwania.", "WishlistUnknown": "Pokazuje przedmioty bez rekomendacji rolli w załadowanej liście życzeń.", "Year": "Pokazuje przedmioty na podstawie którego roku z Destiny 2 pochodzą." }, "General": { "ClickForDetails": "Kliknij po szczegóły", "Close": "Zamknij", "Confirm": "Potwierdzić?", "UserGuideLink": "Podręcznik użytkownika" }, "Glyphs": { "Axe": "Axe", "DarkAbility": "Zdolność Ciemności", "Gilded": "Pozłocone", "Harmonic": "Harmonic", "HiveSword": "Hive Sword", "LightAbility": "Zdolność Światła", "LightLevel": "Light Level", "Misadventure": "Nieszczęście", "Missing": "Brakujące", "OpenSymbolsPicker": "Otwórz Wybór Symboli", "Prismatic": "Pryzmatyczny", "Quickfall": "Pikowanie", "RespawnRestricted": "Respawn Restricted", "ScorchCannon": "Działo Spopielające", "SearchSymbols": "Szukaj symboli...", "Smoke": "Dym" }, "Header": { "About": "O DIM", "AutoRefresh": "DIM automatycznie przeładuje się, dopóki nadal grasz.", "BulkTag": "Masowo otaguj przedmioty", "BungieNetAlert": "Alarm od Bungie", "Clear": "Wyczyść filtry szukania", "CompareMatching": "Porównaj Przedmioty", "DeleteSearch": "Usuń Wyszukiwanie", "FilterHelp": "Szukaj przedmiotu/cechy, {{example}} i więcej", "FilterHelpBrief": "Szukaj przedmioty", "FilterHelpLoadouts": "Szukaj nazw i notatek na temat uzbrojenia", "FilterHelpMenuItem": "Pomoc z filtrami...", "FilterHelpOptimizer": "Filter armor included in builds, e.g.: {{example}}", "FilterHelpProgress": "Szukaj kamieni milowych i zleceń", "FilterHelpRecords": "Szukaj triumfy i kolekcje", "FilterMatchCount_few": "{{count}} przedmiotów", "FilterMatchCount_many": "{{count}} przedmiotów", "FilterMatchCount_one": "1 przedmiot", "FilterMatchCount_other": "{{count}} przedmiotów", "Filters": "Filtry", "InstallDIM": "Zainstaluj jako aplikację", "InstallDIMBanner": "Zainstaluj DIM jako aplikację na ekranie głównym", "Inventory": "Ekwipunek", "IosPwaPrompt": "W Safari, kliknij ikonę udostępniania (środkowy przycisk u dołu) i wybierz \"Dodaj do Ekranu Domowego\".", "KeyboardShortcuts": "Skróty Klawiszowe", "LaunchDIMAlone": "Oddzielne okno", "MaterialCounts": "Liczba materiałów", "Menu": "Menu", "ProfileAge": "Serwery Destiny ostatni raz wysłały zaktualizowane dane {{age}} temu.\nOdświeżanie z DIM ma szansę uzyskać nowsze dane, ale może również powtarzać informacje z pamięci podręcznej.", "Refresh": "Odśwież Dane Destiny [R]", "ReloadApp": "Przeładuj aplikację", "ReportBug": "Zgłoś błąd", "SaveSearch": "Zapisz wyszukiwanie", "SearchActions": "Open Search Actions", "SearchResults": "Pokaż Przedmioty", "Shop": "Sklep", "TagAs": "Otaguj jako '{{tag}}'", "UpgradeDIM": "Aktualizuj DIM", "WhatsNew": "Co nowego" }, "Help": { "CannotMove": "Nie można zdjąć tego przedmiotu z tej postaci.", "NoStorage": "DIM nie może przechować danych", "NoStorageMessage": "DIM can't store data in your browser. This can be caused by browsing in private or incognito mode, or when you have low disk space, or a browser bug. Try restarting your computer! You won't be able to log in to or use DIM until you fix this." }, "Hotkey": { "Armory": "Show Armory for an item", "CheatSheetTitle": "Skróty klawiszowe:", "ClearDialog": "Zamknij dialog", "ClearNewItems": "Wyczyść nowe przedmioty", "Enter": "ENTER", "ItemPopupTab": "Switch item details tab", "LockUnlock": "Zablokuj lub odblokuj przedmiot", "MarkItemAs": "Oznacz przedmiot jako '{{tag}}'", "Menu": "Przełącz menu", "Note": "Wpisz notatki", "Pull": "Odbierz przedmiot do aktywnej postaci", "RefreshInventory": "Odśwież ekwipunek", "ShowHotkeys": "Pokaż skróty klawiszowe", "StartSearch": "Rozpocznij wyszukiwanie", "StartSearchClear": "Rozpocznij nowe wyszukiwanie", "Tab": "TAB", "Vault": "Wyślij przedmiot do schowka" }, "InGameLoadout": { "ClearSlot": "Clear Slot {{index}}", "Create": "Utwórz Uzbrojenie", "CreateTitle": "Create In-Game Loadout From Current Equipment", "CurrentlyEquipped": "Aktualnie wyposażone", "DeleteFailed": "Failed to delete loadout", "Deleted": "Loadout Deleted", "DeletedBody": "Cleared the in-game loadout at slot {{index}}", "EditFailed": "Failed to update loadout", "EditIdentifiers": "Edit Identifiers", "EditTitle": "Edit Loadout Name and Icon", "EquipNotReady": "In-game Equip Not Ready", "EquipReady": "In-game Equip Ready", "LoadoutDetails": "Loadout Details", "MatchingLoadouts": "Matching Loadouts:", "PrepareEquip": "Prepare Equip", "Replace": "Replace Loadout {{index}}", "Save": "Update Loadout", "SaveIdentifiers": "Update Identifiers", "SnapshotFailed": "Failed to snapshot equipped loadout" }, "Infusion": { "Filter": "Filtruj przedmioty", "InfuseSource": "Wybierz przedmiot do nasycenia używając {{name}}", "InfuseTarget": "Wybierz przedmiot do nasycenia {{name}}", "InfusionMaterials": "Materiały do Nasycenia", "NoItems": "Brak przedmiotów do użycia w nasyceniu.", "NoTransfer": "Przenoszenie materiału do nasycenia\n {{target}} nie może być przeniesiony.", "SwitchDirection": "Przełącz", "TransferItems": "Przenieś" }, "Inventory": { "ClickToExpand": "(Kliknij, aby rozwinąć)", "MissingSilver": "Your Silver balance is only available while you are playing the game." }, "Item": { "SetBonus": { "NPiece_few": "{{count}} Piece", "NPiece_many": "{{count}} Piece", "NPiece_one": "{{count}} Piece", "NPiece_other": "{{count}} Piece" }, "ThumbsDown": "Thumbs Down", "ThumbsUp": "Kciuk w górę" }, "ItemFeed": { "ClearFeed": "Wyczyść Kanał", "Description": "Kanał produktu", "HideTagged": "Ukryj oznaczone", "NoNewItems": "Brak nowych przedmiotów", "ShowOlderItems": "Pokaż starsze przedmioty" }, "ItemMove": { "Consolidate": "Skonsolidowany {{name}}", "Distributed": "Rozprowadzanie {{name}}\n {{name}} został równo rozprowadzony pomiędzy postaciami.", "MovingItem": "Przenieś do Schowka", "MovingItem_female": "Przenieś do {{target}}", "MovingItem_male": "Przenieś do {{target}}", "ToStore": "Wszystkie {{name}} są teraz na twoim {{store}}.", "ToVault": "Wszystkie {{name}} są teraz w twoim schowku." }, "ItemPicker": { "ChooseItem": "Wybierz przedmiot:", "SearchPlaceholder": "Szukaj przedmioty" }, "ItemService": { "BucketFull": { "Guardian": "Jest za dużo '{{itemtype}}' przedmiotów na twojej {{store}}.", "Guardian_female": "Jest za dużo '{{itemtype}}' przedmiotów na twojej {{store}}.", "Guardian_male": "Jest za dużo '{{itemtype}}' przedmiotów na twojej {{store}}.", "Vault": "Jest za dużo '{{itemtype}}' przedmiotów w {{store}}." }, "Classified": "Ten przedmiot jest sklasyfikowany i nie może być przeniesiony w tym momencie.", "Classified2": "Sklasyfikowany przedmiot. Bungie jeszcze nie dostarcza informacji o tym przedmiocie. Dodaj notatki do tego przedmiotu i użyj filtru wyszukiwania \"notes:\" aby go odnaleźć.", "Deequip": "Nie można odnaleźć innego przedmiotu do wyposażenia, aby zdjąć {{itemname}}", "ExoticError": "'{{itemname}}' nie może być wyposażony, ponieważ egzotyczny przedmiot w gnieździe {{slot}} nie może być zdjęty. ({{error}})", "NotEnoughRoom": "Nie ma niczego do wyniesienia z {{store}} aby zrobić miejsce dla {{itemname}}", "NotEnoughRoomGeneral": "Nie ma wystarczająco dużo miejsca, aby przenieść ten przedmiot.", "OnlyEquippedClassLevel": "To może być wyposażone tylko przez {{class}} na poziomie {{level}} lub wyższym.", "OnlyEquippedLevel": "To może być wyposażone tylko przez postacie na poziomie {{level}} lub wyższym.", "PostmasterAlmostFull": "Almost full!", "PostmasterFull": "Full!", "PreviewVendor": "Podgląd zawartości {{type}}", "StackFull": "Masz już pełny stos {{name}}", "StoreName": "{{genderRace}} {{className}}" }, "KillType": { "ClassAbilities": "Zdolność klasowa", "Finisher": "Cios kończący", "Grenade": "Granat", "Melee": "Atak wręcz", "Precision": "Precyzyjne", "Super": "Superzdolność" }, "LB": { "AddStack": "Dodaj kolejną kopię tej modyfikacji", "AdvancedOptions": "Ustawienia Zaawansowane", "ChooseAMod": "Wybierz swoje modyfikacje", "ChooseASetBonus": "Choose your set bonuses", "ChooseAnExotic": "Wybierz swój egzotyk", "ClearLocked": "Wyczyść Zablokowane", "ContainsVendorItems": "Te uzbrojenie zawiera przedmioty sprzedawcy", "Current": "Aktualny", "Equip": "Wyposaż na {{character}}", "Exclude": "Wykluczone przedmioty", "ExcludeHelp": "Shift + kliknij na przedmiot (lub przeciągnij i upuść do tego wiaderka) aby tworzyć zestawy bez konkretnego sprzętu.", "ExistingBuildStats": "Existing Build Stats", "ExistingBuildStatsNote": "Only showing builds with strictly higher stats.", "FilterSets": "Zestawy filtrów", "Help": { "And": "Pancerz z każdą z tych cech będzie użyty (\"and\")", "ChangeNodes": "Zmień węzły Intelektu, Dyscypliny lub Siły w grze na to, co jest pokazane aby utworzyć każde uzbrojenie.", "Discipline": "Dyscyplina przyspiesza czas odnowienia Granatów", "DragAndDrop": "Przeciągnij i upuść przedmioty do zablokowanych wiaderek aby tworzyć zestawy tylko z tym sprzętem", "Help": "Potrzebujesz pomocy?", "HigherTiers": "Wyższe Poziomy są lepsze", "Intellect": "Intelekt przyspiesza czas odnowienia Superzdolności", "Lock": "Zablokuj zestaw cech, klikając na wiaderko zablokowania i wybierając cechy", "MultiPerk": "Aby użyć pancerza z wieloma cechami razem shift + kliknij pożądane cechy", "NoPerk": "Jeśli cecha nie pojawi się, oznacza to, że nie posiadasz pancerza z tą cechą", "Or": "Pancerz z dowolną z tych cech będzie użyty (\"or\")", "ShiftClick": "Shift kliknij na przedmiot aby tworzyć zestawy bez tego sprzętu", "StatsIncrease": "W miarę wzrostu poziomu obrony przedmiotu, statystki na tym przedmiocie (int/dis/str) także rosną.", "Strength": "Siła przyspiesza czas odnowienia Ataków Wręcz", "Synergy": "Spróbuj znaleźć pancerz posiadający cechy zwiększające ilość amunicji do broni, które używasz.", "Tier11Example": "4/5/2 (budowa Poziomu 11) ma 4 Intelektu, 5 Dyscypliny, 2 Siły (4+5+2 = Poziom 11)" }, "HideAllConfigs": "Ukryj wszystkie konfiguracje", "HideConfigs": "Ukryj konfiguracje", "IncompatibleWithOptimizer": "Ten przedmiot nie jest kompatybilny z Optymalizatorem. Proszę nabyć nową wersję z Kolekcji.", "LB": "Optymalizator Uzbrojenia", "LightMode": { "HelpCurrent": "Kalkuluje uzbrojenia na obecnych poziomach obrony.", "HelpScaled": "Kalkuluje uzbrojenia jak gdyby wszystkie przedmioty miały 350 obrony.", "LightMode": "Tryb jasny" }, "Loading": "Wczytywanie najlepszych zestawów", "LockEquipped": "Zablokuj Wyposażone", "LockPerk": "Zablokuj cechę", "Locked": "Zablokowane Przedmioty", "LockedHelp": "Przeciągnij i upuść dowolny przedmiot do jego wiaderka aby tworzyć zestaw z tym konkretnym sprzętem. Shift + kliknij aby wykluczyć przedmioty.", "Missing2": "Brakuje rzadkich, legendarnych lub egzotycznych części aby stworzyć pełny zestaw!", "ProcessingMode": { "Fast": "Szybki", "Full": "Pełny", "HelpFast": "Tylko bierze pod uwagę twój najlepszy sprzęt.", "HelpFull": "Bierze pod uwagę więcej sprzętu, ale trwa dłużej.", "ProcessingMode": "Tryb przetwarzania" }, "RemoveStack": "Remove a copy of this mod", "Scaled": "Skalowany", "SearchAMod": "Search for mod name or description", "SearchASetBonus": "", "SearchAnExotic": "Search for exotic name or description", "SelectExotic": "Wybierz egzotyk", "SelectMods": "Wybierz modyfikacje", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Activity Mods", "SelectSetBonus": "Select Set Bonuses", "SelectSubclassOptions": "Dostostuj podklasę", "ShowAllConfigs": "Pokaż wszystkie konfiguracje", "ShowConfigs": "Pokaż konfiguracje", "ShowGear": "Pancerz {{class}}", "Vendor": "Uwzględnij przedmioty Sprzedawcy" }, "Loading": { "Accounts": "Wczytywanie kont Destiny...", "Code": "Wczytywanie kodu DIM...", "FilterHelp": "Wczytywanie pomocy wyszukiwania...", "Profile": "Wczytywanie profilu Destiny...", "Vendors": "Wczytywanie sprzedawców Destiny..." }, "LoadoutAnalysis": { "Analyzed": "Analyzed {{numLoadouts}} Loadouts", "Analyzing": "Analyzing {{numAnalyzed}}/{{numLoadouts}} Loadouts", "BetterStatsAvailable": { "Description": "Choosing different armor or mods for this loadout will allow reaching higher stats. Choose \"$t(Loadouts.OpenInOptimizer)\" to view better builds.", "Name": "Dostępne lepsze statystyki" }, "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat to exceed 200. DIM may identify better stats by reducing the amount of excess stats. If this is undesired, disable \"$t(Loadouts.IncludeRuntimeStatBenefits)\" in the Loadout.", "DoesNotRespectExotic": { "Description": "This loadout's Loadout Optimizer settings specify an exotic choice, but the loadout does not match that exotic.", "Name": "Wrong Exotic" }, "DoesNotSatisfyStatConstraints": { "Description": "Loadout Optimizer settings for this Loadout specify stat minimums, but the Loadout does not reach them.", "Name": "Wrong Stat Minimums" }, "EmptyFragmentSlots": { "Description": "There are empty fragment slots in this subclass.", "Name": "Empty Fragment Slots" }, "InvalidMods": { "Description": "Some mods in this loadout are deprecated or do not otherwise fit into any of your armor pieces.", "Name": "Wycofane Modyfikacje" }, "InvalidSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer that is not valid.", "Name": "Invalid Search Query" }, "ItemsDoNotMatchSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer, and that search query excludes at least one of the items in the loadout.", "Name": "Search Excludes Items" }, "MissingItems": { "Description": "Niektóre elementy w tym uzbrojeniu już nie są w twoim ekwipunku.", "Name": "Brakujące przedmioty" }, "ModsDontFit": { "Description": "Armor in this loadout cannot accommodate all loadout mods, even if the armor was upgraded.", "Name": "Nieprzypisane modyfikacje" }, "NeedsArmorUpgrades": { "Description": "Armor in this loadout needs to be upgraded to accommodate all mods or reach specified stats.", "Name": "Needs Armor Upgrades" }, "NotAFullArmorSet": { "Description": "This loadout could not be analyzed further because it does not include a full set of armor.", "Name": "Not A Full Armor Set" }, "TooManyFragments": { "Description": "There are more fragments configured on the subclass than granted by aspects.", "Name": "Za dużo fragmentów" }, "UsesSeasonalMods": { "Description": "This loadout relies on mods that are only available in some seasons. When the season ends, some mods will be unavailable or exceed armor energy capacity.", "Name": "Używa modyfikacji Sezonowych" } }, "LoadoutBuilder": { "All": "Wszystkie", "AlwaysAutoMods": "Artifice and Tuning mods will always be chosen automatically.", "AnyExotic": "Dowolny Egzotyk", "AnyExoticDescription": "Zestawy muszą zawierać egzotyk, ale każdy egzotyk będzie działać.", "Artifice": "Pomysłowość", "AssumeMasterwork": "Załóżmy, że Mistrzowskie", "AssumeMasterworkOptions": { "All": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nArmor 2.0 Exotics: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Enhanced to accept Artifice stat mods.", "Current": "Current stats, assumed energy level at least {{minLoItemEnergy}}.", "Legendary": "Legendary: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExotic: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Full masterwork stat bonuses, assumed energy level at least 10.", "None": "All armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "Automatycznie dodaj modyfikacje statystyk", "AutomaticallyPicked": "This mod was added automatically to improve build stats.", "CompareLoadout": "Porównaj uzbrojenie", "ConfirmOverwrite": "Czy na pewno chcesz zastąpić pancerz w uzbrojeniu \"{{name}}\" tym nowym zestawem pancerza?", "DecreaseStatPriority": "Decrease stat priority", "DisabledByAutoStatMods": "Stat mods are being chosen automatically by Loadout Optimizer.", "DisabledDueToMaintenance": "Optymalizator uzbrojenia jest obecnie wyłączony ze względu na prace konserwacyjne Bungie API.", "EquipItems": "Wyposaż", "ExcludeItem": "Wyklucz Przedmiot", "ExcludeVendors": "Search \"not:vendor\" to exclude vendor items from Loadout Optimizer.", "ExcludedItems": "Wykluczone przedmioty", "ExistingLoadout": "Istniejące uzbrojenie", "Exotic": "Egzotyczny pancerz", "ExoticClassItemPerks": "If you want specific perks, use searches like exactperk:\"spirit of verity\". Click perks in the Optimizer results to add or remove them from the item filter.", "ExoticSpecialCategory": "Specjalne", "FOTLWildcardWarning": "This set contains a Festival of the Lost mask. Manually apply the correct mod to activate desired set bonuses.", "Filter": "Ustawienia", "IgnoreStat": "If unchecked, Loadout Optimizer will pretend this stat doesn't exist when building sets", "IncreaseStatPriority": "Increase stat priority", "Legendary": "Legendarny", "LimitToNewFeaturedGear": "Limit to new/featured gear", "LockItem": "Przypnij przedmiot", "MissingClass": "Zestaw jest dla: {{className}}", "MissingClassDescription": "Zestaw który chcesz zobaczyć dotyczy klasy której nie posiadasz.", "MwExotic": "Exotic", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "Aktywne zapytanie ogranicza elementy, które DIM może umieścić w zestawach", "AllowAutoStatMods": "Zezwalaj DIM na automatyczne dołączanie dodatkowych Modyfikacji statystyk", "AlwaysInvalidMods": "Te Modyfikacje nie pasują do żadnego z twoich przedmiotów:", "AssumeMasterworked": "Zezwól DIM na rekomendowanie ulepszenie pancerza do poziomu mistrzowskiego", "AssumptionsRestricted": "DIM nie może zalecać zmian energii pancerza:", "BadSlot": "W gnieździe {{bucketName}} żaden z dozwolonych przedmiotów nie może pomieścić tych Modyfikacji:", "ExoticDoesNotExist": "You don't have any of the selected exotic armor in your inventory.", "Header": "Nie znaleziono zestawów. To są możliwe powody, dla których DIM nie mógł znaleźć żadnych zestawów:", "LowerBoundsFailed": "Many sets did not meet minimum stat requirements", "MaybeAllowMoreItems": "Rozważ dopuszczenie innych przedmiotów:", "MaybeDecreaseLowerBounds": "Consider reducing minimum stat requirements", "MaybeRemoveMods": "Rozważ usunięcie niektórych Modyfikacji:", "MaybeRemoveSearchQuery": "Rozważ wyczyszczenie lub zmianę filtra w pasku wyszukiwania", "ModAssignmentFailed": "Wiele zestawów nie mogło zmieścić wszystkich żądanych Modyfikacji", "RemoveMods": "Usuń te Modyfikacje", "RemoveSetBonuses": "Consider removing some set bonuses", "SetBonuses": "You have chosen some set bonuses, maybe you don't have the right items to use them." }, "NoExotic": "Brak egzotyku", "NoExoticDescription": "Equivalent to searching \"not:exotic\" in the search bar - sets will not use any exotic armor.", "NoExoticPreference": "No Exotic Selected", "NoExoticPreferenceDescription": "Exotic armor will be used if it maximizes stats.", "NoLoadoutsToCompare": "Brak uzbrojeń do porównania", "None": "Brak", "OptimizerExplanationGuide": "Przeczytaj podręcznik użytkownika, aby uzyskać więcej informacji i poradnik wideo.", "OptimizerExplanationMods": "Choose an exotic, mods, and a subclass. These will contribute stats to the build, while any mods already on the armor are ignored.", "OptimizerExplanationSearch": "Use the search bar to narrow down which armor to consider, e.g. {{example}}. If no armor in a slot matches the search, all items will be considered for that slot.", "OptimizerExplanationStats": "Drag the most important stats to the top, and uncheck stats you don't want to maximize.", "OptimizerSet": "Zestaw optymalizatora", "PinnedItems": "Pinned Items", "PinnedItemsFinePrint": "Search filters are saved with Loadout Optimizer settings, but pinned and excluded items are not. Pins and exclusions will be ignored when DIM checks existing Loadouts for better stat builds.", "ProcessingSets": "Finding highest stat sets...", "SaveAs": "Zapisz jako", "SetBonus": "Set Bonuses", "SpeedReport": "Evaluated {{combos, number}} combinations in {{time}} seconds using {{cpus}} CPU cores.", "StatConstraints": "Stat Priorities & Ranges", "StatMax": "Maks", "StatMin": "Min", "StatRangeTooltip": "With the current min/max setting, loadouts exist which have {{min}} to {{max}} points in this stat. Double-click to set min to {{max}}.", "StatTotal": "Total: {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "Nie można dodać wszystkich modyfikacji.", "UnableToAddAllModsBody": "Nie ma wystarczającej ilości miejsc na modyfikacje, aby założyć {{mods}}.", "UnlockItem": "Odepnij Przedmiot" }, "LoadoutFilter": { "Contains": "Shows loadouts which have an item or a mod matching the filter text. Search for items with spaces in their name using quotes.", "FashionOnly": "Shows loadouts that contain only fashion (shaders or ornaments).", "LoadoutLight": "Shows loadouts based on their calculated light level. Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.", "ModsOnly": "Shows loadouts that only contain armor mods.", "Name": "Shows loadouts whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.", "Notes": "Search for loadouts by their notes field.", "PartialMatch": "Shows loadouts where their name or notes has a partial match to the filter text. Search for entire phrases using quotes.", "Season": "Shows loadouts by which season of Destiny 2 they were last modified in.", "Subclass": "Shows loadouts whose subclass name or damage type partially matches the filter text." }, "Loadouts": { "Abilities": "Umiejętności", "Actions": "Actions for {{title}}", "AddEquippedItems": "Dodaj Wyposażone", "AddNotes": "Dodaj notatki", "AddUnequippedItems": "Dodaj Niewyposażone", "Any": "Dowolna klasa", "Apply": "Zastosuj", "ApplyInGameLoadoutInGame": "Your loadout is ready to equip but since you're in an activity you need to equip it in-game.", "ApplyMods": "Zastosowywanie modyfikacji", "ApplySearch": "Przenieś wyszukanie \"{{query}}\"", "ArmorStats": "Statystyki Pancerza", "ArtifactUnlocks": "Artifact Unlocks", "ArtifactUnlocksDesc": "Due to Bungie.net limitations, DIM cannot automatically configure your artifact. You need to perform these unlocks in-game before applying the Loadout.", "ArtifactUnlocksWithSeason": "Artifact Unlocks – S{{seasonNumber}}", "BadLoadoutShare": "Nie można załadować udostępnionego Uzbrojenia", "BadLoadoutShareBody": "Uzbrojenie, które próbujesz załadować, jest nieprawidłowe: {{error}}", "Before": "Przed '{{name}}'", "CancelEditing": "Cancel Editing", "CannotCustomizeSubclass": "Ta podklasa nie może zostać skonfigurowana", "ChooseItem": "Dodaj {{name}}", "ClassType": "Any class loadout", "ClassTypeMismatch": "Przedmiot {{className}} nie może być dodany do tego Uzbrojenia", "ClassTypeMissing": "Nie posiadasz {{className}}, aby utworzyć Uzbrojenie dla", "ClassType_female": "{{className}} loadout", "ClassType_male": "{{className}} loadout", "Classified": "Niektóre z twoich przedmiotów są sklasyfikowane i nie mogą być uwzględnione przy kalkulacji maksymalnej mocy.", "ClearLoadoutParameters": "Usuń ustawienia Optymalizatora Uzbrojenia", "ClearSection": "Usuń wszystko", "ClearSpace": "Move others away", "ClearSpaceArmor": "Move other armor away", "ClearSpaceWeapons": "Move other weapons away", "ClearUnsetMods": "Usuń inne Modyfikacje", "ClearingSpace": "Przenoszenie innych przedmiotów", "CopyAndEdit": "Edit Copy", "Create": "Utwórz Uzbrojenie", "CurrentlyEquipped": "Aktualnie wyposażone", "Deequip": "Rozbrojenie postaci z przedmiotów", "Delete": "Usuń", "DimLoadouts": "DIM Loadouts", "Edit": "Edytuj Uzbrojenie", "EditBrief": "Edytuj", "EquipInGameLoadout": "Equipping in-game loadout", "EquipItems": "Wyposażanie przedmiotów", "EquippableDifferent1": "Wielokrotne przedmioty egzotyczne zostały użyte do obliczenia twojej maksymalnej mocy, więc pokazana liczba może nie być osiągalna podczas wyposażenia twoich przedmiotów w grze.", "EquippableDifferent2": "Maksymalna moc nie jest ograniczona przez regułę \"Jeden egzotyk\" podczas określania Mocy twoich upuszczonych przedmiotów, potężnych, oraz szczytowych nagród.", "Failed": "Wyposażenie nie zostało zastosowane w całości", "Fashion": "Wybierz modę", "FashionOnly": "Fashion-only", "FillFromEquipped": "Uzupełnij używając wyposażonych przedmiotów", "FillFromInventory": "Wypełnij przy użyciu niewyposażonych przedmiotów", "FilteredItems": "Filtrowane Przedmioty", "FindAnother": "Znajdź następny {{name}}", "FromEquipped": "Wyposażone", "Generated": "{{statTotal}} Stat Point Loadout", "HashtagTip": "Wskazówka: Używaj #hashtagów w swoich nazwach Uzbrojenia lub notatkach, a one pojawią się tutaj.", "Import": { "BadURL": "Nieprawidłowy adres URL udostępnienia Uzbrojenia.", "Error": "Błąd pobierania Uzbrojenia:", "Error404": "To Uzbrojenie nie istnieje.", "PasteHere": "Wklej link Uzbrojenia, aby otworzyć Uzbrojenie." }, "ImportLoadout": "Importuj Uzbrojenie", "InGameActions": "In-Game Loadout Actions", "InGameLoadouts": "In-Game Loadouts", "IncludeRuntimeStatBenefits": "Include Font mod stats", "IncludeRuntimeStatBenefitsDesc": "\"Font of ...\" armor mods provide a flat boost to character stats while you have Armor Charges.\n\nWith this setting, DIM considers these mods active and adds their benefits to this Loadout's stats in calculations and optimizations.", "ItemErrorSummary_few": "{{count}} błędów przedmiotu:", "ItemErrorSummary_many": "{{count}} błędów przedmiotu:", "ItemErrorSummary_one": "1 błąd przedmiotu:", "ItemErrorSummary_other": "{{count}} błędów przedmiotu:", "ItemLeveling": "Levelowanie Przedmiotów", "LoadoutName": "Nazwa uzbrojenia", "LoadoutParameters": "Ustawienia Optymalizatora Uzbrojenia", "LoadoutParametersExotic": "Loadout must include this exotic: {{exoticName}}", "LoadoutParametersQuery": "Items must match this search filter", "LoadoutParametersStats": "Stat priorities and minimum/maximum stat ranges", "Loadouts": "Uzbrojenia", "MakeRoom": "Zrób miejsce dla Kuriera", "MakeRoomDone_female_few": "Ukończono robienie miejsca dla {{count}} przedmiotów od Kuriera przenosząc {{movedNum}} przedmiotów z {{store}}.", "MakeRoomDone_female_many": "Ukończono robienie miejsca dla {{count}} przedmiotów od Kuriera przenosząc {{movedNum}} przedmiotów z {{store}}.", "MakeRoomDone_female_one": "Ukończono robienie miejsca dla 1 przedmiotu od Kuriera przenosząc 1 przedmiot z {{store}}.", "MakeRoomDone_female_other": "Ukończono robienie miejsca dla {{count}} przedmiotów od Kuriera przenosząc {{movedNum}} przedmiotów z {{store}}.", "MakeRoomDone_few": "Ukończono robienie miejsca dla {{count}} przedmiotów od Kuriera przenosząc {{movedNum}} przedmiotów z {{store}}.", "MakeRoomDone_male_few": "Ukończono robienie miejsca dla {{count}} przedmiotów od Kuriera przenosząc {{movedNum}} przedmiotów z {{store}}.", "MakeRoomDone_male_many": "Ukończono robienie miejsca dla {{count}} przedmiotów od Kuriera przenosząc {{movedNum}} przedmiotów z {{store}}.", "MakeRoomDone_male_one": "Ukończono robienie miejsca dla 1 przedmiotu od Kuriera przenosząc 1 przedmiot z {{store}}.", "MakeRoomDone_male_other": "Ukończono robienie miejsca dla {{count}} przedmiotów od Kuriera przenosząc {{movedNum}} przedmiotów z {{store}}.", "MakeRoomDone_many": "Ukończono robienie miejsca dla {{count}} przedmiotów od Kuriera przenosząc {{movedNum}} przedmiotów z {{store}}.", "MakeRoomDone_one": "Ukończono robienie miejsca dla 1 przedmiotu od Kuriera przenosząc 1 przedmiot z {{store}}.", "MakeRoomDone_other": "Ukończono robienie miejsca dla {{count}} przedmiotów od Kuriera przenosząc {{movedNum}} przedmiotów z {{store}}.", "MakeRoomError": "Nie można zrobić miejsca dla każdego przedmiotu od Kuriera: {{error}}.", "ManageLoadouts": "Zarządzaj uzbrojeniami", "MaxSlots": "Możesz mieć tylko {{slots}} {{bucketName}} w Uzbrojeniu.", "MaximizeLight": "Maksymalizuj Światło", "MaximizePower": "Maksymalizuj Moc", "MaximizeStat": "Maksymalizuj statysktykę", "MissingItemsWarning": "Niektóre elementy w tym uzbrojeniu już nie są w twoim ekwipunku.", "ModErrorSummary_few": "{{count}} błędów modyfikacji:", "ModErrorSummary_many": "{{count}} błędów modyfikacji:", "ModErrorSummary_one": "1 błąd modyfikacji:", "ModErrorSummary_other": "{{count}} błędów modyfikacji:", "ModPlacement": { "InvalidMods": "Invalid Mods", "InvalidModsDesc_few": "{{count}} mods cannot fit into any armor piece.", "InvalidModsDesc_many": "{{count}} mods cannot fit into any armor piece.", "InvalidModsDesc_one": "1 mod cannot fit into any armor piece.", "InvalidModsDesc_other": "{{count}} mods cannot fit into any armor piece.", "ModPlacement": "Umieszczenie Modyfikacji", "StackableMod": "Stackable", "UnassignedMods": "Nieprzypisane modyfikacje", "UnassignedModsDesc_few": "{{count}} mods did not fit due to insufficient energy capacity or mod slots. Energy upgrades to the selected armor will not fix the issue.", "UnassignedModsDesc_many": "{{count}} mods did not fit due to insufficient energy capacity or mod slots. Energy upgrades to the selected armor will not fix the issue.", "UnassignedModsDesc_one": "1 mod did not fit due to insufficient energy capacity or mod slots. Energy upgrades to the selected armor will not fix the issue.", "UnassignedModsDesc_other": "{{count}} mods did not fit due to insufficient energy capacity or mod slots. Energy upgrades to the selected armor will not fix the issue.", "UnstackableMod": "Not Stackable", "UpgradeCosts": "Upgrade Costs", "UpgradeCostsDesc": "Some armor needs energy capacity upgrades to fit the requested mods. In total, these upgrades cost:" }, "Mods": "Modyfikacje", "ModsOnly": "Mods-only", "MoveItems": "Przenoszenie przedmiotów", "NoSpace": "Nie masz miejsca w schowku ani na każdej innej postaci.", "NoneMatch": "Żadne z Twoich Uzbrojeń nie pasowało do filtrów.", "NotStarted": "Oczekiwanie na zakończenie innych działań lub odświeżenie ekwipunku, aby zakończyć ładowanie", "NotesPlaceholder": "Napisz kilka notatek na temat tego Uzbrojenia albo użyj #hashtagów, aby go skategoryzować", "NotificationTitle": "Uzbrojenie: {{name}}", "OnWrongCharacterAdvice": "Kliknij tutaj, aby znaleźć przedmioty o najwyższej Mocy dla tej postaci.", "OnWrongCharacterWarning": "Najpotężniejszy pancerz tej postaci znajduje się na innej postaci. Aby liczyć się do Mocy dropów, potężnych i szczytowych nagród, pancerz musi znajdować się na tej postaci lub w Schowku.", "OnlyItems": "Tylko przedmioty, które można założyć, materiały i przedmioty konsumpcyjne można dodać do uzbrojenia.", "OpenInOptimizer": "Optymalizuj zbroję", "OpenOnStreamDeck": "Open on Stream Deck", "PickArmor": "Pick Armor", "PickMods": "Dodaj modyfikacje pancerza", "Prismatic": { "Aspect": "Prismatic Aspect", "Grenade": "Prismatic Grenade", "Melee": "Prismatic Melee", "Super": "Superzdolność" }, "PullFromPostmaster": "Odbierz Kuriera", "PullFromPostmasterError": "Nie można odebrać Kuriera: {{error}}.", "PullFromPostmasterGeneralError": "Nie można zebrać wszystkich przedmiotów od Kuriera.", "PullFromPostmasterNotification_female_few": "Zbieranie {{count}} przedmiotów od Kuriera do {{store}}.", "PullFromPostmasterNotification_female_many": "Zbieranie {{count}} przedmiotów od Kuriera do {{store}}.", "PullFromPostmasterNotification_female_one": "Zbieranie 1 przedmiotu od Kuriera do {{store}}.", "PullFromPostmasterNotification_female_other": "Zbieranie {{count}} przedmiotów od Kuriera do {{store}}.", "PullFromPostmasterNotification_few": "Zbieranie {{count}} przedmiotów od Kuriera do {{store}}.", "PullFromPostmasterNotification_male_few": "Zbieranie {{count}} przedmiotów od Kuriera do {{store}}.", "PullFromPostmasterNotification_male_many": "Zbieranie {{count}} przedmiotów od Kuriera do {{store}}.", "PullFromPostmasterNotification_male_one": "Zbieranie 1 przedmiotu od Kuriera do {{store}}.", "PullFromPostmasterNotification_male_other": "Zbieranie {{count}} przedmiotów od Kuriera do {{store}}.", "PullFromPostmasterNotification_many": "Zbieranie {{count}} przedmiotów od Kuriera do {{store}}.", "PullFromPostmasterNotification_one": "Zbieranie 1 przedmiotu od Kuriera do {{store}}.", "PullFromPostmasterNotification_other": "Zbieranie {{count}} przedmiotów od Kuriera do {{store}}.", "PullFromPostmasterPopupTitle": "Odbierz od Kuriera", "Random": "Losowy", "Randomize": "Wylosuj uzbrojenie", "RandomizeButton": "Losuj", "RandomizeNew": "Create Random", "RandomizeQueryHint": "Tip: Search for items first to restrict what items can be randomly chosen.", "RandomizeSearch": "Losowo z Wyszukiwania", "RandomizeSearchPrompt": "Wylosuj wyposażone przedmioty z wyszukania \"{{query}}\"?", "Redo": "Przywróć", "RestoreAllItems": "Wszystkie Przedmioty", "SalvationsEdgeMods": "Salvation's Edge Mods", "Save": "Zapisz", "SaveAsDIM": "Save as DIM Loadout", "SaveAsNew": "Zapisz jako nowe", "SaveAsNewTooltip": "Zachowaj oryginalne Uzbrojenie i zapisz to jako nowe Uzbrojenie", "SaveDisabled": { "AlreadyExists": "Wybierz nową nazwę dla Uzbrojenia.", "Empty": "Uzbrojenie jest puste.", "NoName": "Uzbrojenie wymaga nazwy." }, "SaveLoadout": "Zapisz Uzbrojenie", "Season": "Sezon {{season}}", "SetBonusesDesc": "Required set bonuses", "Share": { "Copied": "Skopiowano link uzbrojenia do schowka", "CopyButton": "Kopiuj link", "Error": "Błąd przy pobieraniu linku udostępniania", "Fashion": "Moda (barwy & ozdoby)", "LoadoutOptimizer": "Ustawienia Optymalizatora Uzbrojenia", "NativeShare": "Udostępnij link", "Notes": "Notatki", "NumItems_few": "{{count}} przedmioty(-ów) - odbiorcy zostaną poproszeni o wybranie podobnych przedmiotów z ich ekwipunku", "NumItems_many": "{{count}} przedmioty(-ów) - odbiorcy zostaną poproszeni o wybranie podobnych przedmiotów z ich ekwipunku", "NumItems_one": "{{count}} przedmiotu - odbiorcy zostaną poproszeni o wybranie podobnego przedmiotu z ich ekwipunku", "NumItems_other": "{{count}} przedmioty(-ów) - odbiorcy zostaną poproszeni o wybranie podobnych przedmiotów z ich ekwipunku", "NumMods_few": "{{count}} Modyfikacje(-ji)", "NumMods_many": "{{count}} Modyfikacje(-ji)", "NumMods_one": "{{count}} Modyfikacja", "NumMods_other": "{{count}} Modyfikacje(-ji)", "Placeholder": "Wczytywanie linku udostępniania", "Subclass": "Dostosowanie podklasy", "Summary": "Udostępnij to Uzbrojenie zawierające:", "Title": "Udostępnij \"{{name}}\"" }, "ShareLoadout": "Udostępnij", "ShowModPlacement": "Pokaż Umieszczenie Modyfikacji", "Snapshot": "Save As In-Game Loadout", "SocketOverrides": "Zmiana opcji podklasy", "SortByEditTime": "Sortuj według ostatniej modyfikacji", "SortByName": "Sortuj według nazwy", "SubclassOptions": "Opcje {{subclass}}", "SubclassOptionsSearch": "Szukaj opcje {{subclass}}", "Succeeded": "Uzbrojenie powiodło się", "SyncFromEquipped": "Synchronizuj z wyposażonych przedmiotów", "TooManyRequested": "Masz {{total}} {{itemname}}, ale Twoje wyposażenie wymaga {{requested}}. Przenieśliśmy wszystko, co miałeś.", "TuningMods": "Tuning Mods", "UnassignedModError": "Modyfikacja nie pasuję do twojej bieżącej zbroi", "Undo": "Cofnij", "Update": "Zapisz zmiany", "UpdateLoadout": "Update Loadout", "VendorsCannotEquip": "Nie masz tych przedmiotów. Naciśnij, aby wybrać zamiennik lub kliknij X, aby usunąć:" }, "Manifest": { "Download": "Pobieranie najnowszej bazy danych informacji Destiny od Bungie...", "Error": "Błąd wczytywania bazy danych informacji Destiny:\n{{error}}\nOdśwież żeby spróbować ponownie.", "Load": "Wczytywanie bazy danych informacji Destiny..." }, "Milestone": { "Daily": "Codzienne wyzwanie", "OneTime": "Wyzwanie jednorazowe", "SeasonalRank": "Sezonowa Ranga {{rank}}", "Special": "Wyzwanie Wydarzenia Specjalnego", "Tutorial": "Wyzwanie Samouczka", "Unknown": "Wyzwanie", "Weekly": "Cotygodniowe wyzwanie" }, "Mods": { "HarmonicModDescription": "This mod's effect comes at a reduced cost and changes element depending on the equipped subclass." }, "MoveAmount": { "Amount": "Ilość:" }, "MovePopup": { "Acquired": "Ten przedmiot jest odblokowany w Kolekcji.", "AcquiredMod": "Ta modyfikacja jest odblokowana w kolekcjach.", "AddNote": "Dodaj notatki", "AddToLoadout": "Uzbrojenie", "AddToLoadoutTitle": "Add this to a loadout", "All": "Wszystkie", "ArtifactBreaker": "This weapon has {{breaker}} because of an unlocked artifact perk.", "CannotCurrentlyRoll": "Ta cecha nie może być otrzymana na bieżącej wersji tego przedmiotu.", "CantPullFromPostmaster": "You must visit the postmaster in game to retrieve this item.", "CatalystProgress": "Postęp Katalizatora", "CommunityData": "Spostrzeżenia społeczności", "Consolidate": "Połącz", "DistributeEvenly": "Rozdziel po równo", "EnhancementTier": "Poziom {{tier}}", "Equip": "Wyposaż na:", "EquipWithName": "Wyposaż na {{character}}", "FavoriteUnFavorite": { "Favorite": "Ulubiony {{itemType}}", "Favorited": "Ulubione", "Unfavorite": "Usuń {{itemType}} z ulubionych", "Unfavorited": "Nieulubione" }, "Infuse": "Nasyć", "InfuseTitle": "Open the infusion fuel finder", "IntrinsicBreaker": "This weapon intrinsically has {{breaker}}.", "LoadingSockets": "Perk and stat details have not loaded yet for this item.", "LockUnlock": { "AutoLock": "Stan blokady jest zsynchronizowany z tagiem tego przedmiotu", "Lock": "Zablokuj {{itemType}}", "Locked": "Zablokowany", "Unlock": "Odblokuj {{itemType}}", "Unlocked": "Odblokowane" }, "MissingSockets": "Szczegóły cech i modyfikacji są niedostępne, gdy Bungie aktualizuje swoje usługi. Funkcja ta powróci po zakończeniu pracy, zwykle zajmuje to kilka godzin.", "Notes": "Notatki:", "OpenOnStreamDeck": "Open on Stream Deck", "OverviewTab": "Przegląd", "Owned": "Ten przedmiot jest w twoim ekwipunku.", "OwnedMod": "Ten mod znajduje się w ekwipunku modyfikacji.", "PullItem": "Wyciągnij z {{bucket}} do {{store}}", "PullPostmaster": "Odbierz od Kuriera", "ReadLore": "Czytaj lore na Ishtar Collective", "ReadLoreLink": "Czytaj fabułę", "Rewards": "Nagrody:", "SendToVault": "Wyślij do schowka", "Store": "Pull to:", "StoreWithName": "Pull to {{character}}", "Subtitle": { "QuestProgress": "Krok {{questStepNum}} z {{questStepsTotal}}", "Type": "{{classType}} {{typeName}}" }, "TabList": "Item detail tabs", "ToggleSidecar": "Rozwiń lub zwiń akcje przedmiotu", "TrackUntrack": { "Track": "Śledź {{itemType}}", "Tracked": "Śledzone", "Untrack": "Nie śledź {{itemType}}", "Untracked": "Nieśledzone" }, "TriageTab": "Triaż", "UnreliablePerkOption": "Ta cecha pojawia się tylko w widoku kolekcji. Może nie być wylosowana na tym przedmiocie.", "Vault": "Schowek", "WeaponLevel": "Poziom Broni {{level}}" }, "Notes": { "Error": "Błąd! Max 120 znaków na notatki.", "Help": "Add notes, #hashtags, and :symbols:" }, "Notification": { "Cancel": "Anuluj", "OK": "Odrzuć" }, "Objectives": { "Complete": "Ukończone", "Incomplete": "Niekompletne" }, "Organizer": { "BulkMove": "Przenieś do", "BulkMoveLoadoutName": "Wybrane w organizatorze", "BulkTag": "Tag", "Columns": { "Ammo": "Ammo", "Archetype": "Archetyp", "BaseStats": "Podstawowe statystyki", "Breaker": "Przełamanie", "Crafted": "Data utworzenia", "CustomTotal": "Niestandardowa suma", "Damage": "Obrażenia", "Energy": "Energia", "Event": "Zdarzenie", "Featured": "New Gear", "Foundry": "Foundry", "Frame": "Frame", "Harmonizable": "Harmonizable", "Holofoil": "Holofoil", "Icon": "Ikona", "ItemTier": "Tier", "KillTracker": "Kills", "Level": "Poziom", "Loadouts": "Uzbrojenia", "Location": "Położenie", "Locked": "Zablokowany", "MasterworkStat": "MW Stat", "MasterworkTier": "MW Tier", "ModSlot": "Gniazdo modyfikacji", "Mods": "Modyfikacje", "Name": "Nazwa", "New": "Nowy", "Notes": "Notatki", "OriginTraits": "Origin Trait", "OtherPerks": "Weapon Components", "PercentComplete": "% Ukończone", "Perks": "Cechy", "PerksGrid": "Perks Grid", "Power": "Moc", "Quality": "Jakość %", "Recency": "Nowość", "Season": "Sezon", "Shaders": "Cosmetics", "Source": "Źródło", "StatQuality": "Jakość statystyki", "StatQualityStat": "{{stat}}%", "Stats": "Statystyki", "Tag": "Tag", "TertiaryStat": "3rd Stat", "Tier": "Rarity", "Traits": "Cechy Broni", "TuningStat": "Tuner", "WishList": "Lista Życzeń", "WishListNotes": "Notatki listy życzeń", "Year": "Rok" }, "EnabledColumns": "Włączone kolumny", "Lock": "Zablokuj", "NoItems": "No items match the filters. If you have a search query, try clearing it.", "NoMobile": "Obróć telefon na bok, aby użyć organizatora.", "Note": "Ustaw Notatki", "OpenIn": "Pokaż w organizatorze", "Organizer": "Organizator", "SelectAll": "Zaznacz wszystko", "SelectItem": "Wybierz lub odznacz {{name}}", "ShiftTip": "Wskazówka: Przytrzymaj klawisz Shift i kliknij komórkę, aby filtrować przedmioty", "Stats": { "Aim": "Cel", "Airborne": "Lotna skuteczność", "AmmoGeneration": "Ammo Gen", "Power": "Moc", "RPM": "RPM", "Recoil": "Odrzut", "Reload": "Przeładowanie" }, "Unlock": "Odblokuj" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "Kurier jest prawie pełny! ({{number}}/{{postmasterSize}})", "PostmasterFull": "Kurier jest pełny! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "Zlecenia", "CatalystSource": "Source: {{source}}", "CrucibleRank": "Rangi", "Items": "Przedmioty zadań", "Milestones": "Kamienie milowe i wyzwania", "NoEventChallenges": "Ukończyłeś wszystkie wyzwania wydarzenia", "NoTrackedTriumph": "You have no tracked triumphs. Track as many as you like in DIM.", "PaleHeartPathfinder": "Pale Heart Pathfinder", "PercentMax": "{{pct}}% to maximum", "PercentPrestige": "{{pct}}% do resetu", "PointsUsed_few": "{{count}} points used", "PointsUsed_many": "{{count}} points used", "PointsUsed_one": "1 point used", "PointsUsed_other": "{{count}} points used", "PowerBonusHeader": "Nagrody o Mocy +{{powerBonus}}", "PowerBonusHeaderUndefined": "Inne nagrody", "Progress": "Postęp", "QueryFilteredTrackedTriumphs": "Żaden z Twoich śledzonych triumfów nie pasował do wyszukania", "QuestExpired": "Wygasło", "QuestExpires": "Wygasa za ", "Quests": "Zadania", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}pkt", "Resets_few": "{{count}} resety(/ów)", "Resets_many": "{{count}} resety(/ów)", "Resets_one": "1 reset", "Resets_other": "{{count}} resety(/ów)", "RewardPassEndsIn": "Reward Pass ends in ", "RewardPassPrestigeRank": "Prestige Rank {{rank}}", "SeasonalHub": "Seasonal Hub", "StatTrackers": "Śledzenie statystyk", "TrackedTriumphs": "Śledzone triumfy" }, "RecordBooks": { "HideCompleted": "Ukryj ukończone zapisy", "RecordBooks": "Księga Rekordów" }, "Records": { "Title": "Rekordy", "UniversalOrnamentSetOther": "Inne" }, "SearchHistory": { "Date": "Ostatnio używane", "DeleteAll": "Usuń wszystkie wyszukiwania nieoznaczone gwiazdką", "Description": "To są Twoje przeszłe i zapisane wyszukiwania. Możesz je stąd usunąć.", "Item": "Item Searches", "Link": "Zobacz i edytuj historię wyszukiwania", "Loadout": "Loadout Searches", "Query": "Szukaj", "Title": "Historia wyszukiwania", "UsageCount": "# Używanych" }, "Settings": { "Appearance": "Appearance", "ArmorArchetypeModslot": "Armor Archetype / Modslot", "AutoLockTagged": "Zsynchronizuj stan blokady przedmiotu z tagiem", "AutoLockTaggedExplanation": "DIM będzie automatycznie blokował i odblokowywał przedmioty, aby dopasować je do ich tagów. Stworzone przedmioty pozostaną odblokowane, aby umożliwić przemodelowanie. Gdy to ustawienie jest włączone, ikona blokady nie będzie wyświetlana na kafelku przedmiotu dla otagowanych przedmiotów.", "BadgePostmaster": "Pokarz liczbę przedmiotów u kuriera dla obecnej postaci w ikonie aplikacji", "BadgePostmasterExplanation": "Aby to zadziałało, musisz zainstalować DIM jako aplikację, a system operacyjny musi obsługiwać wyświetlanie powiadomień", "BothDescriptions": "Oba opisy", "BungieDescriptionOnly": "Opisy Bungie", "CharacterOrder": "Sortuj postaci wg", "CharacterOrderFixed": "Wiek postaci (buguje się na PC)", "CharacterOrderRecent": "Ostatnio używana postać", "CharacterOrderReversed": "Ostatnio używana postać (odwrócone)", "ColumnSize": "{{num}} przedmiotów", "ColumnSizeAuto": "Auto", "CommunityData": "Spostrzeżenia Społeczności dotyczące Cech", "CommunityDescriptionOnly": "Opisy społeczności", "CsvImport": "Importuj z CSV", "CustomErrorLabel": "A stat name must contain word characters, and be different from other stat names for this Guardian class.", "CustomErrorValues": "Stat weights must be positive numbers.\nAt least 2 stat weights must be above zero.", "CustomStatChooseName": "Choose a Custom Stat name", "CustomStatCreate": "Create a new custom stat", "CustomStatDelete": "Delete this Custom Stat", "CustomStatDeleteConfirm": "Delete this Custom Stat?", "CustomStatDesc1": "Choose desired armor stats to make a custom total stat.", "CustomStatDesc3": "Custom stats will appear in the Item Popup, Organizer, and Compare.", "CustomStatTitle": "Custom Stat Total", "Data": "Arkusze kalkulacyjne", "DefaultItemSizeNote": "Rozmiar przedmiotu 50px będzie wyglądał najostrzej, bez rozmycia obrazu przedmiotu lub tekstu.", "DontForgetDupes": "Nie zapomnij, że możesz wyszukać is:dupe aby szybko znaleźć zduplikowane przedmioty, oraz możesz użyć narzędzia porównującego lub organizera do oceny podobnych przedmiotów.", "EnableAdvancedStats": "Pokaż ocenę jakości statystyk pancerza (D1)", "ExpandSingleCharacter": "Pokaż wszystkie postacie", "ExportLoadoutSS": "Loadout spreadsheets", "ExportLoadoutSSHelp": "Download a CSV list of your DIM Loadouts that can be easily viewed in the spreadsheet app of your choice.", "ExportProfile": "Eksportuj reakcję profilu API", "ExportSS": "Arkusze kalkulacyjne ekwipunku", "ExportSSHelp": "Pobierz listę CSV swoich przedmiotów, którą można łatwo przeglądać w wybranej aplikacji arkusza kalkulacyjnego.", "HidePullFromPostmaster": "Ukryj przycisk \"$t(Loadouts.PullFromPostmaster)\"", "Inventory": "Wyświetlanie ekwipunku", "InventoryColumns": "Szerokość ekwipunku postaci", "InventoryColumnsMobile": "Szerokość ekwipunku postaci na mobilnym trybie portretowym", "InventoryColumnsMobileLine2": "Elementy zostaną przeskalowane, aby uwzględnić nowe ustawienie", "InventoryNumberOfSpacesToClear": "Number of empty spaces to make when using Farming Mode", "Items": "Wyświetlanie przedmiotu", "Language": "Język", "LogOut": "Wyloguj się", "Masterworked": "Mistrzowskie", "MaxParallelCores": "Maximum cores for parallel tasks", "MaxParallelCoresExplanation": "Controls how many CPU cores DIM can use for intensive tasks like Loadout Optimizer and Loadout Analyzer. Higher values may improve performance but use more system resources.", "OrnamentDisplay": "Show Ornaments on item tiles", "OrnamentDisplayExplanationDisabled": "Items will never display their ornaments", "OrnamentDisplayExplanationEnabled": "Hovering or long-pressing armor will hide its ornament", "OrnamentDisplayExplanationHide": "Hovering or long-pressing an item will hide its ornament", "OrnamentDisplayExplanationShow": "Hovering or long-pressing an item will show its ornament", "ResetToDefault": "Resetuj", "RestoreVaultSide": "Show vaulted items in their own column", "ReverseSort": "Przełącz sortowanie do przodu/do tyłu", "SetSort": "Sortuj produkty według:", "SetVaultWeaponGrouping": "Group vault weapons by:", "Settings": "Ustawienia", "ShowNewItems": "Pokaż czerwoną kropkę na nowych przedmiotach", "SingleCharacter": "Widok pojedynczej postaci", "SingleCharacterExplanation": "DIM pokaże tylko ostatnio graną postać.\nPrzedmioty trzymane przez ukryte postacie pojawią się w sejfie, jeżeli mogą zostać użyte przez bieżącą postać.\nPrzedmioty konkretne dla innych klas zostaną całkowicie ukryte.", "SizeItem": "Rozmiar elementu", "SortByAmmoType": "Rodzaj amunicji", "SortByAmount": "Rozmiar stosu", "SortByClassType": "Wymagana klasa", "SortByCrafted": "Stworzone (D2)", "SortByDeepsight": "Rezonans Głębokiego Wglądu (D2)", "SortByFeatured": "New Gear / Featured (D2)", "SortByPrimary": "Poziom mocy", "SortByRarity": "Rzadkość", "SortByRating": "Jakość pancerza (D1)", "SortByRecent": "Ostatnio nabyte (D2)", "SortBySeason": "Sezon (D2)", "SortByTag": "Tag ({{taglist}})", "SortByTier": "Tier (D2)", "SortByType": "Rodzaj", "SortByWeaponElement": "Typ obrażeń", "SortCustom": "Sortowanie niestandardowe", "SortName": "Nazwa", "SpacesSize_few": "{{count}} spaces", "SpacesSize_many": "{{count}} spaces", "SpacesSize_one": "{{count}} space", "SpacesSize_other": "{{count}} spaces", "Theme": "Theme", "Troubleshooting": "Troubleshooting", "VaultArmorGroupingStyle": "Separate armor on different lines by class", "VaultGroupingNone": "Brak", "VaultUnder": "Show vaulted items under equipped items", "VaultWeaponGroupingStyle": "Separate weapon groups on different lines", "WeaponFrame": "Weapon Frame", "WishlistRefreshNotificationBody": "If you do not see any updates, be sure the source (such as GitHub) reflects them!", "WishlistRefreshNotificationTitle": "Wishlists Reloaded" }, "Sockets": { "ApplyPerks": "Zastosuj cechy", "GridStyle": "Wyświetl cechy w siatce", "Insert": { "Ability": "Wyposaż umiejętność", "Aspect": "Wstaw aspekt", "Fragment": "Wstaw fragment", "Mod": "Wstaw modyfikację", "Ornament": "Zastosuj Zdobienie", "Projection": "Zastosuj Projekcję Ducha", "Shader": "Zastosuj Barwę", "Super": "Wyposaż Superzdolność", "Transmat": "Nałóż Efekt Teleportacji" }, "ListStyle": "Wyświetlaj cechy jako listę", "Search": "Wyszukaj nazwę lub opis", "Select": { "Ability": "Podejrzyj umiejętność", "Aspect": "Podejrzyj aspekt", "Fragment": "Podejrzyj fragment", "Mod": "Podejrzyj modyfikację", "Ornament": "Podejrzyj ozdobę", "Projection": "Podejrzyj projekcję Ducha", "Shader": "Podejrzyj barwę", "Super": "Podejrzyj Superzdolność", "Transmat": "Podejrzyj Efekt Transmatu" }, "SelectWishlistPerks": "Podgląd Listy Życzeń Cech" }, "Stats": { "CrouchingSpeed": "Kucanie", "Custom": "Niestandardowa suma", "CustomDesc": "Niestandardowa suma wybranych statystyk podstawowych, ignorując modyfikacje lub mistrzostwo. Odwiedź ustawienia, aby skonfigurować, które statystyki są dołączone.", "DamageResistance": "PvE Damage Resist", "Discipline": "Dyscyplina", "DropLevel": "Account Power", "DropLevelExplanation1": "Account Power is the base power level when calculating the increased level of rewards.", "DropLevelExplanation2": "Account Power uses the highest level item in each slot, regardless of required Class or the \"One Exotic\" rule.", "EquippableGear": "Equippable Gear", "FlinchResistance": "Flinch Resist", "HP": "PŻ", "Intellect": "Intelekt", "MaxGearPower": "Maksymalna Moc dostępnego wyposażenia", "MaxGearPowerAll": "Maksymalna moc całego wyposażenia", "MaxGearPowerOneExoticRule": "Maximum Power of equippable gear\n(only one Exotic armor piece equipped)", "MaxTotalPower": "Maksymalna całkowita Moc", "MetersPerSecond": "m/s", "Milliseconds": "ms", "NoBonus": "Bez bonusu", "NotApplicable": "ND.", "OfMaxRoll": "{{range}} maksymalnego rolla", "PercentHelp": "Kliknij, aby uzyskać więcej informacji na temat tego czym jest Jakość Statystyk.", "Percentage": "%", "PowerModifier": "Moc przyznana przez postęp sezonowego doświadczenia", "Prestige": "Poziom Prestiżu: {{level}}\n{{exp}}xp do 5 pyłków światła.", "Quality": "Jakość statystyk", "ShieldHP": "PŻ tarczy", "StrafingSpeed": "Strafing", "Strength": "Siła", "TierProgress": "T{{tier}} {{statName}} ({{progress}}/60 do T{{nextTier}})\n", "TierProgress_Max": "T{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "Czas do pełnych PŻ", "Total": "Łącznie", "TotalHP": "Całkowita liczba PŻ", "WalkingSpeed": "Chodzenie", "WeaponPart": "Weapon Part" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM może teraz przechowywać tagi, uzbrojenie, i ustawienia na naszych własnych serwerach i synchronizować te dane pomiędzy różnymi wersjami DIM, bez oddzielnego logowania. Możesz zaimportować Twoje istniejące dane ze strony ustawień, jeśli wcześniej nie włączyłeś synchronizacji DIM. Było to możliwe dzięki wsparciu naszych zwolenników OpenCollective!", "No": "Nie teraz", "Title": "Włączyć synchronizację DIM?", "Yes": "Włącz synchronizację" }, "AutoBackup": "Zrobiliśmy kopię zapasową Twoich danych do pliku w folderze pobrane o nazwie dim-data.json, na wszelki wypadek.", "BackUpFirst": "MUSISZ wykonać kopię zapasową swoich danych, zanim je usuniesz. Na wszelki wypadek.", "BrowserMayClearData": "Przeglądarka może usunąć te informacje, jeśli zabraknie Ci miejsca lub nie będziesz często odwiedzać DIM.", "DataIsLocal": "Tag and notes data is local only", "DeleteAllData": "Usuń WSZYSTKIE dane z serwerów synchronizacji DIM", "DeleteAllDataConfirm": "Czy na pewno chcesz usunąć WSZYSTKIE dane, z wszystkich kont, ze synchronizacji DIM? Nie możesz tego cofnąć.", "Details": { "IndexedDBStorage": "Lokalna pamięć zapisze twoje informacje tylko w tej przeglądarce. Wyczyszczenie danych przeglądania spowoduje usunięcie tych informacji." }, "DimApiFinePrint": "DIM zapisze tagi, uzbrojenie i ustawienia na serwerach DIM i zsynchronizuje je między różnymi wersjami DIM.", "DimSyncDown": "DIM Sync nie jest połączony z powodu problemu z komunikacją z serwerem.", "DimSyncEnabled": "Synchronizacja DIM włączona", "DimSyncNotEnabled": "DIM Sync nie jest włączony, więc Twoje ustawienia, tagi, Uzbrojenia i wyszukiwania są przechowywane tylko lokalnie i zostaną utracone, jeśli wyczyścisz pamięć przeglądarki. Włącz DIM Sync w Ustawieniach, aby automatycznie tworzyć kopię zapasową danych, lub regularnie wykonuj ją ręcznie.", "EnableDimApi": "Włącz synchronizację DIM (zalecane)", "Export": "Pobierz kopię zapasową danych", "ExportError": "Nie udało się pobrać kopii zapasowej z DIM Sync", "ExportErrorBody": "DIM Sync może być wyłączony lub możesz mieć problemy z połączeniem. Zamiast tego pobierzemy kopię Twoich lokalnie przechowywanych danych.", "Import": "Importuj kopię zapasową danych", "ImportConfirmDimApi": "Czy na pewno chcesz nadpisać swoje obecne tagi, uzbrojenie i ustawienia tą wersją? To całkowicie zastąpi to, co posiadasz.", "ImportExport": "Kopia zapasowa i import", "ImportFailed": "Importowanie nieudanie! {{error}}", "ImportNoFile": "Nie wybrano pliku!", "ImportNotification": { "FailedBody": "Nie można zaimportować danych. {{error}}", "FailedTitle": "Importowanie nie powiodło się", "NoData": "Nie znaleziono uzbrojeń ani tagów w kopii zapasowej", "SuccessBodyForced": "Zaimportowano ustawienia, {{loadouts}} uzbrojenia i {{tags}} otagowanych przedmiotów z kopii zapasowej do synchronizacji DIM, zastępując to, co już tam było.", "SuccessBodyLocal": "Zaimportowano ustawienia, {{loadouts}} uzbrojenia i {{tags}} otagowanych przedmiotów z kopii zapasowej do pamięci lokalnej, zastępując to, co już tam było. Nie możemy zagwarantować, że pamięć lokalna nie zostanie utracona - rozważ włączenie DIM Sync.", "SuccessTitle": "Import zakończony sukcesem" }, "ImportTooManyFiles": "Wybierz tylko jeden plik do importu.", "ImportWrongFileType": "Plik nie jest plikiem JSON. To może nie być kopia zapasowa DIM.", "IndexedDBStorage": "Lokalna pamięć przeglądarki", "LearnMore": "Dowiedz się więcej o synchronizacji DIM", "MenuTitle": "Synchronizacja i kopie zapasowe", "ProfileErrorBody": "Wystąpił problem z komunikacją z DIM Sync. Twoje najnowsze ustawienia, tagi, ładunki i wyszukiwania mogą nie być wyświetlane. Twoje dane nadal znajdują się na naszych serwerach, a wszelkie aktualizacje dokonane lokalnie zostaną zapisane, gdy będziemy mogli ponownie nawiązać połączenie. Będziemy próbować, dopóki DIM jest otwarty.", "ProfileErrorTitle": "Błąd pobierania DIM Sync", "RefreshDimSync": "Reload remote data from DIM Sync", "UpdateErrorBody": "Wystąpił problem podczas zapisywania Twoich danych do DIM Sync. Będziemy próbować ponownie, dopóki DIM jest otwarty.", "UpdateErrorTitle": "Błąd zapisu DIM Sync", "UpdateInvalid": "Failed to save data to DIM Sync", "UpdateInvalidBody": "Data sent to DIM Sync was invalid and will not be saved.", "UpdateInvalidBodyLoadout": "The loadout \"{{name}}\" is invalid and will not be saved. If you imported it from another site, please let them know that they are exporting invalid loadouts.", "UpdateQueueLength_few": "{{count}} nowe(-wych) zmian(y) zostaną(-nie) zapisane(-nych), gdy będziemy mogli się ponownie połączyć.", "UpdateQueueLength_many": "{{count}} nowe(-wych) zmian(y) zostaną(-nie) zapisane(-nych), gdy będziemy mogli się ponownie połączyć.", "UpdateQueueLength_one": "{{count}} nowa zmiana zostanie zapisana, gdy będziemy mogli się ponownie połączyć.", "UpdateQueueLength_other": "{{count}} nowe(-wych) zmian(y) zostaną(-nie) zapisane(-nych), gdy będziemy mogli się ponownie połączyć.", "Usage": "DIM używa {{usage, humanBytes}} z {{quota, humanBytes}} dostępnych dla niego na tym urządzeniu. Obejmuje to pobrane bazy danych przedmiotów Destiny z Bungie.net." }, "StreamDeck": { "Authorize": "Połącz aplikację", "Enable": "Wtyczka Stream Deck", "Error": { "Body": "There was an error sending data to the Stream Deck plugin. Please contact the plugin developer. {{error}}", "Title": "Stream Deck Plugin Error" }, "FinePrint": "Włącz połączenie ze wtyczką DIM Stream Deck. Ta wtyczka jest osobnym projektem, który nie jest napisany ani nie jest wspierany przez zespół DIM.", "Install": "Zainstaluj wtyczkę", "MissingAuthorization": "You must authorize the Stream Deck application to connect to DIM. Go to settings and click \"Connect application\".", "Tooltip": { "Application": "Aplikacja Stream Deck", "AuthRequired": "Click this button or go to settings and click \"Connect application\".", "Error": "Your Stream Deck plugin is no longer supported. Please update to the latest version. This plugin requires at least:", "ErrorConnection": "if you're already using the latest version, check if some browser extension is blocking the connection.", "ExtensionIssue": "Extensions Issue", "Plugin": "Plugin", "Title": "DIM Stream Deck Plugin", "Version": "Version:" } }, "StripSockets": { "Action": "Opróżnij Gniazda", "ArmorMods": "{{count}}x Modyfikacja Pancerza", "Button": "Opróżnij {{numSockets}} Gniazd(a)", "Cancel": "Anuluj", "Choose": "Wybierz Gniazda do opróżnienia", "DiscountedMods": "{{count}}x Discounted Mod", "Done": "Opróżnione Gniazda", "NoSockets": "Brak Gniazd do wyczyszczenia", "Ok": "Ok", "Ornaments": "{{count}}x Zdobień", "Others": "{{count}}x Duchowa Projekcja", "Running": "Opróżnianie Gniazd", "Shaders": "{{count}}x Barwa", "Subclass": "{{count}}x Opcja Podklasy", "WeaponMods": "{{count}}x Modyfikacja Broni" }, "Tags": { "Archive": "Archiwizuj", "ClearTag": "Wyczyść tag", "Favorite": "Ulubione", "Infuse": "Nasyć", "Junk": "Śmieci", "Keep": "Zachowaj", "LockAll": "Zablokuj przedmioty", "TagItem": "Otaguj przedmiot", "UnlockAll": "Odblokuj Przedmioty" }, "Triage": { "AccountsForArtifice": "Sprawdza, czy Pancerz Pomysłowości może być lepszy, jeśli użyto modyfikacji statystyk +3.", "BetterArmor": "Ściśle lepszy Pancerz", "BetterArtificeArmor": "Lepszy Pancerz Pomysłowości", "BetterStatArmor": "Lepsze statystyki Pancerza", "BetterStatArtificeArmor": "Lepsze statystyki Pancerza Pomysłowości", "BetterWorseArmor": "Lepszy/Gorszy Pancerz", "BetterWorseIncludes": "Identyfikuje Pancerz z:", "HighStats": "Wysokie statystyki", "InLoadouts": "W Uzbrojeniu", "OwnedCount": "# Posiadane", "PerkBetterArmorDesc": "Te same lub więcej, Cech Pancerza lub specjalnych gniazd modyfikacji.", "PerkWorseArmorDesc": "Ta sama Cecha Pancerza lub jej brak.", "SimilarItems": "Podobne przedmioty", "StatBetterArmorDesc": "Wszystkie statystyki co najmniej tak samo wysokie, a co najmniej jedna statystyka lepsza.", "StatNotPerkArmorDesc": "To testuje tylko statystyki. Pancerz z niższymi statykami może nadal mieć specjalne gniazda na modyfikacje lub Cechy Pancerza.", "StatWorseArmorDesc": "Brak lepszych statystyk i co najmniej jedna gorsza.", "ThisItem": "Ten przedmiot", "WorseArmor": "Znacznie gorszy Pancerz", "WorseArtificeArmor": "Gorszy Pancerz (nie Pancerz Pomysłowości)", "WorseStatArmor": "Gorsze statystyki Pancerza", "WorseStatArtificeArmor": "Gorszy statystyki Pancerza (nie Pancerz Pomysłowości)", "YourBestItem": "Twój najlepszy przedmiot" }, "Triumphs": { "GildingTriumph": "Pozłacany Triumf", "HideCompleted": "Ukryj ukończone triumfy", "RevealRedacted": "Ujawnij zredagowane triumfy", "SortRecords": "Sort triumphs by completion" }, "Vendors": { "Collections": "Kolekcja", "Engram": "Ranga", "FilterToUnacquired": "Pokazuje tylko niezdobyte przedmioty", "HideSilverItems": "Hide Silver items", "NoItems": "Ten sprzedawca nie oferuje obecnie żadnych przedmiotów.", "RefreshTime": "Ekwipunek odświeży się za:", "Vendors": "Sprzedawcy" }, "Views": { "About": { "APIHistory": "Zobacz historię wszystkich działań podejmowanych przez DIM (i inne aplikacje Destiny)", "BungieCopyright": "Wszystkie obrazy i zawartość jest własnością Bungie.", "CommunityInsight": "Community Insights for Perks and Character Stats courtesy of {{clarityLink}}. If you notice inaccuracies or have questions, join the {{clarityDiscordLink}}.", "Discord": "Discord", "DiscordHelp": "Zadawaj pytania, wyrażaj opinie i uzyskaj wsparcie na naszych kanałach Discord.", "FAQ": "Często Zadawane Pytania (FAQ)", "FAQAccess": "W jaki sposób DIM uzyskuje dostęp do moich danych Destiny?", "FAQAccessAnswer": "Używamy uwierzytelniania aplikacji Bungie w celu przyznania dostępu do DIM, aby zobaczyć i przenieść Twoje przedmioty. DIM nigdy nie widzi Twojej nazwy użytkownika ani hasła. W ten sam sposób działa aplikacja Companion.", "FAQKeyboard": "Czy DIM wspiera skróty klawiszowe?", "FAQKeyboardAnswer": "Tak! Wciśnij \"?\" żeby zobaczyć listę dostępnych skrótów.", "FAQLogout": "Jak mogę wylogować się z DIM?", "FAQLogoutAnswer": "Otwórz menu w lewym górnym rogu i kliknij \"Wyloguj\"", "FAQLostItem": "Straciłem przedmiot używając waszego narzędzia!", "FAQLostItemAnswer": "Bungie nie zezwala aplikacjom na usuwanie elementów (nawet w ich własnej aplikacji!). Prawdopodobnie transfer nie powiódł się, pozostawiając twój przedmiot w skarbcu lub na innej postaci. Możesz wyszukać element. Jeśli to nie pomoże, przeładuj stronę. Sprawdź {{link}} lub w grze, aby zobaczyć, czy Twój przedmiot nadal istnieje. Jesteśmy pewni, że wciąż tam jest.", "FAQMobile": "Czy DIM wspiera urządzenia mobilne? Czy będzie dostępna aplikacja?", "FAQMobileAnswer": "Strona DIM może być używana na telefonach i tabletach już dziś, możesz również dodać skrót na pulpicie.", "GitHub": "GitHub", "GitHubHelp": "Jeśli chcesz wziąć udział w projekcie, odwiedź nas na naszej stronie projektu {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM jest darmową aplikacją z otwartym źródłem, tworzona przez programistów społeczności używając tych samych usług, z których korzysta Bungie.net i aplikacja Destiny Companion.", "Schedule": { "beta": "Ta wersja beta DIM jest aktualizowana za każdym razem, gdy zmieniamy kod - dostaje najnowsze funkcje i poprawki, ale także najnowsze błędy!", "release": "Ta wersja DIM jest aktualizowana raz w tygodniu, około północy w niedziele czasu Pacyficznego." }, "Translation": "Dołącz do Drużyny Tłumaczy!", "TranslationText": "Używamy {{link}} w celu ułatwienia tłumaczenia. Jeśli chcesz poprawić jedno z tłumaczeń DIM, dołącz do zespołu.", "Version": "Wersja {{version}} ({{flavor}}), zbudowana {{date}}", "Wiki": "Podręcznik użytkownika DIM", "WikiHelp": "Dowiedz się, jak korzystać z funkcji DIM." }, "Login": { "Auth": "Autoryzuj za pomocą Bungie.net", "EnableDimSyncWarning": "Wcześniej wyłączono synchronizację DIM i używano tylko lokalnego przechowywania danych. Włączenie synchronizacji DIM zastąpi wszystkie lokalne dane danymi z synchronizacji DIM. Przed włączeniem synchronizacji DIM powinieneś utworzyć kopię zapasową swoich danych. Możesz przywrócić tę kopię zapasową w ustawieniach.", "Explanation": "Pozwól, aby DIM mógł wyświetlać i modyfikować postacie, schowek i postęp w Destiny.", "LearnMore": "Dowiedz się więcej o kontach i logowaniu", "NewAccount": "Zaloguj się przy użyciu innego konta Bungie.net", "Permission": "Potrzebujemy Twojej zgody..." }, "Support": { "BackersDetail": "Wesprzyj nas darowizną jednorazową lub miesięczną i pomóż nam kontynuować nasz aktywny rozwój.", "FreeToDownload": "DIM to produkt, który można bezpłatnie pobrać i używać. Kod źródłowy dla DIM jest open source i jest darmowy dla każdego kto chce go ulepszyć. Nigdy nie zobaczysz reklamy w DIM. To jest nasze zobowiązanie.", "OpenCollective": "Używamy {{link}} jako usługi, aby zapewnić naszym programistom wynagrodzenie za poświęcenie i czas spędzony na tym projekcie.", "Store": "Mamy merch z naszym logo i innymi projektami na sprzedaż w {{link}}", "Support": "Wesprzyj DIM" } }, "WishListRoll": { "BestRatedTip_few": "Te cechy dokładnie odpowiadają rollowi broni na twojej liście życzeń.", "BestRatedTip_many": "Te cechy dokładnie odpowiadają rollowi broni na twojej liście życzeń.", "BestRatedTip_one": "Ten atut dokładnie odpowiada rollu broni na twojej liście życzeń.", "BestRatedTip_other": "Te cechy dokładnie odpowiadają rollowi broni na twojej liście życzeń.", "Clear": "Wyczyść Listę Życzeń", "CopiedLine": "Lista Życzeń rolli skopiowana do schowka", "CopyLine": "Skopiuj wybrane cechy jako rolle Listy Życzeń", "DupeRolls": " (+{{num, number}} ignored dupes)", "ExternalSource": "Dodaj kolejną Listę Życzeń", "ExternalSourcePlaceholder": "Wklej tutaj adres URL listy życzeń", "Header": "Lista Życzeń", "Import": "Wczytaj Rolle z Listy Życzeń", "ImportError": "Error loading wish list from \"{{url}}\": {{error}}", "ImportFailed": "None of your wish lists contained any valid rolls.", "ImportNoFile": "Nie wybrano pliku.", "InvalidExternalSource": "Wprowadź prawidłowy adres URL dla zewnętrznego źródła listy życzeń. Adres URL musi zaczynać się od jednego z następujących:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "Ostatnia aktualizacja: {{lastUpdatedDate}} o {{lastUpdatedTime}}", "Num": "{{num, number}} rolli na twojej liście życzeń", "NumRolls": "{{num, number}} rolls", "Refresh": "Refresh Wishlist", "SourceAlreadyAdded": "Lista życzeń już dodana", "UpdateExternalSource": "Dodaj listę życzeń", "Voltron": "voltron (domyślna)", "WishListNotes": "Notatki listy życzeń:", "WorstRatedTip_few": "These perks exactly match a weapon roll on your trashlist.", "WorstRatedTip_many": "These perks exactly match a weapon roll on your trashlist.", "WorstRatedTip_one": "This perk exactly matches a weapon roll on your trashlist.", "WorstRatedTip_other": "These perks exactly match a weapon roll on your trashlist." }, "no-space": "brak-miejsca", "wrong-level": "zły-poziom" } ================================================ FILE: src/locale/ptBR.json ================================================ { "AWA": { "ConfirmDescription": "Por favor, utilize o app Destiny Companion para dar permissão ao DIM de modificar seus itens.", "ConfirmTitle": "Confirmar Ação", "Error": "Erro ao modificar mods ou perks", "ErrorMessage": "Não foi possível equipar {{plug}} em {{item}}.\n\n{{error}}", "FailedToken": "Não foi possível obter a permissão para alterar o item", "IrreversiblePlugging": "Você não possui {{plug}}, então não podemos sobrescrevê-lo." }, "Accounts": { "Choose": "Perfis para {{bungieName}}", "ErrorLoadInventory": "Não foi possível carregar os seus personagens e inventário de Destiny {{version}}", "ErrorLoadManifest": "Não foi possível carregar as informações de banco de dados de Destiny da Bungie", "ErrorLoading": "Não foi possível carregar as contas de Destiny da Bungie.net", "MissingAccountWarning": "Caso não encontre sua conta aqui, significa que você ainda não a autorizou através da Bungie.net ou o serviço da Bungie pode estar em manutenção no momento.", "MissingDescription": "A conta que você está tentando visualizar não está vinculada ao seu perfil da Bungie.net. Selecione uma das suas contas abaixo.", "MissingTitle": "Conta não encontrada", "NoCharacters": "Você não possui um personagem de Destiny associado a esta conta Bungie.net. Tente se logar com uma conta diferente.", "NoCharactersTitle": "Nenhum Personagem Encontrado", "SwitchAccounts": "Você poderá alternar entre contas posteriormente através do menu superior.", "Title": "Contas" }, "Activities": { "Activities": "Atividades", "Hard": "Difícil", "Nightfall": "Assalto do Anoitecer", "Normal": "Normal", "WeeklyHeroic": "Assalto Heroico Semanal" }, "Armory": { "AlternateItems": "Versões alternativas", "Armory": "Arsenal", "DifferentSeason": "Reissue from a different season", "NoNotes": "Sem anotações", "OpenInArmory": "ver no Arsenal", "Season": "Temporada {{season}}, ano {{year}}", "TrashlistedRolls_one": "Valores indesejáveis", "TrashlistedRolls_other": "{{count, number}} valores indesejáveis", "Unknown": "Item desconhecido", "UnknownPerkHash": "The perk hash {{hash}} ({{perkName}}) does not appear on this item, so this wish list roll is invalid. Please contact the wish list author to correct this. Note that wish lists should always specify the non-enhanced version of perks.", "WishlistedRolls_one": "Valores desejáveis", "WishlistedRolls_other": "{{count, number}} valores desejáveis", "YourItems": "Seus itens" }, "Browsercheck": { "Samsung": "O Samsung Internet pode fazer com que alguns sites pareçam muito escuros quando o modo escuro está ativado. Ative Configurações > Labs > Usar tema escuro do site da web ou mude para outro navegador.", "Steam": "O navegador do Painel Steam é muito antigo e alguns recursos do DIM podem não funcionar. Não podemos oferecer suporte para isso.", "Unsupported": "A equipe do DIM não oferece suporte a este navegador. Alguns recursos do DIM podem não funcionar." }, "Bucket": { "Armor": "Armaduras", "Class": "Subclasse", "General": "Geral", "Ghost": "Fantasma", "Inventory": "Inventário", "Postmaster": "Chefe do Correio", "Progress": "Progresso", "Reputation": "Reputações", "Unknown": "Desconhecido", "Vault": "Cofre", "Weapons": "Armas" }, "BulkNote": { "Append": "Anexar a anotações / adicionar #hashtags", "Confirm": "Atualizar Anotações", "Remove": "Remover de anotações / remover #hashtags", "Replace": "Substituir anotações", "Title_one": "Alterar anotação para 1 item", "Title_other": "Alterar anotações para {{count}} itens" }, "BungieAlert": { "Title": "Uma mensagem da Bungie:" }, "BungieService": { "AppNotPermitted": "DIM não tem permissão para realizar esta ação.", "DestinyCannotPerformActionAtThisLocation": "Você não pode equipar itens ou alterar mods enquanto estiver em uma atividade. Tente ir para a órbita ou área social. Isso é uma limitação do próprio jogo, não do DIM.", "DestinyItemUnequippable": "Você não pode equipar este item. Se a última atividade deste personagem bloqueou seus equipamentos, tente entrar no personagem novamente.", "DestinyLegacyPlatform": "Os serviços da Bungie tem atualmente um bug que impede que o DIM carregue informação da sua conta de Destiny 2 se você jogou o Destiny 1 em um console de última geração. A Bungie irá corrigir isso em breve, mas até lá, você deve jogar Destiny 1 em um console de geração atual para ser capaz de acessar a sua informação.", "DevVersion": "Você está executando uma versão de desenvolvimento do DIM? Você deve registrar sua extensão do Chrome no Bungie.net.", "Difficulties": "Bungie.net está enfrentando dificuldades no momento.", "ErrorTitle": "Erro na Bungie.net", "ItemUniquenessExplanation": "Um personagem só pode ter um '{{name}}' equipado.", "Maintenance": "Os servidores Bungie.net estão fora do ar para manutenção.", "MissingInventory": "Não foi possível receber seu inventário da Bungie.net, possivelmente devido às suas configurações de privacidade. Tente desconectar sua conta e autenticar-se novamente.", "NetworkError": "Erro de rede - {{status}} {{statusText}}", "NoAccount": "Nenhuma conta de Destiny encontrada. Você escolheu a plataforma certa?", "NoAccountForPlatform": "Não foi possível encontrar uma conta de Destiny no {{platform}}.", "NotConnected": "Você deve estar sem conexão com a Internet.", "NotConnectedOrBlocked": "Você deve estar sem conexão com a Internet ou uma extensão de bloqueio de anúncios ou privacidade pode estar bloqueando Bungie.net.", "NotLoggedIn": "Por favor, autorize o DIM para poder utilizar este app.", "Slow": "Bungie.net está temporariamente lento", "SlowDetails": "Bungie.net está levando mais tempo que o esperado para responder. Normalmente isto ocorre quando vários jogadores estão ativos no jogo ao mesmo tempo ou quando a Bungie.net está instável. E, quem sabe, seja apenas sua conexão com a internet mesmo. De qualquer forma, vamos continuar esperando por uma resposta.", "SlowResponse": "Bungie.net está demorando muito para responder.", "Throttled": "Bungie.net está limitando quantas solicitações o DIM pode fazer.", "Twitter": "Veja atualizações em:", "UnknownError": "Mensagem da Bungie.net: {{message}}", "VendorNotFound": "Dados do vendedor não disponíveis." }, "Compare": { "Archetype": "Arquétipo", "AssumeMasterworked": "Assumir Obra-prima", "AssumeMasterworkedDescription": "Stats if fully Masterworked, without current Mods", "BaseStatsDescription": "Base stats, without Masterwork or Mods", "Button": "Comparar", "ButtonHelp": "Comparar Itens", "CompareBaseStats": "Exibir atributos base", "CurrentStats": "Current Stats", "CurrentStatsDescription": "Current stats, including Mods and Masterwork level", "Error": { "Invalid": "Não existem itens válidos para comparação.", "Unmatched": "Este item não corresponde ao tipo dos itens que estão sendo comparados." }, "InitialItem": "A ferramenta de Comparação foi ativada a partir deste item", "IsVendorItem": "Esse item não está em seu inventário, mas {{vendorName}} o vende.", "NoModArmor": "Pré mods" }, "Cooldown": { "Grenade": "Tempo de carga da granada: {{cooldown}}", "Melee": "Tempo de carga do corpo-a-corpo: {{cooldown}}", "Super": "Tempo de carga da Super: {{cooldown}}" }, "Countdown": { "Days_compact_one": "{{count}}d", "Days_compact_other": "{{count}}d", "Days_one": "1 Dia", "Days_other": "{{count}} Dias" }, "Csv": { "EmptyFile": "Não haviam linhas no arquivo.", "ImportConfirm": "Tem certeza que deseja importar etiquetas/anotações do CSV? Isso irá sobrescrever etiquetas/anotações para todos os itens contidos em sua planilha.", "ImportFailed": "Falha ao importar etiquetas/anotações do CSV: {{error}}", "ImportSuccess_one": "Etiquetas/anotações carregadas para um item.", "ImportSuccess_other": "Etiquetas/anotações carregadas para {{count}} itens.", "ImportWrongFileType": "Arquivo não é um arquivo CSV.", "WrongFields": "CSV deve conter colunas 'Id', 'Notes', 'Tag' e 'Hash'." }, "Dialog": { "Cancel": "Cancelar", "OK": "OK" }, "EnergyMeter": { "Energy": "Energia", "Unused": "Não usado", "UpgradeNeeded": "A capacidade atual de energia deste item é {{energyCapacity}}. Para se adequar aos mods selecionados, sua capacidade de energia deve ser {{energyUsed}}.", "Used": "Usado" }, "ErrorBoundary": { "Title": "Algo deu errado" }, "ErrorPanel": { "BrowserTooOld": "Seu navegador é muito antigo para usar o DIM. Atualize seu navegador para a versão mais recente.", "BrowserTooOldTitle": "Navegador incompatível", "Description": "Tente carregar seu inventário no app Destiny 2 Companion para saber se a Bungie.net está fora do ar.", "ReadTheGuide": "Leia nosso Guia do Usuário (link do menu) para etapas de solução de problemas.", "SystemDown": "Este problema afeta todos os apps de Destiny. Sendo assim, nem mesmo o DIM será capaz de resolver.", "Troubleshooting": "Guia para Solução de Problemas" }, "FarmingMode": { "D2Desc_female_one": "DIM está evitando que itens sejam mantidos no Chefe do Correio, certificando-se que sempre haverá um único espaço livre por tipo de item no {{store}}.", "D2Desc_female_other": "DIM está evitando que itens sejam mantidos no Chefe do Correio, certificando-se que sempre haverá {{count}} espaços livres por tipo de item na {{store}}.", "D2Desc_male_one": "DIM está evitando que itens sejam mantidos no Chefe do Correio, certificando-se que sempre haverá um único espaço livre por tipo de item no {{store}}.", "D2Desc_male_other": "DIM está evitando que itens sejam mantidos no Chefe do Correio, certificando-se que sempre haverá {{count}} espaços livres por tipo de item na {{store}}.", "D2Desc_one": "DIM está evitando que itens sejam mantidos no Chefe do Correio, certificando-se que sempre haverá um único espaço livre por tipo de item no {{store}}.", "D2Desc_other": "DIM está evitando que itens sejam mantidos no Chefe do Correio, certificando-se que sempre haverá {{count}} espaços livres por tipo de item na {{store}}.", "Desc_female_one": "DIM está movendo engramas e consumíveis do {{store}} para o Cofre, mantendo um único espaço livre por tipo de item para prevenir que algo vá para o Chefe do Correio.", "Desc_female_other": "DIM está movendo engramas e consumíveis da {{store}} para o Cofre, mantendo {{count}} espaços livres por tipo de item para prevenir que algo vá para o Chefe do Correio.", "Desc_male_one": "DIM está movendo engramas e consumíveis do {{store}} para o Cofre, mantendo um único espaço livre por tipo de item para prevenir que algo vá para o Chefe do Correio.", "Desc_male_other": "DIM está movendo engramas e consumíveis da {{store}} para o Cofre, mantendo {{count}} espaços livres por tipo de item para prevenir que algo vá para o Chefe do Correio.", "Desc_one": "DIM está movendo engramas e consumíveis do {{store}} para o Cofre, mantendo um único espaço livre por tipo de item para prevenir que algo vá para o Chefe do Correio.", "Desc_other": "DIM está movendo engramas e consumíveis da {{store}} para o Cofre, mantendo {{count}} espaços livres por tipo de item para prevenir que algo vá para o Chefe do Correio.", "FarmingMode": "Modo Farm", "FarmingModeNote": "(mantém espaço livre para drops)", "MakeRoom": { "Desc": "O DIM está movendo apenas engramas e consumíveis do {{store}} para o cofre ou outros personagens para evitar que eles vão para o Chefe dos Correios.", "Desc_female": "O DIM está movendo apenas engramas e consumíveis do {{store}} para o cofre ou outros personagens para evitar que eles vão para o Chefe dos Correios.", "Desc_male": "O DIM está movendo apenas engramas e consumíveis do {{store}} para o cofre ou outros personagens para evitar que eles vão para o Chefe dos Correios.", "MakeRoom": "Abrir espaço para novos itens movendo equipamentos", "Tooltip": "Se marcado, o DIM irá mover também armas e armaduras para abrir espaço no cofre para engramas." }, "OutOfRoom": "Você está sem espaço para mover itens de {{character}}. Hora de limpar o lixo!", "OutOfRoomTitle": "Sem espaço", "Stop": "Parar", "Vault": "Itens serão movidos para o Cofre para liberar espaço." }, "FashionDrawer": { "Accept": "Salvar estilo", "CannotFitOrnament": "Este item não possui um soquete de ornamento ou você não tem um ornamento para ele.", "CannotFitShader": "Este item não pode receber um tonalizador", "ClearOrnaments": "Limpar Ornamentos", "ClearOrnamentsTitle": "Redefinir todos os ornamentos para \"sem preferência\"", "ClearShaders": "Limpar Tonalizadores", "ClearShadersTitle": "Redefinir todos os tonalizados para \"sem preferência\"", "NoPreference": "Sem preferência - este soquete não será alterado", "Reset": "Redefinir estilo", "Sync": "Sincronizar", "SyncOrnaments": "Sincronizar Ornamentos", "SyncOrnamentsTitle": "Usar os ornamentos do mesmo conjunto em todos os itens, se estiverem desbloqueados", "SyncShaders": "Sincronizar Tonalizadores", "SyncShadersTitle": "Usar o mesmo tonalizador em todos os itens", "Title": "Escolher tonalizadores e ornamentos", "UseEquipped": "Usar estilos equipados" }, "FileUpload": { "Instructions": "Clique ou arraste arquivos" }, "Filter": { "Adept": "\\(adepto\\)", "AmmoType": "Mostrar itens baseados no seu tipo de munição.", "Armor": "Exibe itens que são armaduras.", "Armor3": "Shows items that use the Armor 3.0 stat system introduced in Edge of Fate.", "ArmorCategory": "Mostrar armaduras baseadas na sua categoria.", "ArmorIntrinsic": "Exibe armaduras lendárias que possuem um perk intrínseco, como Armadura de Artífice.", "Artifice": "Shows Artifice armor.", "Ascended": "Exibe itens que possuem nódulos ascendentes e que foram ascendidos.", "Breaker": "Filtrar por tipo de quebra ou tipo de campeão correspondente. breaker:instrinsic mostra itens com habilidade intrínseca de quebra.", "BulkClear_one": "Etiqueta removida de 1 item.", "BulkClear_other": "Etiquetas removidas de {{count}} itens.", "BulkRevert_one": "Etiqueta revertida em 1 item.", "BulkRevert_other": "Etiquetas revertidas em {{count}} itens.", "BulkTag_one": "Adicionado a etiqueta {{tag}} ao item selecionado.", "BulkTag_other": "Adicionado a etiqueta {{tag}} aos {{count}} itens selecionados.", "Catalyst": "Exibe os catalisadores com base em sua situação atual. O filtro catalyst:complete exibe catalisadores completos e aplicados; catalyst:incomplete exibe catalisadores desbloqueados, mas com objetivos incompletos ou não aplicado; catalyst:missing exibe itens que tem catalisador mas você ainda não os encontrou.", "Class": "Mostra itens baseado na sua afinidade por classe.", "Combine": "Filtros podem ser combinados ou agrupados com parênteses, \"or\" (ou) e \"and\" (e) para restringir sua pesquisa, por exemplo: \"{{example}}\".", "ContributePower": "Mostra os itens que tem poder e que podem contribuir para o seu nível de poder.", "Cosmetic": "Exibe itens que são de estilo ou cosméticos.", "Craftable": "Exibe itens que são fabricáveis.", "CraftedDupe": "Exibe os itens duplicados onde pelo menos uma das duplicatas tenha sido criada.", "Curated": "Exibe itens que possuem valores curados.", "CurrentClass": "Exibe itens que podem ser equipados no guardião que está jogando atualmente.", "CustomStatLower": "Mostra armaduras cujas estatísticas são estritamente menores que outra do mesmo tipo de armadura, levando em conta apenas estatísticas que estão em qualquer lista personalizada de estatísticas dessa classe.", "DamageType": "Mostra itens baseado no seu tipo de dano.", "Deepsight": "Exibe armas com Ressonância de Visão Penetrante, que podem ter seus padrões extraídos, ou que podem ter Ressonância de Visão Penetrante habilitada utilizando um Harmonizador de Visão Penetrante.", "Deprecated": "Este filtro não é mais suportado.", "Description": "Descrição", "DescriptionFilter": "Exibe itens cuja descrição coincide parcialmente com o texto do filtro. Procure por frases inteiras usando aspas.", "DisabledModSlot": "Mostra itens com um mod desabilitado.", "Dupe": "Exibe itens duplicados, incluindo revisões", "DupeArchetype": "Groups armor with the same stat Archetype.", "DupeCount": "Itens que tenham o número especificado de duplicatas.", "DupeLower": "Itens duplicados, incluindo similares, que não possuem o maior poder duplicado. Apenas uma das duplicatas é escolhida como a maior. Todos os demais são considerados menores.", "DupePerks": "Mostra itens cujos perks são ou uma duplicata ou um subconjunto de outro item do mesmo tipo.", "DupeSetBonus": "Groups armor with the same set bonus.", "DupeStats": "Shows armor with identical base stats, and matching stat adjustment mods like Artifice or Tuners.", "DupeTertiary": "Groups armor with the same tertiary stat.", "DupeTraits": "Weapons whose traits are either a duplicate of, or a subset of, another weapon of the same type.", "DupeTunedStat": "Groups armor with the same Tuned stat.", "DupeUntunedStats": "Groups armor with identical base stats, ignoring stat adjustment mods.", "DupeZeroStats": "Groups armor with the same 3 non-zero base stats.", "Energy": "Shows items that use the Armor 2.0 mod system introduced in Shadowkeep.", "EnergyCapacity": "Shows items based on their current energy capacity.", "Engrams": "Mostrar engramas.", "Enhanceable": "Mostra armas que podem ser melhoradas.", "Enhanced": "Mostra armas baseado em seu nível de aprimoramento.", "EnhancedPerk": "Exibe armas que possuem um número específico de perks melhorados.", "EnhancementReady": "Exibe armas que atingiram limites para melhoramento de perks.", "Equipment": "Itens que podem ser equipados.", "Equipped": "Itens que no momento estão equipados em outra personagem.", "Event": "Mostra itens de que evento apareceram no Destiny 2.", "ExtraPerk": "Mostra armas Lendárias com valores aleatórios que tenham um perk adicional selecionável.", "Featured": "Items that count as one of the \"New Gear\" or \"Featured Items\" in the current season.", "Filter": "Filtro", "FilterWith": "Filtrar com:", "Focusable": "Exibe itens que podem ser focados em um vendedor", "Foundry": "Exibe itens de acordo com sua fonte criadora.", "Glimmer": "Exibe itens que são consumíveis que estão relacionados a ganho de lúmen.", "Harrowed": "\\(Sepulcral\\)", "HasNotes": "Exibe itens que possuem anotações.", "HasOrnament": "Mostra itens que tem um ornamento aplicado.", "HasShader": "Mostra itens que tem um tonalizador aplicado.", "Holofoil": "Shows holofoil weapons.", "InDimLoadout": "is:indimloadout mostra itens que estão incluídos em qualquer set do DIM.", "InInGameLoadout": "is:iningameloadout mostra itens que estão incluídos em qualquer set no jogo.", "InInventory": "Shows items that you have at least one copy of in your inventory. Only really useful in the Vendors and Records screens.", "InLoadout": "is:inloadout mostra os itens que estão incluídos em qualquer set. Buscando com inloadout: mostra os itens que estão inclusos nos sets com títulos correspondentes. Quando usado com uma hashtag, inloadout: mostrará itens cujos sets possuem a hashtag no título ou nas anotações. Quando usado com um intervalo, mostrará itens que estão naqueles tantos sets.", "Infusable": "Mostra itens que podem ser infundidos.", "InfusionFodder": "Exibe itens que podem ser infundidos em versões de menor poder do mesmo item usando apenas lúmen.", "IsAdept": "Mostra armas compatíveis com os mods Adeptos.", "IsCrafted": "Mostra armas que foram criadas.", "ItemHash": "Exibe itens com determinado hash de inventário de item. Para usuários avançados.", "ItemId": "Exibe o item com determinado ID de inventário de item. Para usuários avançados.", "Leveling": { "Complete": "{{term}} - mostra itens que estão totalmente completos - todos os aprimoramentos destravados.", "Incomplete": "{{term}} - mostra itens que não estão completos - ainda existe pelo menos um aprimoramento a destravar.", "NeedsXP": "{{term}} - mostra itens que ainda podem ganhar XP.", "Upgraded": "{{term}} - mostra itens que tem XP suficiente pra destravar todos os seus nódulos, mas nem todos eles foram destravados.", "XPComplete": "{{term}} - mostra itens que não podem mais ganhar XP (estando com seus todos os seus aprimoramentos destravados ou não)." }, "Location": "Mostra itens baseado na sua localização dentro do app. left/middle/right (esquerda/meio/direita) são a localização visual do personagem, e enquanto inleftchar funciona sempre, os outros funcionam baseado em quantos personagens você tem. current é o seu personagem atualmente logado ou logado pela última vez (é aquele marcado com um triângulo amarelo).", "LockAllFailed": "Falha ao travar itens", "LockAllSuccess": "{{num}} itens travados", "Locked": "Mostra itens baseado em seu estado de travamento.", "Masterwork": "Exibe itens baseado em seu atributo ou nível de obra-prima.", "MasterworkKills": "Exibe itens baseado no contador de mortes de obra-prima.", "MaxPower": "Exibe os itens que possuem o maior poder por slot.", "MaxPowerLoadout": "Exibe os itens no set que maximizarão seu nível de poder para cada classe de personagem.", "Memento": "Exibe armas que possuem um soquete de memento.", "ModSlot": "Shows armor with a specific mod type slot.", "Mods": { "Y3": "Shows items with any mods applied." }, "Name": "Mostra itens cujos nomes correspondam (exactname:) ou parcialmente correspondam (nome:) o texto do filtro. Procure por frases inteiras usando aspas.", "NamedStat": "Mostra armaduras que tem pontos no atributo nomeado.", "Negate": "Para negar uma pesquisa, prefixe o termo com um sinal de menos ou a palavra \"not\", por exemplo: \"{{notexample}}\" ou \"{{notexample2}}\".", "NewItems": "Mostra novos itens.", "Notes": "Procura por itens que você marcou com anotações personalizadas.", "OriginTrait": "Mostra armas que têm uma característica de origem.", "Ornament": "Mostra itens com ornamentos e filtros pra seus estados.", "PartialMatch": "Mostra itens onde seu nome, descrição, qualquer perk, ou qualquer mod que tenha uma correspondência parcial com o texto do filtro. Pesquise por frases inteiras usando aspas.", "PatternUnlocked": "Mostra itens que possuem um padrão desbloqueado, mesmo que o item em si não tenha sido criado.", "Perk": "Exibe itens onde um de seus perks ou mods tem uma correspondência parcial com seu nome ou descrição. Pesquise por frases inteiras usando aspas.", "PerkName": "Exibe itens com um perk ou mod cujo nome corresponda exatamente (exactperk:) ou parcialmente (perkname:) com o texto do filtro. Procure por frases inteiras usando aspas.", "PinnacleReward": "Exibe empreitadas que recompensam equipamentos pináculos.", "Postmaster": "Itens que estão no Chefe dos Correios atualmente.", "PowerKeywords": "Use o filtro \"pinnaclecap\" ou \"softcap\", ao invés de um número, para referir-se ao limite de poder da temporada atual.", "PowerLevel": "Exibe itens baseados em seu nível de poder. $t(Filter.PowerKeywords)", "PowerfulReward": "Exibe empreitadas que recompensam equipamentos poderosos.", "PrismaticDamageType": "Mostra itens baseados em se eles são um tipo de dano de luz ou treva. Tipos de luz são arco, solar e vácuo. Tipos de treva são estase e filamento.", "Quality": "Mostra itens baseado na porcentagem total da qualidade de seus atributos. '{{percentage}}' é um apelido pra '{{quality}}'.", "RandomRoll": "Mostra itens que caem com valores aleatórios.", "RarityTier": "Mostra itens baseado no nível de raridade deles.", "Reforgeable": "Mostra itens que podem ser reforjados no Armeiro.", "Release": "Shows items available from a specific release or event.", "RequiredLevel": "Mostra itens baseado em seu nível necessário.", "RetiredPerk": "Exibe armas com perks que não podem mais ser obtidos.", "SearchPrompt": "Pesquisa comandos disponíveis para filtros", "Season": "Mostra itens de que temporada apareceram no Destiny 2.", "StackFull": "Exibe itens que atingiram a capacidade máxima de acúmulo (Núcleos de Aprimoramento, Moedas Estranhas, Materiais do Armeiro, etc.)", "StackLevel": "Mostra itens baseados na quantidade de itens em sua pilha.", "Stackable": "Mostra itens que podem acumular (munições, moedas estranhas, etc)", "StatLower": "Exibe as armaduras nas quais seus atributos são necessariamente menores que as outras do mesmo tipo.", "Stats": "Mostra itens baseado em um atributo específico. $t(Filter.StatsExtras)", "StatsBase": "Filtra armaduras baseando-se no valor base do atributo, não incluindo mods anexados ou obra-prima. $t(Filter.StatsExtras)", "StatsExtras": "Suporta a adição de atributos conectando vários nomes de atributos (em inglês) com os símbolos + ou &. Há também palavras-chave especiais highest (mais alto), secondhighest (segundo mais alto), thirdhighest (terceiro mais alto), etc. que correspondem à uma classificação dentre os atributos de um item. Cada atributo customizado também tem seus próprios termos de busca, exibidos nas configurações de Atributos Customizados.", "StatsLoadout": "Encontra um conjunto de itens para equipar com valor máximo de um dado atributo.", "StatsMax": "Encontra armaduras com o maior número para um atributo específico. Inclui todos os itens com o atributo mais alto.", "StatsOrdinal": "Finds armor 3.0 with the specified stat focusing.", "Tags": { "Tag": "Exibe itens que possuem uma determinada etiqueta.", "Tagged": "Exibe itens que possuam qualquer etiqueta." }, "Tier": "Shows items based on their tier from 0-5.", "Timelost": "\\(Retroperda\\)", "Tracked": "Exibir jornadas/contratos baseados em seu status de rastreamento.", "Transferable": "Itens que podem ser movidos entre personagens.", "Trashlist": "Exibe itens que correspondem à sua lista de itens indesejáveis.", "TunedStat": "Shows items with tuning mods for the specified stat.", "Unascended": "Exibe itens que possuem nódulos ascendentes e que não foram ascendidos.", "Undo": "Desfazer", "UnlockAllFailed": "Falha ao destravar itens", "UnlockAllSuccess": "{{num}} itens destravados", "Vendor": "Itens que estão disponíveis através de um vendedor específico.", "VendorItem": "O item é de um vendedor, não está em seu inventário. Útil para excluir itens do fornecedor do Otimizador de Sets.", "Weapon": "Exibe itens que são armas.", "WeaponLevel": "Exibe armas baseando-se no seu nível de poder.", "WeaponType": "Exibir armas baseado no seu tipo de arma.", "Wishlist": "Exibe os itens que correspondem à sua lista de desejos.", "WishlistDupe": "Exibe os itens duplicados onde pelo menos uma das duplicatas faça parte da sua lista de desejos.", "WishlistEnabled": "Exibe os itens que são elegíveis para ter os valores na lista de desejos.", "WishlistNotes": "Exibe itens listados e desejados onde as notas combinam com a pesquisa.", "WishlistUnknown": "Exibe itens sem nenhuma recomendação de valor na lista de desejos atual.", "Year": "Exibir itens a partir de qual ano foi inserido em Destiny." }, "General": { "ClickForDetails": "Clique para detalhes", "Close": "Fechar", "Confirm": "Confirmar?", "UserGuideLink": "Guia do usuário" }, "Glyphs": { "Axe": "Machado", "DarkAbility": "Habilidade da Treva", "Gilded": "Dourado", "Harmonic": "Harmônico", "HiveSword": "Espada da Colmeia", "LightAbility": "Habilidade da Luz", "LightLevel": "Nível de Luz", "Misadventure": "Acidente", "Missing": "Ausente", "OpenSymbolsPicker": "Abrir Seletor de Símbolos", "Prismatic": "Prismático", "Quickfall": "Queda rápida", "RespawnRestricted": "Reaparecimento Restrito", "ScorchCannon": "Canhão Causticante", "SearchSymbols": "Buscar Símbolos...", "Smoke": "Fumaça" }, "Header": { "About": "Sobre o DIM", "AutoRefresh": "DIM atualizará automaticamente enquanto você estiver jogando.", "BulkTag": "Etiquetar itens em massa", "BungieNetAlert": "Alerta da Bungie", "Clear": "Limpar filtro de busca", "CompareMatching": "Comparar Itens", "DeleteSearch": "Apagar pesquisa", "FilterHelp": "Pesquisar item/perk, {{example}}, e mais", "FilterHelpBrief": "Procurar itens", "FilterHelpLoadouts": "Pesquisar por nomes de sets e anotações", "FilterHelpMenuItem": "Ajuda sobre filtros...", "FilterHelpOptimizer": "Filter armor included in builds, e.g.: {{example}}", "FilterHelpProgress": "Pesquisar marcos e contratos", "FilterHelpRecords": "Pesquisar em triunfos e coleções", "FilterMatchCount_one": "1 item", "FilterMatchCount_other": "{{count}} itens", "Filters": "Filtros", "InstallDIM": "Instalar como um App", "InstallDIMBanner": "Instale o DIM como um aplicativo em sua tela inicial", "Inventory": "Inventário", "IosPwaPrompt": "No Safari, clique no ícone de compartilhamento (botão do meio, na parte inferior) e selecione \"Adicionar à Tela Inicial\".", "KeyboardShortcuts": "Atalhos do teclado", "LaunchDIMAlone": "Em Janela Separada", "MaterialCounts": "Contagem de Materiais", "Menu": "Menu", "ProfileAge": "Os servidores do Destiny enviaram os dados atualizados pela última vez há {{age}}.\nAtualizar o DIM pode trazer dados mais recentes, mas o Bungie.net também pode repetir informações em cache.", "Refresh": "Atualizar dados do Destiny [R]", "ReloadApp": "Recarregar o App", "ReportBug": "Relatar Falhas", "SaveSearch": "Salvar pesquisa", "SearchActions": "Abrir Ações de Pesquisa", "SearchResults": "Mostrar Itens", "Shop": "Loja", "TagAs": "Marcar como '{{tag}}'", "UpgradeDIM": "Atualize o DIM", "WhatsNew": "Novidades" }, "Help": { "CannotMove": "Não é possível remover o item deste personagem.", "NoStorage": "DIM não pode salvar os dados", "NoStorageMessage": "DIM não pode armazenar dados no seu navegador. Isto pode ser causado pela navegação em modo privado ou anônimo, ou quando você tem pouco espaço em disco, ou um erro no navegador. Tente reiniciar seu computador! Você não será capaz de fazer login ou usar o DIM até corrigir isso." }, "Hotkey": { "Armory": "Mostrar Arsenal para um item", "CheatSheetTitle": "Teclas de Atalho:", "ClearDialog": "Dispensar diálogo", "ClearNewItems": "Limpar novos itens", "Enter": "ENTER", "ItemPopupTab": "Alternar aba de detalhes do item", "LockUnlock": "Travar ou destravar um item", "MarkItemAs": "Etiquetar item como '{{tag}}'", "Menu": "Alternar menu", "Note": "Inserir anotações", "Pull": "Trazer o item para o personagem ativo", "RefreshInventory": "Atualizar inventário", "ShowHotkeys": "Exibir teclas de atalho", "StartSearch": "Iniciar uma busca", "StartSearchClear": "Iniciar uma busca limpa", "Tab": "TAB", "Vault": "Enviar item para o cofre" }, "InGameLoadout": { "ClearSlot": "Limpar Slot {{index}}", "Create": "Criar set", "CreateTitle": "Criar Set no Jogo a partir do Equipamento Atual", "CurrentlyEquipped": "Equipado Atualmente", "DeleteFailed": "Falha ao excluir Set", "Deleted": "Set Excluído", "DeletedBody": "O Set no slot {{index}} foi limpo no jogo", "EditFailed": "Falha ao atualizar o set", "EditIdentifiers": "Editar Identificadores", "EditTitle": "Editar Nome e Ícone do Set", "EquipNotReady": "Equipar no Jogo Não Está Pronto", "EquipReady": "Equipar no Jogo Está Pronto", "LoadoutDetails": "Detalhes do set", "MatchingLoadouts": "Sets Correspondentes:", "PrepareEquip": "Preparar para Equipar", "Replace": "Substituir Set {{index}}", "Save": "Atualizar Set", "SaveIdentifiers": "Atualizar Identificadores", "SnapshotFailed": "Falha ao capturar o Set equipado" }, "Infusion": { "Filter": "Filtrar itens", "InfuseSource": "Selecione o item para infundir {{name}}", "InfuseTarget": "Selecione o item para infundir em {{name}}", "InfusionMaterials": "Materiais de infusão", "NoItems": "Nenhum item para infusão disponível.", "NoTransfer": "O material de infusão\n {{target}} não pode ser movido.", "SwitchDirection": "Inverter", "TransferItems": "Transferir" }, "Inventory": { "ClickToExpand": "(clique para expandir)", "MissingSilver": "Your Silver balance is only available while you are playing the game." }, "Item": { "SetBonus": { "NPiece_one": "{{count}} Piece", "NPiece_other": "{{count}} Piece" }, "ThumbsDown": "Polegar para Baixo", "ThumbsUp": "Polegar para Cima" }, "ItemFeed": { "ClearFeed": "Limpar lista", "Description": "Itens Recentes", "HideTagged": "Ocultar Etiquetados", "NoNewItems": "Nenhum item novo", "ShowOlderItems": "Exibir itens antigos" }, "ItemMove": { "Consolidate": "{{name}} consolidado", "Distributed": "{{name}} distribuído\n{{name}} está agora dividido por igual entre os personagens.", "MovingItem": "Transferir para o cofre", "MovingItem_female": "Transferir para a {{target}}", "MovingItem_male": "Transferir para a {{target}}", "ToStore": "Todos os {{name}} estão agora no seu {{store}}.", "ToVault": "Todos os {{name}} estão agora no seu cofre." }, "ItemPicker": { "ChooseItem": "Escolha um item:", "SearchPlaceholder": "Procurar itens" }, "ItemService": { "BucketFull": { "Guardian": "There are too many '{{itemtype}}' items on your {{store}}.", "Guardian_female": "There are too many '{{itemtype}}' items on your {{store}}.", "Guardian_male": "There are too many '{{itemtype}}' items on your {{store}}.", "Vault": "Há muitos '{{itemtype}}' no {{store}}." }, "Classified": "Este item é confidencial e não pode ser transferido no momento.", "Classified2": "Item secreto. Bungie ainda não fornece informações sobre este item. Adicione uma nota a este item e use o \"notas\" como filtro de busca para encontrá-lo.", "Deequip": "Não foi possível encontrar outro item para equipar no lugar de {{itemname}}", "ExoticError": "'{{itemname}}' não pode ser equipado porque o exótico no slot de {{slot}} não pode ser removido. ({{error}})", "NotEnoughRoom": "Não há nada que possamos mover do {{store}} para abrir espaço para {{itemname}}", "NotEnoughRoomGeneral": "Não há espaço suficiente para mover este item.", "OnlyEquippedClassLevel": "Este item só pode ser equipado em um {{class}} igual ou acima do nível {{level}}.", "OnlyEquippedLevel": "Este item só pode ser equipado em personagens igual ou acima do nível {{level}}.", "PostmasterAlmostFull": "Quase cheio!", "PostmasterFull": "Cheio!", "PreviewVendor": "Pré-visualizar conteúdo de {{type}}", "StackFull": "Você já tem uma pilha cheia de {{name}}", "StoreName": "{{className}} {{genderRace}}" }, "KillType": { "ClassAbilities": "Habilidade de Classe", "Finisher": "Finalização", "Grenade": "Granada", "Melee": "Corpo a corpo", "Precision": "Precisão", "Super": "Super" }, "LB": { "AddStack": "Adicionar outra cópia deste mod", "AdvancedOptions": "Opções avançadas", "ChooseAMod": "Escolha seus mods", "ChooseASetBonus": "Choose your set bonuses", "ChooseAnExotic": "Escolha seu exótico", "ClearLocked": "Limpar itens travados", "ContainsVendorItems": "Essa carga contém itens de vendedores", "Current": "Atual", "Equip": "Equipar em {{character}}", "Exclude": "Itens ignorados", "ExcludeHelp": "SHIFT+Clique em um item (ou arraste e solte no quadro abaixo) para construir sets ignorando itens específicos.", "ExistingBuildStats": "Atributos de Sets Existentes", "ExistingBuildStatsNote": "Only showing builds with strictly higher stats.", "FilterSets": "Filtrar atributos", "Help": { "And": "Armadura com todos esses perks será usada (\"e\")", "ChangeNodes": "Mude o Intelecto, Disciplina e Força dentro do jogo de acordo com o que é exibido ao criar cada set.", "Discipline": "Disciplina acelera o tempo de recarga de granadas", "DragAndDrop": "Arraste e solte itens nas caixas para construir sets somente com aquele equipamento", "Help": "Precisa de ajuda?", "HigherTiers": "Níveis mais altos são melhores", "Intellect": "Intelecto acelera o tempo de recarga da Super", "Lock": "Trave um conjunto de perks clicando na caixa e selecionando os perks", "MultiPerk": "Se você quiser usar uma armadura com múltiplos perks juntos, Shift+clique nos perks desejados", "NoPerk": "Se um perk não aparece, significa que você não tem armaduras com este perk", "Or": "Armadura com qualquer um destes perks será usada (\"ou\")", "ShiftClick": "Shift+clique em um item para construir sets sem aquele equipamento", "StatsIncrease": "Quando o nível de defesa de um item aumenta, os atributos deste item (intelecto, disciplina e força) também aumentam.", "Strength": "Força acelera o tempo de recarga do corpo-a-corpo", "Synergy": "Tente encontrar uma armadura com perks que aumentem a quantidade de munição das armas que você mais usa.", "Tier11Example": "4/5/2 (um set Nível 11) significa 4 Intelecto, 5 Disciplina, 2 Força (4+5+2 = Nível 11)" }, "HideAllConfigs": "Ocultar todas as configurações", "HideConfigs": "Ocultar configurações", "IncompatibleWithOptimizer": "Esse item é incompatível com o Otimizador. Por favor, pegue uma nova versão das Coleções.", "LB": "Otimizador de Set", "LightMode": { "HelpCurrent": "Calcular set com os níveis de defesa atuais.", "HelpScaled": "Calcular set como se todo os itens fossem 350 de defesa.", "LightMode": "Modo de luz" }, "Loading": "Carregando os melhores sets", "LockEquipped": "Travar itens equipados", "LockPerk": "Travar perk", "Locked": "Itens travados", "LockedHelp": "Arraste e solte itens para construir sets com itens específicos. SHIFT+Clique para excluir.", "Missing2": "Faltam itens raros, lendários ou exóticos para construir um set completo!", "ProcessingMode": { "Fast": "Rápido", "Full": "Completo", "HelpFast": "Busca apenas pelos seus melhores itens.", "HelpFull": "Busca por todos os itens, mas leva mais tempo.", "ProcessingMode": "Modo de processamento" }, "RemoveStack": "Remover uma cópia deste mod", "Scaled": "Nivelado", "SearchAMod": "Busca pelo nome ou descrição do mod", "SearchASetBonus": "", "SearchAnExotic": "Busca pelo nome ou descrição do exótico", "SelectExotic": "Selecionar exótico", "SelectMods": "Selecionar mods", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Mods de Atividade", "SelectSetBonus": "Select Set Bonuses", "SelectSubclassOptions": "Personalizar subclasse", "ShowAllConfigs": "Exibir todas as configurações", "ShowConfigs": "Exibir configurações", "ShowGear": "Armadura de {{class}}", "Vendor": "Incluir itens de vendedores" }, "Loading": { "Accounts": "Carregando contas de Destiny...", "Code": "Carregando código DIM...", "FilterHelp": "Carregando ajuda de busca...", "Profile": "Carregando perfil do Destiny...", "Vendors": "Carregando vendedores do Destiny..." }, "LoadoutAnalysis": { "Analyzed": "{{numLoadouts}} Sets Analisados", "Analyzing": "Analisando {{numAnalyzed}}/{{numLoadouts}} Sets", "BetterStatsAvailable": { "Description": "Choosing different armor or mods for this loadout will allow reaching higher stats. Choose \"$t(Loadouts.OpenInOptimizer)\" to view better builds.", "Name": "Melhores Atributos Disponíveis" }, "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat to exceed 200. DIM may identify better stats by reducing the amount of excess stats. If this is undesired, disable \"$t(Loadouts.IncludeRuntimeStatBenefits)\" in the Loadout.", "DoesNotRespectExotic": { "Description": "As configurações do Otimizador de Set deste set especificam uma escolha de exótica, mas o set não corresponde a esta exótica.", "Name": "Exótico Errado" }, "DoesNotSatisfyStatConstraints": { "Description": "Loadout Optimizer settings for this Loadout specify stat minimums, but the Loadout does not reach them.", "Name": "Wrong Stat Minimums" }, "EmptyFragmentSlots": { "Description": "Há slots de fragmento vazios nesta subclasse.", "Name": "Slots de Fragmento Vazios" }, "InvalidMods": { "Description": "Alguns mods neste set estão descontinuados ou não se encaixam em nenhuma das suas armaduras.", "Name": "Mods Descontinuados" }, "InvalidSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer that is not valid.", "Name": "Pesquisa Inválida" }, "ItemsDoNotMatchSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer, and that search query excludes at least one of the items in the loadout.", "Name": "Search Excludes Items" }, "MissingItems": { "Description": "Alguns itens deste set não estão mais em seu inventário.", "Name": "Itens Faltando" }, "ModsDontFit": { "Description": "Armaduras neste set não conseguem acomodar todos os mods de set, mesmo se a armadura fosse melhorada.", "Name": "Mods não Atribuídos" }, "NeedsArmorUpgrades": { "Description": "Armor in this loadout needs to be upgraded to accommodate all mods or reach specified stats.", "Name": "Precisa de Melhorias na Armadura" }, "NotAFullArmorSet": { "Description": "Esse set não pode ser mais analisado, pois não inclui um conjunto completo de armadura.", "Name": "Não é uma Armadura Completa" }, "TooManyFragments": { "Description": "Há mais fragmentos configurados na subclasse do que concedidos pelos aspectos.", "Name": "Muitos Fragmentos" }, "UsesSeasonalMods": { "Description": "Esse set depende de mods que só estão disponíveis em algumas temporadas. Quando a temporada terminar, alguns mods estarão indisponíveis ou excederão a capacidade de energia da armadura.", "Name": "Usa Mods de Temporada" } }, "LoadoutBuilder": { "All": "Todos", "AlwaysAutoMods": "Artifice and Tuning mods will always be chosen automatically.", "AnyExotic": "Qualquer exótico", "AnyExoticDescription": "O set deverá conter um exótico, mas qualquer exótico servirá.", "Artifice": "Artífice", "AssumeMasterwork": "Assumir Obra-prima", "AssumeMasterworkOptions": { "All": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nArmor 2.0 Exotics: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Melhorada para aceitar mods Artífice de atributos.", "Current": "Atributos atuais, nível de energia assumido de pelo menos {{minLoItemEnergy}}.", "Legendary": "Lendárias: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExóticas: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Full masterwork stat bonuses, assumed energy level at least 10.", "None": "Todas as Armaduras: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "Adiciona mods de atributo automaticamente", "AutomaticallyPicked": "Este mod foi adicionado automaticamente para melhorar os atributos do set.", "CompareLoadout": "Comparar Sets", "ConfirmOverwrite": "Você tem certeza que deseja substituir a armadura do set \"{{name}}\" com este novo conjunto de armaduras?", "DecreaseStatPriority": "Reduz prioridade de atributo", "DisabledByAutoStatMods": "Mods de atributos estão sendo escolhidos automaticamente pelo Otimizador de Set.", "DisabledDueToMaintenance": "O Otimizador de Set está indisponível temporariamente devido à manutenção da API da Bungie.", "EquipItems": "Equip", "ExcludeItem": "Ignorar item", "ExcludeVendors": "Search \"not:vendor\" to exclude vendor items from Loadout Optimizer.", "ExcludedItems": "Itens ignorados", "ExistingLoadout": "Set Existente", "Exotic": "Armadura Exótica", "ExoticClassItemPerks": "If you want specific perks, use searches like exactperk:\"spirit of verity\". Click perks in the Optimizer results to add or remove them from the item filter.", "ExoticSpecialCategory": "Especial", "FOTLWildcardWarning": "This set contains a Festival of the Lost mask. Manually apply the correct mod to activate desired set bonuses.", "Filter": "Configurações", "IgnoreStat": "Se desmarcado, o Otimizador de Sets vai fingir que este atributo não existe quando construir os sets", "IncreaseStatPriority": "Aumenta prioridade de atributo", "Legendary": "Lendária", "LimitToNewFeaturedGear": "Limit to new/featured gear", "LockItem": "Fixar item", "MissingClass": "Configuração é para: {{className}}", "MissingClassDescription": "A configuração que você está tentando ver é para uma classe de personagem que você não possui.", "MwExotic": "Exôtico", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "A pesquisa atual está restringindo os itens que o DIM pode incluir nas builds", "AllowAutoStatMods": "Permitir que o DIM automaticamente inclua mods de atributos adicionais", "AlwaysInvalidMods": "Estes mods não se encaixam em nenhum dos itens que você possui:", "AssumeMasterworked": "Permitir ao DIM recomendar a transformação de armaduras em obra-prima", "AssumptionsRestricted": "DIM não pode sugerir alterações no tipo de energia de armadura:", "BadSlot": "No slot {{bucketName}}, nenhum dos itens permitidos puderam acomodar estes mods:", "ExoticDoesNotExist": "You don't have any of the selected exotic armor in your inventory.", "Header": "Nenhuma build encontrada. Aqui estão os possíveis motivos que impediram o DIM de encontrá-las:", "LowerBoundsFailed": "Many sets did not meet minimum stat requirements", "MaybeAllowMoreItems": "Considere permitir outros itens:", "MaybeDecreaseLowerBounds": "Consider reducing minimum stat requirements", "MaybeRemoveMods": "Considere remover alguns mods:", "MaybeRemoveSearchQuery": "Considere limpar ou alterar o filtro na barra de pesquisa", "ModAssignmentFailed": "Vários sets não puderam acomodar todos os mods selecionados", "RemoveMods": "Remova estes mods", "RemoveSetBonuses": "Consider removing some set bonuses", "SetBonuses": "You have chosen some set bonuses, maybe you don't have the right items to use them." }, "NoExotic": "Nenhum exótico", "NoExoticDescription": "Equivalente à busca \"not:exotic\" na barra de pesquisa - sets não usarão nenhuma armadura exótica.", "NoExoticPreference": "Nenhum Exótico Selecionado", "NoExoticPreferenceDescription": "Armadura exótica será usada se maximizar os atributos.", "NoLoadoutsToCompare": "Nenhum Set para comparar", "None": "Nenhum", "OptimizerExplanationGuide": "Leia o Guia de Usuário para mais informações e um vídeo tutorial.", "OptimizerExplanationMods": "Choose an exotic, mods, and a subclass. These will contribute stats to the build, while any mods already on the armor are ignored.", "OptimizerExplanationSearch": "Use the search bar to narrow down which armor to consider, e.g. {{example}}. If no armor in a slot matches the search, all items will be considered for that slot.", "OptimizerExplanationStats": "Drag the most important stats to the top, and uncheck stats you don't want to maximize.", "OptimizerSet": "Set do Otimizador", "PinnedItems": "Itens Fixados", "PinnedItemsFinePrint": "Search filters are saved with Loadout Optimizer settings, but pinned and excluded items are not. Pins and exclusions will be ignored when DIM checks existing Loadouts for better stat builds.", "ProcessingSets": "Finding highest stat sets...", "SaveAs": "Salvar como", "SetBonus": "Set Bonuses", "SpeedReport": "Evaluated {{combos, number}} combinations in {{time}} seconds using {{cpus}} CPU cores.", "StatConstraints": "Stat Priorities & Ranges", "StatMax": "Máximo", "StatMin": "Min", "StatRangeTooltip": "With the current min/max setting, loadouts exist which have {{min}} to {{max}} points in this stat. Double-click to set min to {{max}}.", "StatTotal": "Total: {{total}}", "TierNumber": "N{{tier}}", "UnableToAddAllMods": "Não foi possível adicionar todos os mods.", "UnableToAddAllModsBody": "Não havia espaços de mods suficientes para caber {{mods}}.", "UnlockItem": "Desfixar item" }, "LoadoutFilter": { "Contains": "Exibe sets que possuem um item ou um mod correspondente ao texto do filtro. Pesquise itens com espaços em seu nome usando aspas.", "FashionOnly": "Mostra sets que contém apenas estilos (tonalizadores ou ornamentos).", "LoadoutLight": "Mostra os sets baseados em seu nível de luz calculado. Use a palavra-chave pinnaclecap ou softcap em vez de um número para se referir aos limites de poder da temporada atual.", "ModsOnly": "Mostra sets que contém apenas mods de armadura.", "Name": "Mostra sets cujos nomes correspondam (exactname:) ou correspondam parcialmente (name:) ao texto do filtro. Procure por frases inteiras utilizando aspas.", "Notes": "Procurar por sets pelo campo de notas.", "PartialMatch": "Exibe sets cujo nome ou notas coincidem parcialmente com o texto do filtro. Procure por frases inteiras usando aspas.", "Season": "Mostra sets por qual temporada do Destiny 2 eles foram modificados pela última vez.", "Subclass": "Exibe sets cujo nome da subclasse ou tipo de dano corresponda parcialmente o texto do filtro." }, "Loadouts": { "Abilities": "Habilidades", "Actions": "Ações para {{title}}", "AddEquippedItems": "Adicionar Equipado", "AddNotes": "Adicionar Anotações", "AddUnequippedItems": "Adicionar Não Equipados", "Any": "Qualquer classe", "Apply": "Aplicar", "ApplyInGameLoadoutInGame": "Seu arsenal está pronto para equipar, mas já que você está em uma atividade, você precisa equipá-lo dentro do jogo.", "ApplyMods": "Aplicando mods", "ApplySearch": "Transferir a busca \"{{query}}\"", "ArmorStats": "Atributos de Armadura", "ArtifactUnlocks": "Desbloqueado no Artefato", "ArtifactUnlocksDesc": "Devido a limitações da Bungie.net, DIM não pode configurar automaticamente seu artefato. Você precisa executar estes desbloqueios no jogo antes de aplicar o Arsenal.", "ArtifactUnlocksWithSeason": "Desbloqueado no Artefato – T{{seasonNumber}}", "BadLoadoutShare": "Não foi possível carregar este set compartilhado", "BadLoadoutShareBody": "O set que você está tentando carregar é inválido: {{error}}", "Before": "Antes de '{{name}}'", "CancelEditing": "Cancelar Edição", "CannotCustomizeSubclass": "Esta subclasse não pode ser configurada", "ChooseItem": "Adicionar {{name}}", "ClassType": "Set de qualquer classe", "ClassTypeMismatch": "Um item de {{className}} não pode ser adicionado neste set", "ClassTypeMissing": "Você não possui um {{className}} para criar um set para ele", "ClassType_female": "Set de {{className}}", "ClassType_male": "Set de {{className}}", "Classified": "Alguns de seus itens são secretos e não podem ser incluídos no cálculo do Poder máximo.", "ClearLoadoutParameters": "Remover configurações do Otimizador de Set", "ClearSection": "Remover todas", "ClearSpace": "Move outros para fora", "ClearSpaceArmor": "Move outras armaduras para fora", "ClearSpaceWeapons": "Move outras armas para fora", "ClearUnsetMods": "Remover outros mods", "ClearingSpace": "Movendo outros itens para fora", "CopyAndEdit": "Editar Cópia", "Create": "Criar set", "CurrentlyEquipped": "Equipado Atualmente", "Deequip": "Desequipando itens de outros personagens", "Delete": "Apagar", "DimLoadouts": "Sets no DIM", "Edit": "Editar set", "EditBrief": "Editar", "EquipInGameLoadout": "Equipando set do jogo", "EquipItems": "Equipando itens", "EquippableDifferent1": "Múltiplos itens exóticos foram usados para calcular seu Poder Máximo. Então o número mostrado pode não ser alcançável ao equipar seus itens no jogo.", "EquippableDifferent2": "O poder máximo não considera a regra de \"um exótico\" ao determinar o poder dos seus itens comuns, poderosos ou pináculos.", "Failed": "Ocorreu uma falha ao aplicar o set completamente", "Fashion": "Escolher estilo", "FashionOnly": "Somente-Estilos", "FillFromEquipped": "Preencher usando equipado", "FillFromInventory": "Preencher usando não equipado", "FilteredItems": "Itens filtrados", "FindAnother": "Encontrar outro {{name}}", "FromEquipped": "Equipado", "Generated": "{{statTotal}} Stat Point Loadout", "HashtagTip": "Dica: use #hashtags nos nomes ou anotações de seus sets e elas aparecerão aqui.", "Import": { "BadURL": "Não é uma URL de compartilhamento de set válida.", "Error": "Falha ao carregar o set:", "Error404": "Este set não existe.", "PasteHere": "Cole o link do set para abri-lo." }, "ImportLoadout": "Importar Set", "InGameActions": "Ações de Sets no Jogo", "InGameLoadouts": "Sets no Jogo", "IncludeRuntimeStatBenefits": "Incluir atributos de mods de Fonte", "IncludeRuntimeStatBenefitsDesc": "\"Fonte de ...\" mods de armadura fornecem um bônus base para atributos do personagem enquanto você tem Cargas de Armadura.\n\nCom essa configuração, o DIM considera esses mods ativos e adiciona seus benefícios aos atributos desse Set em cálculos e otimizações.", "ItemErrorSummary_one": "1 erro no item:", "ItemErrorSummary_other": "{{count}} erros no item:", "ItemLeveling": "Nivelamento de itens", "LoadoutName": "Nome do set", "LoadoutParameters": "Configurações do Otimizador de Set", "LoadoutParametersExotic": "Set deve incluir este exótico: {{exoticName}}", "LoadoutParametersQuery": "Itens devem obedecer este filtro de pesquisa", "LoadoutParametersStats": "Stat priorities and minimum/maximum stat ranges", "Loadouts": "Sets", "MakeRoom": "Abrir espaço para itens do Correio", "MakeRoomDone_female_one": "Finished making room for 1 Postmaster item by moving 1 item off of {{store}}.", "MakeRoomDone_female_other": "Finished making room for {{count}} Postmaster items by moving {{movedNum}} items off of {{store}}.", "MakeRoomDone_male_one": "Finished making room for 1 Postmaster item by moving 1 item off of {{store}}.", "MakeRoomDone_male_other": "Finished making room for {{count}} Postmaster items by moving {{movedNum}} items off of {{store}}.", "MakeRoomDone_one": "Finished making room for 1 Postmaster item by moving 1 item off of {{store}}.", "MakeRoomDone_other": "Finished making room for {{count}} Postmaster items by moving {{movedNum}} items off of {{store}}.", "MakeRoomError": "Incapaz de abrir espaço para todos os itens do Correio: {{error}}.", "ManageLoadouts": "Gerenciar Sets", "MaxSlots": "Você só pode ter {{slots}} {{bucketName}} em um set.", "MaximizeLight": "Maximizar Luz", "MaximizePower": "Poder Máximo", "MaximizeStat": "Maximizar atributos", "MissingItemsWarning": "Alguns itens deste set não estão mais em seu inventário.", "ModErrorSummary_one": "1 erro no mod:", "ModErrorSummary_other": "{{count}} erros no mod:", "ModPlacement": { "InvalidMods": "Mods Inválidos", "InvalidModsDesc_one": "1 mod não cabe em nenhuma peça de armadura.", "InvalidModsDesc_other": "{{count}} mods não cabem em nenhuma peça de armadura.", "ModPlacement": "Posicionamento de Mods", "StackableMod": "Empilhável", "UnassignedMods": "Mods não Atribuídos", "UnassignedModsDesc_one": "1 mod não coube devido à capacidade de energia insuficiente ou a falta de slots de mods. Aumento da energia na armadura selecionada não resolverá o problema.", "UnassignedModsDesc_other": "{{count}} não couberam devido à capacidade de energia insuficiente ou a falta de slots de mods. Aumento da energia na armadura selecionada não resolverá o problema.", "UnstackableMod": "Não Empilhável", "UpgradeCosts": "Custo de Melhorias", "UpgradeCostsDesc": "Algumas armaduras precisam de aumento da capacidade de energia para receber os mods solicitados. No total, estas melhorias custam:" }, "Mods": "Mods", "ModsOnly": "Somente-Mods", "MoveItems": "Movendo itens", "NoSpace": "Você está sem espaço no cofre e em quaisquer outros personagens.", "NoneMatch": "Nenhum dos seus sets combinaram com os filtros.", "NotStarted": "Aguardando para que as outras ações terminem ou uma atualização do inventário para finalizar o carregamento", "NotesPlaceholder": "Escreva algumas anotações sobre este set ou use #hashtags para categorizá-lo", "NotificationTitle": "Set: {{name}}", "OnWrongCharacterAdvice": "Clique aqui para encontrar os itens com maior Poder deste personagem.", "OnWrongCharacterWarning": "A armadura mais poderosa deste personagem está em outro personagem. Para garantir um maior Poder nos drops e recompensas pináculo, a armadura deve estar neste personagem ou no Cofre.", "OnlyItems": "Somente itens equipáveis, materiais e consumíveis podem ser adicionados em um set.", "OpenInOptimizer": "Otimizar armadura", "OpenOnStreamDeck": "Abrir no Stream Deck", "PickArmor": "Pegar Armadura", "PickMods": "Adicionar mods de armadura", "Prismatic": { "Aspect": "Aspecto Prismático", "Grenade": "Granada Prismática", "Melee": "Corpo-a-corpo Prismático", "Super": "Super Habilidade" }, "PullFromPostmaster": "Coletar do Chefe do Correio", "PullFromPostmasterError": "Não foi possível resgatar do Chefe do Correio: {{error}}.", "PullFromPostmasterGeneralError": "Não foi possível puxar todos os itens do Chefe do Correio.", "PullFromPostmasterNotification_female_one": "Movido um item do Chefe do Correio para seu {{store}}.", "PullFromPostmasterNotification_female_other": "Movido {{count}} itens do Chefe do Correio para seu {{store}}.", "PullFromPostmasterNotification_male_one": "Movido um item do Chefe do Correio para seu {{store}}.", "PullFromPostmasterNotification_male_other": "Movido {{count}} itens do Chefe do Correio para seu {{store}}.", "PullFromPostmasterNotification_one": "Movido um item do Chefe do Correio para seu {{store}}.", "PullFromPostmasterNotification_other": "Movido {{count}} itens do Chefe do Correio para seu {{store}}.", "PullFromPostmasterPopupTitle": "Resgatar do Chefe do Correio", "Random": "Aleatória", "Randomize": "Set Aleatório", "RandomizeButton": "Tornar aleatório", "RandomizeNew": "Criar Aleatório", "RandomizeQueryHint": "Dica: Primeiro, procure por itens para restringir quais itens podem ser escolhidos aleatoriamente.", "RandomizeSearch": "Tornar aleatório a partir da busca", "RandomizeSearchPrompt": "Tornar aleatório seus itens equipados a partir da busca \"{{query}}\"?", "Redo": "Refazer", "RestoreAllItems": "Todos os itens", "SalvationsEdgeMods": "Salvation's Edge Mods", "Save": "Salvar", "SaveAsDIM": "Salvar como Set DIM", "SaveAsNew": "Salvar como Novo", "SaveAsNewTooltip": "Manter o set original e salvar este como novo", "SaveDisabled": { "AlreadyExists": "Escolha um novo nome para o set.", "Empty": "O set está vazio.", "NoName": "O set precisa de um nome." }, "SaveLoadout": "Salvar Set", "Season": "Temporada {{season}}", "SetBonusesDesc": "Required set bonuses", "Share": { "Copied": "Link de compartilhamento copiado para a área de transferência", "CopyButton": "Copiar Link", "Error": "Erro ao obter o link de compartilhamento", "Fashion": "Estilo (tonalizadores e ornamentos)", "LoadoutOptimizer": "Configurações do Otimizador de Set", "NativeShare": "Compartilhar Link", "Notes": "Anotações", "NumItems_one": "{{count}} item - será solicitado aos destinatários escolher um item comparável ao inventário deles", "NumItems_other": "{{count}} itens - será solicitado aos destinatários escolher um item comparável ao inventário deles", "NumMods_one": "{{count}} mod", "NumMods_other": "{{count}} mods", "Placeholder": "Carregando link de compartilhamento", "Subclass": "Personalização de subclasse", "Summary": "Compartilhar este set contendo:", "Title": "Compartilhar \"{{name}}\"" }, "ShareLoadout": "Compartilhar", "ShowModPlacement": "Exibir Localização do Mod", "Snapshot": "Salvar como Set no jogo", "SocketOverrides": "Alterando opções de subclasse", "SortByEditTime": "Ordenar pela última alteração", "SortByName": "Ordenar por nome", "SubclassOptions": "Opções da {{subclass}}", "SubclassOptionsSearch": "Pesquisa as opções da {{subclass}}", "Succeeded": "Set aplicado com sucesso", "SyncFromEquipped": "Sincronizar a partir dos equipados", "TooManyRequested": "Você tem {{total}} {{itemname}} mas o seu set busca por {{requested}}. Nós transferimos tudo o que você tem.", "TuningMods": "Tuning Mods", "UnassignedModError": "Mod incompatível com a armadura atual", "Undo": "Desfazer", "Update": "Salvar Alterações", "UpdateLoadout": "Atualizar Set", "VendorsCannotEquip": "Você não tem esses itens. Toque para pegar um substituto ou clique no X para remover:" }, "Manifest": { "Download": "Baixando últimas informações da Bungie referentes à base de dados de Destiny...", "Error": "Erro durante o carregamento da base de dados de Destiny:\n{{error}}\nRecarrege para tentar novamente.", "Load": "Carregando informações da base de dados de Destiny..." }, "Milestone": { "Daily": "Desafio Diário", "OneTime": "Desafio Único", "SeasonalRank": "Rank de Temporada {{rank}}", "Special": "Desafio de Evento Especial", "Tutorial": "Desafio de Tutorial", "Unknown": "Desafio", "Weekly": "Desafio Semanal" }, "Mods": { "HarmonicModDescription": "O efeito deste mod vem a um custo reduzido e muda o elemento dependendo da subclasse equipada." }, "MoveAmount": { "Amount": "Quantidade:" }, "MovePopup": { "Acquired": "Este item está desbloqueado em coleções.", "AcquiredMod": "Este mod está desbloqueado nas coleções.", "AddNote": "Adicionar anotações", "AddToLoadout": "Set", "AddToLoadoutTitle": "Adicionar isto a um set", "All": "Todos", "ArtifactBreaker": "This weapon has {{breaker}} because of an unlocked artifact perk.", "CannotCurrentlyRoll": "Este perk não pode ser utilizado na versão atual deste item.", "CantPullFromPostmaster": "Você deve visitar o postmaster no jogo para recuperar este item.", "CatalystProgress": "Progresso do Catalisador", "CommunityData": "Conhecimento da Comunidade", "Consolidate": "Consolidar", "DistributeEvenly": "Distribuir igualmente", "EnhancementTier": "Nível {{tier}}", "Equip": "Equipar em:", "EquipWithName": "Equipar em {{character}}", "FavoriteUnFavorite": { "Favorite": "Favoritar {{itemType}}", "Favorited": "Favoritado", "Unfavorite": "Desfavoritar {{itemType}}", "Unfavorited": "Desfavoritado" }, "Infuse": "Infundir", "InfuseTitle": "Abrir o buscador de itens para infusão", "IntrinsicBreaker": "This weapon intrinsically has {{breaker}}.", "LoadingSockets": "Perk e detalhes de atributo ainda não foram carregados para este item.", "LockUnlock": { "AutoLock": "Estado do bloqueio está sincronizado com a etiqueta deste item", "Lock": "Travar {{itemType}}", "Locked": "Travado", "Unlock": "Destravar {{itemType}}", "Unlocked": "Desbloqueado" }, "MissingSockets": "Detalhes de perks e mods estão indisponíveis enquanto a Bungie está atualizando os seus servidores. Eles retornarão quando tiverem terminado de atualizar, geralmente em algumas horas.", "Notes": "Anotações:", "OpenOnStreamDeck": "Abrir no Stream Deck", "OverviewTab": "Visão geral", "Owned": "Este item está em seu inventário.", "OwnedMod": "Este mod está na parte de modificações do seu inventário.", "PullItem": "Mover de {{bucket}} para {{store}}", "PullPostmaster": "Resgatar do Chefe do Correio", "ReadLore": "Leia a história no Ishtar Collective (somente em inglês)", "ReadLoreLink": "Leia a história", "Rewards": "Recompensas:", "SendToVault": "Enviar para o Cofre", "Store": "Mover para:", "StoreWithName": "Mover para {{character}}", "Subtitle": { "QuestProgress": "Etapa {{questStepNum}} de {{questStepsTotal}}", "Type": "{{classType}} {{typeName}}" }, "TabList": "Abas de detalhe do item", "ToggleSidecar": "Expandir ou recolher ações do item", "TrackUntrack": { "Track": "Seguir {{itemType}}", "Tracked": "Rastreado", "Untrack": "Não seguir {{itemType}}", "Untracked": "Não rastreado" }, "TriageTab": "Triagem", "UnreliablePerkOption": "Este perk aparece apenas na visualização de coleções. Pode não ocorrer aleatoriamente neste item.", "Vault": "Cofre", "WeaponLevel": "Nível da Arma {{level}}" }, "Notes": { "Error": "Erro! Anotações devem ter até 120 caracteres.", "Help": "Adicionar anotações, #hashtags ou :symbols:" }, "Notification": { "Cancel": "Cancelar", "OK": "Descartar" }, "Objectives": { "Complete": "Completo", "Incomplete": "Incompleto" }, "Organizer": { "BulkMove": "Mover Para", "BulkMoveLoadoutName": "Selecionado no Organizador", "BulkTag": "Etiqueta", "Columns": { "Ammo": "Ammo", "Archetype": "Arquétipo", "BaseStats": "Atributos base", "Breaker": "Quebra-guarda", "Crafted": "Data Formação", "CustomTotal": "Total Personalizado", "Damage": "Dano", "Energy": "Energia", "Event": "Evento", "Featured": "New Gear", "Foundry": "Fundição", "Frame": "Frame", "Harmonizable": "Harmonizável", "Holofoil": "Holofoil", "Icon": "Ícone", "ItemTier": "Tier", "KillTracker": "Kills", "Level": "Nível", "Loadouts": "Sets", "Location": "Armazenado em", "Locked": "Travado", "MasterworkStat": "MW Stat", "MasterworkTier": "MW Tier", "ModSlot": "Slot de mod", "Mods": "Mods", "Name": "Nome", "New": "Novo", "Notes": "Anotações", "OriginTraits": "Característica de Origem", "OtherPerks": "Weapon Components", "PercentComplete": "% Completo", "Perks": "Perks", "PerksGrid": "Perks Grid", "Power": "Poder", "Quality": "Qualidade %", "Recency": "Recência", "Season": "Temporada", "Shaders": "Cosméticos", "Source": "Fonte", "StatQuality": "Qualidade dos Atributos", "StatQualityStat": "{{stat}}%", "Stats": "Atributos", "Tag": "Etiqueta", "TertiaryStat": "3rd Stat", "Tier": "Rarity", "Traits": "Atributos de Arma", "TuningStat": "Tuner", "WishList": "Lista de Desejos", "WishListNotes": "Anotações da lista de desejos", "Year": "Ano" }, "EnabledColumns": "Colunas Habilitadas", "Lock": "Travar", "NoItems": "Nenhum item corresponde aos filtros. Se você tiver uma pesquisa, tente limpá-la.", "NoMobile": "Vire seu celular de lado para usar o Organizador.", "Note": "Definir Anotações", "OpenIn": "Mostrar no Organizador", "Organizer": "Organizador", "SelectAll": "Selecionar todos", "SelectItem": "Selecionar ou desmarcar {{name}}", "ShiftTip": "Dica: segure shift e clique em uma célula para filtrar os itens", "Stats": { "Aim": "Mira", "Airborne": "No ar", "AmmoGeneration": "Ammo Gen", "Power": "Poder", "RPM": "RPM", "Recoil": "Recuo", "Reload": "Recarregamento" }, "Unlock": "Destravar" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "O Chefe do Correio está quase cheio! ({{number}}/{{postmasterSize}})", "PostmasterFull": "O Chefe do Correio está cheio! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "Contratos", "CatalystSource": "Source: {{source}}", "CrucibleRank": "Ranque", "Items": "Itens de Jornada", "Milestones": "Marcos e Desafios", "NoEventChallenges": "Você concluiu todos os desafios do evento", "NoTrackedTriumph": "Você não está rastreando triunfos. Rastreie quantos quiser no DIM.", "PaleHeartPathfinder": "Desbravamento do Coração Pálido", "PercentMax": "{{pct}}% do máximo", "PercentPrestige": "{{pct}}% para resetar", "PointsUsed_one": "1 ponto usado", "PointsUsed_other": "{{count}} pontos usados", "PowerBonusHeader": "Recompensas Poderosas +{{powerBonus}}", "PowerBonusHeaderUndefined": "Outras Recompensas", "Progress": "Progresso", "QueryFilteredTrackedTriumphs": "Nenhum dos triunfos que você está rastreando coincidiram com a pesquisa", "QuestExpired": "Expirado", "QuestExpires": "Expira em ", "Quests": "Missões", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}pts", "Resets_one": "1 reset", "Resets_other": "{{count}} resets", "RewardPassEndsIn": "Reward Pass ends in ", "RewardPassPrestigeRank": "Prestige Rank {{rank}}", "SeasonalHub": "Seasonal Hub", "StatTrackers": "Estatísticas", "TrackedTriumphs": "Triunfos Rastreados" }, "RecordBooks": { "HideCompleted": "Esconder os recordes completados", "RecordBooks": "Livro de Recordes" }, "Records": { "Title": "Registros", "UniversalOrnamentSetOther": "Outros" }, "SearchHistory": { "Date": "Último uso", "DeleteAll": "Descartar todas as pesquisas não-favoritadas", "Description": "Aqui está todo seu passado e pesquisas salvas. Você pode deletá-las se precisar.", "Item": "Pesquisas de Itens", "Link": "Visualizar e editar o histórico de pesquisa", "Loadout": "Pesquisas de Sets", "Query": "Termo pesquisado", "Title": "Histórico de Pesquisa", "UsageCount": "Nº de Usos" }, "Settings": { "Appearance": "Appearance", "ArmorArchetypeModslot": "Armor Archetype / Modslot", "AutoLockTagged": "Sincronizar estado de bloqueio de item com etiqueta", "AutoLockTaggedExplanation": "DIM irá travar e destravar automaticamente os itens para que combinem com suas etiquetas. Itens criados permanecerão desbloqueados para permitir sua remodelagem. Quando esta configuração estiver ativada, o ícone de bloqueio não será mostrado na imagem do item para itens marcados.", "BadgePostmaster": "Exibir a quantidade de itens disponíveis com o Chefe do Correio do personagem atual no ícone do aplicativo", "BadgePostmasterExplanation": "Esta opção só funcionará se você instalar o DIM como um app e seu Sistema Operacional suportar este recurso de exibição", "BothDescriptions": "Ambas as Descrições", "BungieDescriptionOnly": "Descrições da Bungie", "CharacterOrder": "Ordenar personagens por", "CharacterOrderFixed": "Idade do personagem (buggy no PC)", "CharacterOrderRecent": "Personagem mais recente", "CharacterOrderReversed": "Personagem mais recente (inverso)", "ColumnSize": "{{num}} itens", "ColumnSizeAuto": "Auto", "CommunityData": "Conhecimento da Comunidade sobre Perks", "CommunityDescriptionOnly": "Descrições da Comunidade", "CsvImport": "Importar CSV", "CustomErrorLabel": "O nome do atributo deve conter caracteres de palavra, e ser diferente dos outros nomes de atributo para esta classe de Guardião.", "CustomErrorValues": "O peso do atributo deve ser um número positivo.\nO peso de pelo menos dois atributos deve ser acima de zero.", "CustomStatChooseName": "Escolha um nome para o Atributo Personalizado", "CustomStatCreate": "Crie um novo atributo personalizado", "CustomStatDelete": "Apagar este Atributo Customizado", "CustomStatDeleteConfirm": "Apagar este Atributo Customizado?", "CustomStatDesc1": "Escolha os atributos de armadura desejados para criar o total do atributo personalizado.", "CustomStatDesc3": "Atributos customizados aparecerão no Popup do Item, Organizador, e Comparação.", "CustomStatTitle": "Total do Atributo Personalizado", "Data": "Planilhas", "DefaultItemSizeNote": "Um item de tamanho 50px vai parecer mais nítido, sem borrar a imagem ou o texto do item.", "DontForgetDupes": "Não se esqueça que você pode buscar por is:dupe ou is:dupelower para localizar rapidamente itens duplicados, e você pode usar a ferramenta de comparação ou o organizador para avaliar itens relacionados.", "EnableAdvancedStats": "Mostrar a avaliação de estatísticas de qualidade em armaduras (D1)", "ExpandSingleCharacter": "Exibir todos os personagens", "ExportLoadoutSS": "Planilhas de Sets", "ExportLoadoutSSHelp": "Baixe uma lista CSV de seus Sets DIM que pode ser facilmente vista no aplicativo de planilhas de sua escolha.", "ExportProfile": "Exportar a resposta da API com os dados do perfil", "ExportSS": "Planilhas do inventário", "ExportSSHelp": "Baixe a planilha CSV de todos os seus itens para que você possa visualizar no software de sua preferência.", "HidePullFromPostmaster": "Ocultar o botão \"$t(Loadouts.PullFromPostmaster)\"", "Inventory": "Exibição Inventário", "InventoryColumns": "Largura do inventário de personagem", "InventoryColumnsMobile": "Tamanho do inventário do personagem em dispositivos móveis em modo retrato", "InventoryColumnsMobileLine2": "Os itens serão redimensionados para acomodar a nova configuração", "InventoryNumberOfSpacesToClear": "Number of empty spaces to make when using Farming Mode", "Items": "Exibição dos Itens", "Language": "Idioma", "LogOut": "Sair", "Masterworked": "Obra-prima", "MaxParallelCores": "Maximum cores for parallel tasks", "MaxParallelCoresExplanation": "Controls how many CPU cores DIM can use for intensive tasks like Loadout Optimizer and Loadout Analyzer. Higher values may improve performance but use more system resources.", "OrnamentDisplay": "Show Ornaments on item tiles", "OrnamentDisplayExplanationDisabled": "Items will never display their ornaments", "OrnamentDisplayExplanationEnabled": "Hovering or long-pressing armor will hide its ornament", "OrnamentDisplayExplanationHide": "Hovering or long-pressing an item will hide its ornament", "OrnamentDisplayExplanationShow": "Hovering or long-pressing an item will show its ornament", "ResetToDefault": "Resetar", "RestoreVaultSide": "Show vaulted items in their own column", "ReverseSort": "Alterar ordem para frente/trás", "SetSort": "Ordenar itens por:", "SetVaultWeaponGrouping": "Agrupar armas no cofre por:", "Settings": "Configurações", "ShowNewItems": "Exibe um ponto vermelho nos itens novos", "SingleCharacter": "Visão de Personagem-Ativo", "SingleCharacterExplanation": "DIM exibirá apenas o personagem jogado mais recentemente.\nItens guardados pelos personagens ocultos aparecerão no Cofre, caso possam ser usados pelo personagem atual.\nItens específicos para outras classes serão totalmente ocultados.", "SizeItem": "Tamanho do item", "SortByAmmoType": "Tipo de Munição", "SortByAmount": "Tamanho da Pilha", "SortByClassType": "Classe Requerida", "SortByCrafted": "Produzida (D2)", "SortByDeepsight": "Visão Penetrante (D2)", "SortByFeatured": "New Gear / Featured (D2)", "SortByPrimary": "Nível de poder", "SortByRarity": "Raridade", "SortByRating": "Qualidade da Armadura (D1)", "SortByRecent": "Obtido recentemente (D2)", "SortBySeason": "Temporada (D2)", "SortByTag": "Etiqueta ({{taglist}})", "SortByTier": "Tier (D2)", "SortByType": "Tipo", "SortByWeaponElement": "Tipo de Dano", "SortCustom": "Ordenação Personalizada", "SortName": "Nome", "SpacesSize_one": "{{count}} space", "SpacesSize_other": "{{count}} spaces", "Theme": "Tema", "Troubleshooting": "Troubleshooting", "VaultArmorGroupingStyle": "Separe as armaduras em linhas diferentes por classe", "VaultGroupingNone": "Nenhum", "VaultUnder": "Show vaulted items under equipped items", "VaultWeaponGroupingStyle": "Separe grupos de armas em linhas diferentes", "WeaponFrame": "Weapon Frame", "WishlistRefreshNotificationBody": "Se você não vê nenhuma atualização, certifique-se de que a fonte (como o GitHub) as reflete!", "WishlistRefreshNotificationTitle": "Lista de Desejos Recarregada" }, "Sockets": { "ApplyPerks": "Aplicar perks", "GridStyle": "Exibir perks em grades", "Insert": { "Ability": "Equipar Habilidade", "Aspect": "Inserir Aspecto", "Fragment": "Inserir Fragmento", "Mod": "Inserir Mod", "Ornament": "Aplicar Ornamento", "Projection": "Aplicar Projeção de Fantasma", "Shader": "Aplicar Tonalizador", "Super": "Equipar Super", "Transmat": "Aplicar Efeito de Transmaterialização" }, "ListStyle": "Exibir perks em listagem", "Search": "Pesquisar por nome ou descrição", "Select": { "Ability": "Pré-visualizar Habilidade", "Aspect": "Pré-visualizar Aspecto", "Fragment": "Pré-visualizar Fragmento", "Mod": "Pré-visualizar Mod", "Ornament": "Pré-visualizar Ornamento", "Projection": "Pré-visualizar Projeção de Fantasma", "Shader": "Pré-visualizar Tonalizador", "Super": "Pré-visualizar Super", "Transmat": "Pré-visualizar Efeito de Transmaterialização" }, "SelectWishlistPerks": "Pré-visualizar lista de desejos de perks" }, "Stats": { "CrouchingSpeed": "Agachado", "Custom": "Total Personalizado", "CustomDesc": "Total personalizado de atributos base selecionados, ignorando mods e obras-primas. Acesse as Configurações para modificar quais atributos serão incluídos.", "DamageResistance": "Resistência à Dano no PvE", "Discipline": "Disciplina", "DropLevel": "Poder da Conta", "DropLevelExplanation1": "Account Power is the base power level when calculating the increased level of rewards.", "DropLevelExplanation2": "O Poder da conta usa o item de nível mais alto em cada slot, independentemente da Classe requerida ou da regra \"Um Exótico\".", "EquippableGear": "Equipamento Equipável", "FlinchResistance": "Resistência à Oscilação causada por disparos", "HP": "Vida", "Intellect": "Intelecto", "MaxGearPower": "Poder máximo de itens equipáveis", "MaxGearPowerAll": "Poder máximo de todos os equipamentos", "MaxGearPowerOneExoticRule": "Poder Máximo do equipamento equipável\n(somente uma peça de armadura Exótica equipada)", "MaxTotalPower": "Poder máximo total", "MetersPerSecond": "m/s", "Milliseconds": "ms", "NoBonus": "Sem bônus", "NotApplicable": "N/D", "OfMaxRoll": "{{range}} do valor máximo", "PercentHelp": "Clique para mais informações sobre Níveis de Qualidade.", "Percentage": "%", "PowerModifier": "Poder concedido pela progressão de experiência sazonal", "Prestige": "Nível de prestígio: {{level}}\n{{exp}}xp para 5 fragmentos de luz.", "Quality": "Níveis de qualidade", "ShieldHP": "Vida do Escudo", "StrafingSpeed": "Movimentação Lateral", "Strength": "Força", "TierProgress": "N{{tier}} {{statName}} ({{progress}}/60 para N{{nextTier}})\n", "TierProgress_Max": "N{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "Tempo para Completar Vida", "Total": "Total", "TotalHP": "Vida Total", "WalkingSpeed": "Caminhando", "WeaponPart": "Parte da Arma" }, "Storage": { "ApiPermissionPrompt": { "Description": "Agora o DIM é capaz de armazenar suas configurações, etiquetas e sets em servidores próprios, permitindo sincronizar estes dados entre diferentes versões do DIM, sem login separado. Você pode importar seus dados atuais a partir da página de Configurações, caso não tenha ativado a Sincronização DIM anteriormente. Este feito só foi possível devido aos nossos apoiadores do OpenCollective!", "No": "Por enquanto, não", "Title": "Ativar Sincronização DIM?", "Yes": "Ativar sincronização" }, "AutoBackup": "Nós fizemos o backup dos seus dados para um arquivo em sua pasta de downloads chamado dim-data.json, só por precaução.", "BackUpFirst": "Você DEVE fazer o backup dos seus dados atuais antes de apagar tudo. Só por precaução.", "BrowserMayClearData": "O navegador pode apagar esses dados se você ficar sem espaço ou não visitar DIM frequentemente.", "DataIsLocal": "Dados de etiquetas e anotações são armazenados localmente", "DeleteAllData": "Remover TODOS os dados dos Servidores de Sincronização DIM", "DeleteAllDataConfirm": "Tem certeza que TODOS os seus dados devem ser removidos, de todas as contas, da Sincronização DIM? Não dá pra voltar atrás.", "Details": { "IndexedDBStorage": "Armazenamento local salvará suas informações apenas neste navegador. Limpar seus dados de navegação apagará essas informações." }, "DimApiFinePrint": "DIM salvará suas configurações, etiquetas e sets em servidores próprios e sincronizará entre diferentes versões do aplicativo.", "DimSyncDown": "A Sincronização DIM está indisponível devido a um problema de comunicação com o servidor.", "DimSyncEnabled": "Sincronização DIM ativada", "DimSyncNotEnabled": "A Sincronização DIM está desativada, portanto, suas configurações, etiquetas, sets e históricos de pesquisas só serão armazenados localmente e se perderão caso você limpe o armazenamento do seu navegador. Ative a Sincronização DIM nas configurações para guardar seus dados automaticamente ou faça backups manual regularmente.", "EnableDimApi": "Ativar a Sincronização DIM (recomendado)", "Export": "Baixar Dados de Backup", "ExportError": "Falha ao baixar o backup da Sincronização DIM", "ExportErrorBody": "A Sincronização DIM pode estar fora do ar no momento ou você pode estar enfrentando problemas de conexão. Iremos utilizar uma cópia dos seus dados salvos localmente, enquanto isso.", "Import": "Importar Dados de Backup", "ImportConfirmDimApi": "Você confirma que quer substituir suas etiquetas, sets e configurações atuais com esta versão? Esta opção sobrescreverá totalmente tudo o que você tiver atualmente.", "ImportExport": "Backup e Importação", "ImportFailed": "Importação Falhou! {{error}}", "ImportNoFile": "Nenhum arquivo selecionado!", "ImportNotification": { "FailedBody": "Não foi possível importar os dados. {{error}}", "FailedTitle": "Falha na importação", "NoData": "Nenhum set ou etiqueta encontrado no backup", "SuccessBodyForced": "Dados de configurações, {{tags}} etiquetas e {{loadouts}} sets foram importados de seu backup para a Sincronização DIM, substituindo o que havia por lá.", "SuccessBodyLocal": "Importamos as configurações, {{loadouts}} sets e {{tags}} itens com etiquetas do seu backup para o armazenamento local, substituindo o que já existia. Não podemos garantir que o armazenamento local não seja perdido, então considere ativar a Sincronização DIM.", "SuccessTitle": "Sucesso ao Importar" }, "ImportTooManyFiles": "Por favor selecione somente um arquivo para importar.", "ImportWrongFileType": "Arquivo não é um arquivo JSON. Pode não ser um backup do DIM.", "IndexedDBStorage": "Armazenamento local do Navegador", "LearnMore": "Saiba mais sobre a Sincronização DIM", "MenuTitle": "Sincronização e Backups", "ProfileErrorBody": "Ocorreu um problema durante a comunicação com a Sincronização DIM. Suas últimas configurações, etiquetas, sets e históricos de pesquisas podem não estar disponíveis no momento. Entretanto, seus dados ainda estão em nossos servidores e qualquer modificação que fizer serão salvas assim que pudermos reestabelecer a conexão. Tentaremos reconectar enquanto o DIM estiver aberto.", "ProfileErrorTitle": "Erro ao carregar a Sincronização DIM", "RefreshDimSync": "Recarregar dados remotos do DIM Sync", "UpdateErrorBody": "Ocorreu um problema ao salvar os seus dados na Sincronização DIM. Tentaremos novamente enquanto o DIM estiver aberto.", "UpdateErrorTitle": "Erro ao salvar a Sincronização DIM", "UpdateInvalid": "Falha ao salvar os dados para o DIM Sync", "UpdateInvalidBody": "Os dados enviados para o DIM Sync são inválidos e não serão salvos.", "UpdateInvalidBodyLoadout": "O set \"{{name}}\" é inválido e não será salvo. Se você o importou de outro site, por favor, deixe-os saber que eles estão exportando sets inválidos.", "UpdateQueueLength_one": "{{count}} alterações serão salvas assim que for possível reconectar.", "UpdateQueueLength_other": "{{count}} alteração será salva assim que for possível reconectar.", "Usage": "DIM está usando {{usage, humanBytes}} de {{quota, humanBytes}} disponíveis neste dispositivo. Isso inclui as bases de dados de itens do Destiny da Bungie.net." }, "StreamDeck": { "Authorize": "Conectar aplicativo", "Enable": "Plugin do Stream Deck", "Error": { "Body": "There was an error sending data to the Stream Deck plugin. Please contact the plugin developer. {{error}}", "Title": "Stream Deck Plugin Error" }, "FinePrint": "Permite a conexão com o plugin DIM Stream Deck. Este plugin pertence a um projeto independente que não foi escrito e nem é suportado pela equipe DIM.", "Install": "Instalar plugin", "MissingAuthorization": "Você deve autorizar o aplicativo Stream Deck para se conectar ao DIM. Vá para configurações e clique em \"Conectar aplicativo\".", "Tooltip": { "Application": "Aplicativo Stream Deck", "AuthRequired": "Clique neste botão ou vá para configurações e clique em \"Conectar aplicativo\".", "Error": "Seu plugin do Stream Deck não é mais suportado. Favor atualize para a versão mais recente. Este plugin requer pelo menos:", "ErrorConnection": "se você já está usando a versão mais recente, verifique se alguma extensão de navegador está bloqueando a conexão.", "ExtensionIssue": "Problema com Extensões", "Plugin": "Plugin", "Title": "Plugin DIM Stream Deck", "Version": "Versão:" } }, "StripSockets": { "Action": "Limpar Soquetes", "ArmorMods": "{{count}}x Mod de Armaduras", "Button": "Limpar {{numSockets}} Soquetes", "Cancel": "Cancelar", "Choose": "Escolha os Soquetes para limpar", "DiscountedMods": "{{count}}x Mod Descontados", "Done": "Soquetes limpos", "NoSockets": "Nenhum soquete para limpar", "Ok": "Ok", "Ornaments": "{{count}}x Ornamento", "Others": "{{count}}x Projeção de Fantasmas", "Running": "Limpando soquetes", "Shaders": "{{count}}x Tonalizador", "Subclass": "{{count}}x Opção de Subclasse", "WeaponMods": "{{count}}x Mod de Armas" }, "Tags": { "Archive": "Arquivar", "ClearTag": "Limpar Tag", "Favorite": "Favorito", "Infuse": "Infundir", "Junk": "Lixo", "Keep": "Manter", "LockAll": "Travar Itens", "TagItem": "Etiquetar como...", "UnlockAll": "Destravar itens" }, "Triage": { "AccountsForArtifice": "Isto testa se uma peça de armadura Artífice poderia ser melhor, caso um mod de +3 atributo fosse usado.", "BetterArmor": "Armadura Estritamente Melhor", "BetterArtificeArmor": "Armadura Artífice Melhor", "BetterStatArmor": "Armadura com Melhores Atributos", "BetterStatArtificeArmor": "Armadura Artífice com Melhores Atributos", "BetterWorseArmor": "Armadura Pior/Melhor", "BetterWorseIncludes": "Identifica peças de armaduras com:", "HighStats": "Atributos Altos", "InLoadouts": "Em Sets", "OwnedCount": "# Possuído", "PerkBetterArmorDesc": "O mesmo, ou mais, perks intrínsecas ou slots especiais de mods.", "PerkWorseArmorDesc": "O mesmo perk intrínseco, ou nenhum.", "SimilarItems": "Itens Similares", "StatBetterArmorDesc": "Todos os atributos são tão altos quanto, e pelo menos um atributo melhor.", "StatNotPerkArmorDesc": "Isto testa somente os atributos. Uma peça inferior ainda pode ter slots especiais ou perks intrínsecos.", "StatWorseArmorDesc": "Nenhum atributo melhor, e pelo menos um atributo pior.", "ThisItem": "Este item", "WorseArmor": "Armadura Estritamente Pior", "WorseArtificeArmor": "Pior Armadura Não-Artífice", "WorseStatArmor": "Armadura com Atributos Piores", "WorseStatArtificeArmor": "Armadura Não Artifície com Atributos Piores", "YourBestItem": "Seu melhor item" }, "Triumphs": { "GildingTriumph": "Triunfo para Dourar", "HideCompleted": "Ocultar triunfos concluídos", "RevealRedacted": "Exibir triunfos editados", "SortRecords": "Ordenar triunfos por conclusão" }, "Vendors": { "Collections": "Coleções", "Engram": "Ranque", "FilterToUnacquired": "Exibir apenas não coletados", "HideSilverItems": "Ocultar Itens adquiridos com Prata", "NoItems": "Este Vendedor não está oferencendo nenhum item no momento.", "RefreshTime": "Inventário atualiza em:", "Vendors": "Vendedores" }, "Views": { "About": { "APIHistory": "Veja o histórico de todas as ações realizadas pelo DIM (e por outros apps de Destiny)", "BungieCopyright": "Todas as imagens e conteúdos são de propriedade da Bungie.", "CommunityInsight": "Conhecimento da Comunidade sobre Perks e Atributos de Personagem cortesia de {{clarityLink}}. Se você notar imprecisões ou tiver dúvidas, participe do {{clarityDiscordLink}}.", "Discord": "Discord", "DiscordHelp": "Faça perguntas, dê seu feedback e obtenha suporte em nossos canais no Discord.", "FAQ": "Perguntas Frequentes", "FAQAccess": "Como o DIM obtém acesso aos meus dados do Destiny?", "FAQAccessAnswer": "Nós usamos o aplicativo de autenticação da Bungie para conceder acesso ao DIM para ver e mover seus itens. DIM nunca vê seu nome de usuário ou senha. Esse é o mesmo modo que o aplicativo Companion funciona.", "FAQKeyboard": "O DIM suporta teclas de atalhos?", "FAQKeyboardAnswer": "Sim! Pressione \"?\" para ver uma lista dos atalhos disponíveis.", "FAQLogout": "Como posso fazer logout do DIM?", "FAQLogoutAnswer": "Abra o menu no ícone superior esquerdo e escolha \"Sair\"", "FAQLostItem": "Eu perdi um item usando a ferramenta de vocês!", "FAQLostItemAnswer": "A Bungie não permite que aplicativos deletem itens (mesmo o próprio aplicativo deles!). Muito provavelmente a transferência falhou, deixando seu item no cofre ou em outro personagem. Você pode procurar pelo item. Se isso não der certo, recarregue a página. Cheque {{link}} ou dentro do jogo pra ver se seu item ainda existe. Nós temos certeza que ele ainda está lá.", "FAQMobile": "O DIM é compatível com aparelhos móveis? Haverá um app?", "FAQMobileAnswer": "O site do DIM pode ser aberto em celulares e tablets. Você ainda pode adicioná-lo à sua tela inicial para uma experiência similar a um app.", "GitHub": "GitHub", "GitHubHelp": "Se você está interessado em contribuir com o projeto, visite nossa página no {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM é um app gratuito e de código-aberto, construído pela comunidade de desenvolvedores sobre os mesmos serviços usados pela Bungie.net e pelo app Destiny Companion.", "Schedule": { "beta": "Essa versão beta do DIM é atualizada toda vez que mudamos o código - ele pega as funcionalidades mais atuais, mas pega os bugs mais atuais também!", "release": "Esta versão do DIM é atualizada uma vez por semana, aproximadamente à meia-noite de Domingo, hora do Pacífico dos EUA." }, "Translation": "Junte-se à equipe de tradução!", "TranslationText": "Nós usamos o {{link}} para facilitar as traduções. Se você deseja aperfeiçoar alguma das traduções do DIM, junte-se ao time.", "Version": "Versão {{version}} ({{flavor}}), construído em {{date}}", "Wiki": "Guia do Usuário DIM", "WikiHelp": "Aprenda como utilizar os recursos do DIM." }, "Login": { "Auth": "Autorize com Bungie.net", "EnableDimSyncWarning": "Você havia desativado a Sincronização DIM e estava usando apenas o armazenamento de dados local. Ao ativar agora, todos os dados locais serão substituídos pelos dados disponíveis remotamente. Você deve fazer backup dos seus dados antes de ativar a Sincronização DIM. Você pode restaurar a partir desse backup em Configurações.", "Explanation": "Dar permissão ao DIM para ver e modificar os seus personagens, cofre e progressão.", "LearnMore": "Saiba mais sobre contas e login", "NewAccount": "Inicie a sessão com uma conta diferente da Bungie.net", "Permission": "Precisamos da sua permissão..." }, "Support": { "BackersDetail": "Apoie-nos com uma única doação, ou doação mensal e ajude-nos a continuar o nosso desenvolvimento ativo.", "FreeToDownload": "DIM é um produto gratuito para baixar e usar. O código-fonte do DIM é aberto e gratuito para qualquer pessoa melhorar. Você nunca verá um anúncio no DIM. É nosso compromisso.", "OpenCollective": "Estamos utilizando o {{link}} como serviço para providenciar uma compensação para os nossos desenvolvedores pela sua dedicação e tempo gasto nesse projeto.", "Store": "Temos artigos com nosso logotipo e outros projetos à venda em {{link}}", "Support": "Apoie o DIM" } }, "WishListRoll": { "BestRatedTip_one": "Este perk combina exatamente com um valor de arma em sua lista de desejos.", "BestRatedTip_other": "Estes perks combinam exatamente com um valor de arma em sua lista de desejos.", "Clear": "Limpar Lista de Desejos", "CopiedLine": "Lista de Desejos copiados para a área de transferência", "CopyLine": "Copiar as Vantagens selecionadas como Lista de Desejos", "DupeRolls": " (+{{num, number}} duplicados ignorados)", "ExternalSource": "Adicionar outra lista de desejos", "ExternalSourcePlaceholder": "Colar URL da lista de desejos aqui", "Header": "Lista de Desejos", "Import": "Carregar valores da Lista de Desejos", "ImportError": "Erro ao carregar lista de desejos de \"{{url}}\": {{error}}", "ImportFailed": "Nenhuma das suas listas de desejos continha quaisquer valores válidos.", "ImportNoFile": "Nenhum arquivo selecionado.", "InvalidExternalSource": "Por favor, insira uma URL válida como fonte de dados de lista de desejos externa. A URL deve começar como uma das seguintes:", "JustAnotherTeam": "Apenas outra equipe", "LastUpdated": "Última atualização: {{lastUpdatedDate}} às {{lastUpdatedTime}}", "Num": "{{num, number}} valores em sua lista de desejos", "NumRolls": "{{num, number}} valores", "Refresh": "Atualizar Lista de Desejos", "SourceAlreadyAdded": "Lista de Desejos já adicionada", "UpdateExternalSource": "Adicionar Lista de Desejos", "Voltron": "voltron (padrão)", "WishListNotes": "Anotações da Lista de Desejos:", "WorstRatedTip_one": "Este perk combina exatamente com um valor de arma em sua lista de itens indesejáveis.", "WorstRatedTip_other": "Estes perks combinam exatamente com um valor de arma em sua lista de itens indesejáveis." }, "no-space": "sem-espaço", "wrong-level": "nível-errado" } ================================================ FILE: src/locale/ru.json ================================================ { "AWA": { "ConfirmDescription": "Пожалуйста, используйте приложение-компаньон Destiny 2, чтобы позволить DIM изменять ваши предметы.", "ConfirmTitle": "Подтверждение", "Error": "Ошибка при изменении модов или перков", "ErrorMessage": "Мы не смогли экипировать {{plug}} в {{item}}.\n\n{{error}}", "FailedToken": "Не удалось получить разрешение на изменение предмета", "IrreversiblePlugging": "У вас нет {{plug}}, поэтому мы не будем его заменять." }, "Accounts": { "Choose": "Профили для {{bungieName}}", "ErrorLoadInventory": "Не удалось загрузить персонажей и инвентарь Destiny {{version}}", "ErrorLoadManifest": "Не удается загрузить информацию базы данных Destiny c Bungie.net", "ErrorLoading": "Не удалось загрузить учётные записи Destiny с Bungie.net", "MissingAccountWarning": "Если вы не видите здесь свой аккаунт, возможно, вы не вошли в нужный аккаунт Bungie.net, или Bungie.net находится на обслуживании.", "MissingDescription": "Учетная запись, которую вы пытаетесь просмотреть, не связана с вашим профилем Bungie.net. Выберите одну из ваших учетных записей ниже.", "MissingTitle": "Аккаунт не найден", "NoCharacters": "На данном аккаунте Bungie.net не имеется ни одного персонажа Destiny. Попробуйте авторизовать другой аккаунт.", "NoCharactersTitle": "Персонажей не найдено", "SwitchAccounts": "Вы сможете переключать свои аккаунты из меню в заголовке.", "Title": "Аккаунты" }, "Activities": { "Activities": "Активности", "Hard": "Сложный", "Nightfall": "Сумрачный налёт", "Normal": "Нормальный", "WeeklyHeroic": "Недельный Героический Налёт" }, "Armory": { "AlternateItems": "Alternate Versions", "Armory": "Оружейная", "DifferentSeason": "Reissue from a different season", "NoNotes": "Нет заметок", "OpenInArmory": "просмотреть в Оружейной", "Season": "Сезон {{season}}, год {{year}}", "TrashlistedRolls_few": "{{count, number}} мусорных роллов", "TrashlistedRolls_many": "{{count, number}} мусорных роллов", "TrashlistedRolls_one": "Мусорный ролл", "TrashlistedRolls_other": "{{count, number}} мусорных роллов", "Unknown": "Неизвестный предмет", "UnknownPerkHash": "The perk hash {{hash}} ({{perkName}}) does not appear on this item, so this wish list roll is invalid. Please contact the wish list author to correct this. Note that wish lists should always specify the non-enhanced version of perks.", "WishlistedRolls_few": "{{count, number}} в списке Желанных Роллов", "WishlistedRolls_many": "{{count, number}} в списке Желанных Роллов", "WishlistedRolls_one": "Желанный Ролл", "WishlistedRolls_other": "{{count, number}} в списке Желанных Роллов", "YourItems": "Ваши предметы" }, "Browsercheck": { "Samsung": "В Samsung Internet сайты могут выглядеть слишком тёмными, когда включен тёмный режим. Включите \"Настройки\" > \"Labs\" > \"Использовать темную тему сайта\" или переключитесь на другой браузер.", "Steam": "Браузер оверлея Steam очень старый, и некоторые или все функции DIM могут не работать. Мы не можем предоставить для него поддержку.", "Unsupported": "Команда DIM не поддерживает использование этого браузера. Некоторые или все функции DIM могут не работать." }, "Bucket": { "Armor": "Броня", "Class": "Подкласс", "General": "Общее", "Ghost": "Призрак", "Inventory": "Инвентарь", "Postmaster": "Почтмейстер", "Progress": "Прогресс", "Reputation": "Репутация", "Unknown": "Неизвестно", "Vault": "Хранилище", "Weapons": "Оружие" }, "BulkNote": { "Append": "Добавить к заметкам / добавить #хештеги", "Confirm": "Обновить заметки", "Remove": "Удалить из заметок / удалить #хештеги", "Replace": "Заменить заметки", "Title_few": "Изменить заметки для {{count}} предметов", "Title_many": "Изменить заметки для {{count}} предметов", "Title_one": "Изменить заметки для 1 предмета", "Title_other": "Изменить заметки для {{count}} предметов" }, "BungieAlert": { "Title": "Сообщение от Bungie:" }, "BungieService": { "AppNotPermitted": "DIM не имеет разрешения на выполнение этого действия.", "DestinyCannotPerformActionAtThisLocation": "Вы не можете надеть предметы или изменять моды во время активности. Попробуйте перейти на Орбиту или в социальную область. Это ограничение Bungie.net API, а не DIM.", "DestinyItemUnequippable": "Вы не можете надеть этот предмет. Если снаряжение персонажа было заблокировано в его последней активности, попробуйте перезайти в него снова.", "DestinyLegacyPlatform": "Сервисы Bungie содержат баг, который препятствует DIM загрузить информацию вашего аккаунта в Destiny 2, если вы играли в Destiny 1 на консоли прошлого поколения. Bungie скоро это исправят, но до тех пор, вы должны играть в Destiny 1 на консоли текущего поколения, чтобы иметь доступ к своим данным.", "DevVersion": "Вы используете версию DIM для разработчиков? Вы должны зарегистрироваться в расширении для браузера Chrome через Bungie.net.", "Difficulties": "Bungie.net сейчас испытывает некоторые трудности при использовании.", "ErrorTitle": "Ошибка Bungie.net", "ItemUniquenessExplanation": "A character can only have one of '{{name}}' on it.", "Maintenance": "Сервера Bungie.net отключены для техобслуживания.", "MissingInventory": "Ваш инвентарь не был загружен с Bungie.net. Возможно, этому препятствуют ваши настройки конфиденциальности. Попробуйте выйти из аккаунта и снова войти в него.", "NetworkError": "Сетевая ошибка - {{status}} {{statusText}}", "NoAccount": "Аккаунт Destiny не найден. Вы уверены, что выбрали правильную платформу?", "NoAccountForPlatform": "Не удалось найти ваш аккаунт Destiny на {{platform}}.", "NotConnected": "Вы, возможно, не подключены к Интернету.", "NotConnectedOrBlocked": "Возможно вы не подключены к Интернету, или блокировщик рекламы или настройки приватности блокируют Bungie.net.", "NotLoggedIn": "Для использования, подтвердите доступ DIM к данным учетной записи на Bungie.net.", "Slow": "Bungie.net медленно отвечает", "SlowDetails": "Bungie.net долго возвращает информацию. Это может быть связано с большим количеством игроков онлайн, с неполадками сервисов Bungie.net или проблема с вашим подключением к Интернету. Мы будем ждать ответа.", "SlowResponse": "Bungie.net слишком долго отвечает на запрос.", "Throttled": "Bungie.net устанавливает ограничение на то, сколько запросов DIM может сделать.", "Twitter": "Получить обновления статуса сервиса:", "UnknownError": "Сообщение от Bungie.net: {{message}}", "VendorNotFound": "Данные вендоров недоступны." }, "Compare": { "Archetype": "Архетип", "AssumeMasterworked": "Предположить Абсолют", "AssumeMasterworkedDescription": "Stats if fully Masterworked, without current Mods", "BaseStatsDescription": "Base stats, without Masterwork or Mods", "Button": "Сравнить", "ButtonHelp": "Сравнить предметы", "CompareBaseStats": "Показать базовые характеристики", "CurrentStats": "Current Stats", "CurrentStatsDescription": "Current stats, including Mods and Masterwork level", "Error": { "Invalid": "Нет допустимых предметов для сравнения.", "Unmatched": "Этот предмет не соответствует выбранным критериям сравниваемых предметов." }, "InitialItem": "Это предмет, с которого была запущена функция Сравнить", "IsVendorItem": "Этот предмет не находится в вашем инвентаре, но {{vendorName}} продаёт его.", "NoModArmor": "До модов" }, "Cooldown": { "Grenade": "Перезарядка гранаты: {{cooldown}}", "Melee": "Перезарядка умения ближнего боя: {{cooldown}}", "Super": "Перезарядка Суперспособности: {{cooldown}}" }, "Countdown": { "Days_compact_few": "{{count}} дн", "Days_compact_many": "{{count}} дн", "Days_compact_one": "{{count}} дн", "Days_compact_other": "{{count}} дн", "Days_few": "{{count}} дн", "Days_many": "{{count}} дн", "Days_one": "1 день", "Days_other": "{{count}} дн" }, "Csv": { "EmptyFile": "Строки в файле не найдены.", "ImportConfirm": "Вы уверены, что хотите импортировать тэги/заметки из CSV? Импорт обновит тэги/описания всех предметов в таблице.", "ImportFailed": "Ошибка импорта тэгов/заметок из CSV: {{error}}", "ImportSuccess_few": "Импортированы тэги и заметки для {{count}} предметов.", "ImportSuccess_many": "Импортированы тэги и заметки для {{count}} предметов.", "ImportSuccess_one": "Импортированы тэги и заметки для одного предмета.", "ImportSuccess_other": "Импортированы тэги и заметки для {{count}} предметов.", "ImportWrongFileType": "Файл не является CSV-файлом.", "WrongFields": "Обязательные столбцы файла CSV: 'Id', 'Notes', 'Tag', 'Hash'." }, "Dialog": { "Cancel": "Отмена", "OK": "ОК" }, "EnergyMeter": { "Energy": "Энергия", "Unused": "Не использовано", "UpgradeNeeded": "Энергоёмкость текущего предмета равна {{energyCapacity}}. Чтобы вместить выбранные модификаторы, энергоёмкость этого предмета должна быть равна {{energyUsed}}.", "Used": "Использовано" }, "ErrorBoundary": { "Title": "Что-то пошло не так" }, "ErrorPanel": { "BrowserTooOld": "Your browser is too old to use DIM. Please update your browser to the latest version.", "BrowserTooOldTitle": "Incompatible browser", "Description": "Попробуйте загрузить свой инвентарь в приложении-компаньоне Destiny 2, чтобы убедиться, что Bungie.net недоступен.", "ReadTheGuide": "Прочтите наше Руководство (ссылка находится в меню) для того, чтобы узнать о шагах по устранению неполадок.", "SystemDown": "Это влияет на все приложения Destiny, и команда DIM не может это исправить или обойти.", "Troubleshooting": "Устранение неполадок" }, "FarmingMode": { "D2Desc_female_few": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета было {{count}} свободных слотов на {{store}}.", "D2Desc_female_many": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета было {{count}} свободных слотов на {{store}}.", "D2Desc_female_one": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета был один свободный слот на {{store}}.", "D2Desc_female_other": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета было {{count}} свободных слотов на {{store}}.", "D2Desc_few": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета было {{count}} свободных слотов на {{store}}.", "D2Desc_male_few": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета было {{count}} свободных слотов на {{store}}.", "D2Desc_male_many": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета было {{count}} свободных слотов на {{store}}.", "D2Desc_male_one": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета был один свободный слот на {{store}}.", "D2Desc_male_other": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета было {{count}} свободных слотов на {{store}}.", "D2Desc_many": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета было {{count}} свободных слотов на {{store}}.", "D2Desc_one": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета был один свободный слот на {{store}}.", "D2Desc_other": "DIM не дает предметам отправиться к Почтмейстеру, проверяя, чтобы на каждый тип предмета было {{count}} свободных слотов на {{store}}.", "Desc_female_few": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает {{count}} открытых мест для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_female_many": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает {{count}} открытых мест для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_female_one": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает одно открытое место для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_female_other": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает {{count}} открытых мест для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_few": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает {{count}} открытых мест для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_male_few": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает {{count}} открытых мест для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_male_many": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает {{count}} открытых мест для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_male_one": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает одно открытое место для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_male_other": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает {{count}} открытых мест для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_many": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает {{count}} открытых мест для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_one": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает одно открытое место для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "Desc_other": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}} в хранилище и удерживает {{count}} открытых мест для каждого типа предмета, чтобы новые не отправлялись Почтмейстеру.", "FarmingMode": "Режим фарма", "FarmingModeNote": "(сохраняет место для выпадений)", "MakeRoom": { "Desc": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}}, чтобы новые не отправлялись Почтмейстеру. Предметы отправляются в Хранилище или другим персонажам.", "Desc_female": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}}, чтобы новые не отправлялись Почтмейстеру. Предметы отправляются в Хранилище или другим персонажам.", "Desc_male": "DIM поддерживает доступное место для Энграмм и Блеска у {{store}}, чтобы новые не отправлялись Почтмейстеру. Предметы отправляются в Хранилище или другим персонажам.", "MakeRoom": "Освобождать место для предметов путём перемещения снаряжения", "Tooltip": "Если включено, DIM переместит оружие и броню, чтобы освободить место в хранилище для энграмм." }, "OutOfRoom": "Не хватает свободного места, чтобы переместить предметы с {{character}}. Самое время почистить всё от мусора!", "OutOfRoomTitle": "Недостаточно свободного места", "Stop": "Остановить", "Vault": "Оно переместит предметы в хранилище, чтобы освободить место." }, "FashionDrawer": { "Accept": "Сохранить стиль", "CannotFitOrnament": "У этого предмета нет ячейки для орнамента, или у вас нет орнамента для него.", "CannotFitShader": "На этот предмет невозможно применить шейдер", "ClearOrnaments": "Очистить орнаменты", "ClearOrnamentsTitle": "Сбросить все орнаменты на «без предпочтений»", "ClearShaders": "Очистить шейдеры", "ClearShadersTitle": "Сбросить все шейдеры на «без предпочтений»", "NoPreference": "Нет предпочтений - этот слот не будет изменен", "Reset": "Очистить стиль", "Sync": "Синхронизировать", "SyncOrnaments": "Синхронизировать орнаменты", "SyncOrnamentsTitle": "Использовать орнаменты одного и того же набора для всех предметов, если они разблокированы", "SyncShaders": "Синхронизировать шейдеры", "SyncShadersTitle": "Использовать один и тот же шейдер для всех предметов", "Title": "Выберите шейдеры и орнаменты", "UseEquipped": "Использовать надетый стиль" }, "FileUpload": { "Instructions": "Выберите или перетащите файлы" }, "Filter": { "Adept": "\\(адепт\\)", "AmmoType": "Показывает предметы по их типу боеприпасов.", "Armor": "Показывает предметы, которые являются бронёй.", "Armor3": "Shows items that use the Armor 3.0 stat system introduced in Edge of Fate.", "ArmorCategory": "Показывает броню по категориям.", "ArmorIntrinsic": "Показывает легендарную броню, которая имеет изначальный перк, такой как Искусная Броня.", "Artifice": "Shows Artifice armor.", "Ascended": "Показывает предметы, которые имеют узел восхождения и получили восхождение.", "Breaker": "Filter by breaker type or corresponding champion type. breaker:instrinsic shows items with intrinsic breaker ability.", "BulkClear_few": "Удалены тэги с {{count}} предметов.", "BulkClear_many": "Удалены тэги с {{count}} предметов.", "BulkClear_one": "Удалён тэг с одного предмета.", "BulkClear_other": "Удалены тэги с {{count}} предметов.", "BulkRevert_few": "Вернули тэги {{count}} предметам.", "BulkRevert_many": "Вернули тэги {{count}} предметам.", "BulkRevert_one": "Вернули тэг одному предмету.", "BulkRevert_other": "Вернули тэги {{count}} предметам.", "BulkTag_few": "{{count}} выбранных предметов отмечены как {{tag}}.", "BulkTag_many": "{{count}} выбранных предметов отмечены как {{tag}}.", "BulkTag_one": "Предмет отмечен {{tag}}.", "BulkTag_other": "{{count}} выбранных предметов отмечены как {{tag}}.", "Catalyst": "Показывает катализаторы на основе их статуса. catalyst:complete показывает катализаторы, которые вы выполнили и применили, catalyst:incomplete показывает катализаторы, которые вы разблокировали, но не выполнили или не применили, и catalyst:missing показывает предметы, которые могут иметь катализатор, но вы его еще не нашли.", "Class": "По классам.", "Combine": "Фильтры совмещаются и группируются скобками, логическими операторами \"or\" [ИЛИ] и \"and\" [И] для сужения области поиска, например \"{{example}}\".", "ContributePower": "Увеличивающие уровень Силы.", "Cosmetic": "Косметика.", "Craftable": "Показывает предметы, которые можно изготовить.", "CraftedDupe": "Показывает одинаковые оружия, среди которых хотя бы один экземпляр сформирован.", "Curated": "Год-роллы.", "CurrentClass": "Показывает предметы, которые могут быть экипированы текущим стражем.", "CustomStatLower": "Shows armor whose stats are strictly lower than another of the same type of armor, only taking into account stats that are in any of that class' custom stat total list.", "DamageType": "По типу стихии.", "Deepsight": "Показывает оружия с Резонансом Глубинного Зрения, из которых можно извлечь образ, или для которых можно активировать Резонанс Глубинного Зрения, используя Гармонизатор Глубинного Зрения.", "Deprecated": "Этот фильтр больше не поддерживается.", "Description": "Описание", "DescriptionFilter": "Показывает предметы, описание которых частично совпадает с введённым текстом. Ищите целые фразы, используя кавычки.", "DisabledModSlot": "Отображает предметы с отключенным модификатором.", "Dupe": "Одинаковые, Включая перевыпуски разных сезонов", "DupeArchetype": "Groups armor with the same stat Archetype.", "DupeCount": "Предметы, которые имеют указанное количество дубликатов.", "DupeLower": "Дубликаты предметов, включая переиздания, не являющиеся дубликатами самого высокого уровня Силы. Только один дубликат выбирается как высший, а остальные считаются низшими.", "DupePerks": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "DupeSetBonus": "Groups armor with the same set bonus.", "DupeStats": "Shows armor with identical base stats, and matching stat adjustment mods like Artifice or Tuners.", "DupeTertiary": "Groups armor with the same tertiary stat.", "DupeTraits": "Weapons whose traits are either a duplicate of, or a subset of, another weapon of the same type.", "DupeTunedStat": "Groups armor with the same Tuned stat.", "DupeUntunedStats": "Groups armor with identical base stats, ignoring stat adjustment mods.", "DupeZeroStats": "Groups armor with the same 3 non-zero base stats.", "Energy": "Shows items that use the Armor 2.0 mod system introduced in Shadowkeep.", "EnergyCapacity": "Shows items based on their current energy capacity.", "Engrams": "Показывает энграммы.", "Enhanceable": "Показывает оружия, которые могут быть улучшены.", "Enhanced": "Shows weapons based on their enhancement tier.", "EnhancedPerk": "Shows weapons that have the specified number of enhanced perk columns.", "EnhancementReady": "Shows weapons that have reached level thresholds for perk enhancement.", "Equipment": "Предметы, которые могут быть экипированы.", "Equipped": "Предметы, которые экипированы на персонаже.", "Event": "По источникам получения.", "ExtraPerk": "Показывает Легендарные оружия со случайными перками которые имеют дополнительный выбираемый перк.", "Featured": "Items that count as one of the \"New Gear\" or \"Featured Items\" in the current season.", "Filter": "Фильтр", "FilterWith": "Фильтровать с:", "Focusable": "Показывает предметы, которые можно сфокусировать у торговца", "Foundry": "Показывает предметы по производителю, который их произвёл.", "Glimmer": "Расходуемые предметы, относящиеся к получению Блеска.", "Harrowed": "\\(Истерзанное\\)", "HasNotes": "С Заметками.", "HasOrnament": "Показывает предметы с применённым орнаментом.", "HasShader": "Покрашенные шейдерами.", "Holofoil": "Shows holofoil weapons.", "InDimLoadout": "is:indimloadout показывает предметы, которые включены в любые комплекты DIM.", "InInGameLoadout": "is:iningameloadout показывает предметы, которые включены в любые внутриигровые комплекты.", "InInventory": "Shows items that you have at least one copy of in your inventory. Only really useful in the Vendors and Records screens.", "InLoadout": "is:inloadout показывает предметы, которые включены в любые комплекты. Поиск при помощи inloadout: показывает предметы, которые включены в комплекты с совпадающими названиями. При использовании вместе с диапазоном, показывает предметы, которые находятся в комплектах в данном диапазоне.", "Infusable": "Доступные для Синтеза.", "InfusionFodder": "Показывает предметы, которые доступные для синтеза к менее мощным версиям одного и того же предмета только для Блеска.", "IsAdept": "Показывает оружия, совместимые с Адепт модификаторами.", "IsCrafted": "Показывает оружия, которые были изготовлены.", "ItemHash": "По item hash предмета. Для тех, кто знает что делает.", "ItemId": "По item ID предмета. Для тех, кто знает что делает.", "Leveling": { "Complete": "{{term}} - показывает вещи, которые завершены - все улучшения открыты.", "Incomplete": "{{term}} - показывать незаконченные предметы - есть как минимум одно улучшение для разблокировки.", "NeedsXP": "{{term}} - показывает вещи, которые еще могут принимать опыт.", "Upgraded": "{{term}} - показывает предметы, у которых достаточно XP, чтобы разблокировать всех их узлы, но не все узлы были разблокированы.", "XPComplete": "{{term}} - показывает вещи, которые не могут принимать опыт (независимо от того, были ли открыты все улучшения)." }, "Location": "По расположению в DIM. \nleft/middle/right - персонаж. middle и right не работают на аккаунтах с одним персонажем. \ncurrent - последний/сейчас активный персонаж (выделен желтым треугольником).", "LockAllFailed": "Блокировка предметов не удалась", "LockAllSuccess": "Заблокировано {{num}} предметов", "Locked": "Заблокированные/разблокированные.", "Masterwork": "С указанным модификатором или уровнем Абсолюта.", "MasterworkKills": "По значению счётчика убийств.", "MaxPower": "Показывает предметы с наивысшей Силой на каждый слот.", "MaxPowerLoadout": "Показывает предметы в снаряжении, которые позволят максимизировать ваш уровень Силы для каждого класса персонажа.", "Memento": "Показывает оружия, которые имеют ячейку Воспоминаний.", "ModSlot": "Shows armor with a specific mod type slot.", "Mods": { "Y3": "Shows items with any mods applied." }, "Name": "Отображает предметы, названия которых совпадает (exactname:) или частично совпадает (name:) с введённым текстом. Ищите целые фразы, используя кавычки.", "NamedStat": "Показывать броню, у которой есть очки в указной характеристике.", "Negate": "Для подсветки всех предметов, кроме попадающих под условие фильтра (Обратной Фильтрации) вводите фильтр с минусом или приставкой \"not\", примеры \"{{notexample}}\" или \"{{notexample2}}\".", "NewItems": "Показывает новые предметы.", "Notes": "Search for items that you have tagged with custom notes.", "OriginTrait": "Shows weapons that have an origin trait perk.", "Ornament": "Показывает вещи с орнаментами и фильтрами их статуса.", "PartialMatch": "Показывает предметы, название, описание, любой перк или модификатор которых частично совпадают с введённым текстом. Ищите целые фразы, используя кавычки.", "PatternUnlocked": "Показывает предметы, чей Образ Изготовления уже разблокирован, даже если сам предмет не был изготовлен.", "Perk": "Показывает предметы, в которых один из их перков или модов частично совпадает с текстом фильтра в их названии или описании. Ищите фразы целиком, используя кавычки.", "PerkName": "Показывает предметы с перком или модификатором, имя которых совпадает (exactperk:) или частично совпадает (perkname:) с текстом в фильтре. Ищите фразы, используя кавычки.", "PinnacleReward": "Испытания со Сверхмощными наградами.", "Postmaster": "Предметы, которые находятся в Почтмейстере.", "PowerKeywords": "Используйте ключевое слово pinnaclecap или softcap вместо числа, чтобы ссылаться на ограничения Силы текущего сезона.", "PowerLevel": "Показывает предметы на основе их уровня Силы. $t(Filter.PowerKeywords)", "PowerfulReward": "Показывает поручения, которые производят мощную награду.", "PrismaticDamageType": "Показывает предметы в зависимости от того, относятся ли они к типу урона света или тьмы. Типы света - это молния, солнце и пустота. Типы тьмы - это стазис и нить.", "Quality": "По общей сумме процентов характеристик. '{{percentage}}' означает '{{quality}}'.", "RandomRoll": "Показывает предметы которые выпадают со случайными перками.", "RarityTier": "Показывает предметы по их уровням редкости.", "Reforgeable": "Перековываемые у Оружейника.", "Release": "Shows items available from a specific release or event.", "RequiredLevel": "По требуемому уровню.", "RetiredPerk": "Показать оружия с перками, которые больше нельзя получить.", "SearchPrompt": "Искать среди Фильтров", "Season": "По сезонам Destiny 2.", "StackFull": "Показать предметы, которые заполнены для их набора (Улучшающие ядра, Странные монеты, Оружейные материалы и т.д.)", "StackLevel": "По количеству в пачке/стаке.", "Stackable": "Стакающиеся (боеприпасы, странные монеты и т.д.)", "StatLower": "Показывает броню, статистика которой находится чуть ниже другого типа брони.", "Stats": "Показывает предметы на основе определенного значения статистики. $t(Filter.StatsExtras)", "StatsBase": "Фильтрует броню на основе ее базового статического значения, не считая включенных модов или Абсолюта. $t(Filter.StatsExtras)", "StatsExtras": "Поддерживает добавление статистики путем объединения нескольких названий статистики с помощью символов + или &. Существуют также специальные ключевые слова: highest, secondhighest, thirdhighest, и т.д., которые соответствуют статистике на основе их рейтинга в статистике предметов. Каждая Пользовательская Характеристика также имеет собственный фильтр для поиска, отображаемый а настройках Пользовательских Характеристик.", "StatsLoadout": "Наборы брони с максимальной суммой Указанной Характеристики для каждого класса.", "StatsMax": "Броня в каждый слот с максимальной Указанной Характеристикой для всех персонажей. Фильтруются все предметы с максимальным значением Характеристики для каждого слота.", "StatsOrdinal": "Finds armor 3.0 with the specified stat focusing.", "Tags": { "Tag": "С указанным Тэгом.", "Tagged": "С любым Тэгом." }, "Tier": "Shows items based on their tier from 0-5.", "Timelost": "\\(вневременн(ой|ая)\\)", "Tracked": "Контракты/Преследования по статусу отслеживания.", "Transferable": "Перемещаемые.", "Trashlist": "Показывает предметы которые соответствуют вашей корзине Списка Желаний.", "TunedStat": "Shows items with tuning mods for the specified stat.", "Unascended": "Показывает предметы, имеющие узел Абсолюта, которые ещё не получили Абсолют.", "Undo": "Отменить", "UnlockAllFailed": "Не удалось разблокировать предметы", "UnlockAllSuccess": "Разблокировано {{num}} предметов", "Vendor": "По Вендорам.", "VendorItem": "Предмет от торговца, не в вашем инвентаре. Полезно для исключения предметов торговцев из Оптимизатора Наборов.", "Weapon": "Оружие.", "WeaponLevel": "Показывает оружия на основе их Уровня оружия.", "WeaponType": "Оружие по типам.", "Wishlist": "Показывает предметы которые совпадают с вашим Списком Желаний.", "WishlistDupe": "Одинаковые, среди которых хотя бы один экземпляр есть в Wish List.", "WishlistEnabled": "Показывает предметы, которые могут иметь роллы из списка желаний.", "WishlistNotes": "По тексту заметок загруженного Wish List.", "WishlistUnknown": "Показывает предметы без рекомендаций к роллам в загруженных Списках Желаний.", "Year": "По году Destiny 2." }, "General": { "ClickForDetails": "Подробнее...", "Close": "Закрыть", "Confirm": "Подтвердить?", "UserGuideLink": "Руководство" }, "Glyphs": { "Axe": "Топор", "DarkAbility": "Способность Тьмы", "Gilded": "Позолоченный", "Harmonic": "Гармонический", "HiveSword": "Меч Улья", "LightAbility": "Способность Света", "LightLevel": "Уровень Света", "Misadventure": "Неудача", "Missing": "Отсутствует", "OpenSymbolsPicker": "Открыть выбор символов", "Prismatic": "Призма", "Quickfall": "Быстрое падение", "RespawnRestricted": "Возрождение Ограничено", "ScorchCannon": "Обжигающая Пушка", "SearchSymbols": "Найти символы...", "Smoke": "Дым" }, "Header": { "About": "О Проекте", "AutoRefresh": "DIM будет автоматически перезагружаться пока вы играете.", "BulkTag": "Массовая маркировка предметов", "BungieNetAlert": "Оповещение Bungie", "Clear": "Очистить поиск", "CompareMatching": "Сравнить предметы", "DeleteSearch": "Удалить", "FilterHelp": "Поиск предмета/перка, {{example}}, и другое", "FilterHelpBrief": "Поиск", "FilterHelpLoadouts": "Поиск названий набора и заметок", "FilterHelpMenuItem": "Помощь по Фильтрам...", "FilterHelpOptimizer": "Filter armor included in builds, e.g.: {{example}}", "FilterHelpProgress": "По Испытаниям и Контрактам", "FilterHelpRecords": "Поиск по Триумфам и Коллекции", "FilterMatchCount_few": "{{count}} предметов", "FilterMatchCount_many": "{{count}} предметов", "FilterMatchCount_one": "1 предмет", "FilterMatchCount_other": "{{count}} предметов", "Filters": "Фильтры", "InstallDIM": "DIM как приложение", "InstallDIMBanner": "Установить DIM в качестве приложения на домашний экран", "Inventory": "Инвентарь", "IosPwaPrompt": "В Safari, нажмите на значок \"поделиться\" (средняя кнопка внизу) и выберите \"Добавить на домашний экран\".", "KeyboardShortcuts": "Горячие клавиши", "LaunchDIMAlone": "Отдельное окно", "MaterialCounts": "Количество материалов", "Menu": "Меню", "ProfileAge": "Сервера Destiny последний раз отправили обновленные данные {{age}} назад.\nОбновление через DIM может получить новые данные, но Bungie.net также может повторять кэшированную информацию.", "Refresh": "Обновить данные Destiny [R]", "ReloadApp": "Перезагрузить DIM", "ReportBug": "Сообщить об ошибке", "SaveSearch": "Избранное", "SearchActions": "Открыть Действия Поиска", "SearchResults": "Результат", "Shop": "Магазин", "TagAs": "Отметить '{{tag}}'", "UpgradeDIM": "Обновить DIM", "WhatsNew": "Что Нового" }, "Help": { "CannotMove": "Невозможно переместить этот предмет этого с персонажа.", "NoStorage": "DIM не смог записать данные", "NoStorageMessage": "DIM не может хранить данные в вашем браузере. Это может быть вызвано просмотром страницы в приватном режиме или в режиме инкогнито, нехваткой места на диске, или ошибкой браузера. Попробуйте перезагрузить ваш компьютер! Вы не сможете авторизоваться или использовать DIM, пока вы это не исправите." }, "Hotkey": { "Armory": "Показать Оружейную для предмета", "CheatSheetTitle": "Горячие клавиши:", "ClearDialog": "Скрыть диалог", "ClearNewItems": "Удалить маркеры", "Enter": "[ENTER]", "ItemPopupTab": "Переключить вкладку сведений о предмете", "LockUnlock": "Заблокировать/Разблокировать", "MarkItemAs": "Тэг '{{tag}}'", "Menu": "Меню", "Note": "Введите заметки", "Pull": "Передать активному персонажу ", "RefreshInventory": "Обновить данные", "ShowHotkeys": "Показать сочетания клавиш", "StartSearch": "Курсор в поле Поиска", "StartSearchClear": "Очистить Поиск и Курсор в поле Поиска", "Tab": "[TAB]", "Vault": "Отправить в Хранилище" }, "InGameLoadout": { "ClearSlot": "Очистить слот {{index}}", "Create": "Создать Набор", "CreateTitle": "Создать внутриигровой комплект из текущего снаряжения", "CurrentlyEquipped": "В настоящее время экипировано", "DeleteFailed": "Не удалось удалить комплект", "Deleted": "Набор удалён", "DeletedBody": "Очищен внутриигровой комплект в слоте {{index}}", "EditFailed": "Не удалось обновить комплект", "EditIdentifiers": "Изменить идентификаторы", "EditTitle": "Изменить название комплекта и значок", "EquipNotReady": "Внутриигровое экипирование не готово", "EquipReady": "Внутриигровое экипирование готово", "LoadoutDetails": "Подробнее о Наборе", "MatchingLoadouts": "Совпадающие комплекты:", "PrepareEquip": "Подготовить к экипированию", "Replace": "Заменить Набор {{index}}", "Save": "Обновить Набор", "SaveIdentifiers": "Update Identifiers", "SnapshotFailed": "Не удалось создать снимок текущего комплекта" }, "Infusion": { "Filter": "Фильтры предметов", "InfuseSource": "Выберите предмет для синтеза {{name}}", "InfuseTarget": "Выберите предмет для синтеза c {{name}}", "InfusionMaterials": "Материалы для синтеза", "NoItems": "Нет предметов для синтеза.", "NoTransfer": "Перемещение материала синтеза\n{{target}} не может быть перемещен.", "SwitchDirection": "Переставить", "TransferItems": "Перенести" }, "Inventory": { "ClickToExpand": "(Нажмите, чтобы раскрыть)", "MissingSilver": "Your Silver balance is only available while you are playing the game." }, "Item": { "SetBonus": { "NPiece_few": "{{count}} Piece", "NPiece_many": "{{count}} Piece", "NPiece_one": "{{count}} Piece", "NPiece_other": "{{count}} Piece" }, "ThumbsDown": "Палец Вниз", "ThumbsUp": "Палец Вверх" }, "ItemFeed": { "ClearFeed": "Очистить Ленту", "Description": "Лента предметов", "HideTagged": "Скрыть отмеченные", "NoNewItems": "Нет новых предметов", "ShowOlderItems": "Показывать старые предметы" }, "ItemMove": { "Consolidate": "Собрали {{name}}", "Distributed": "Распределили {{name}}\nтеперь {{name}} поровну распределены между персонажами.", "MovingItem": "В хранилище", "MovingItem_female": "Передаем {{target}}", "MovingItem_male": "Передаем {{target}}", "ToStore": "Все {{name}} собраны в {{store}}.", "ToVault": "Все {{name}} теперь в вашем хранилище." }, "ItemPicker": { "ChooseItem": "Выберите предмет:", "SearchPlaceholder": "Поиск" }, "ItemService": { "BucketFull": { "Guardian": "Слишком много '{{itemtype}}' в вашем {{store}}.", "Guardian_female": "Слишком много '{{itemtype}}' в вашем {{store}}.", "Guardian_male": "Слишком много '{{itemtype}}' в вашем {{store}}.", "Vault": "Есть слишком много «{{itemtype}}» в {{store}}." }, "Classified": "Предмет засекречен и пока не доступен для перемещения.", "Classified2": "Засекреченный предмет. Bungie пока что не предоставляет информацию об этом предмете. Добавьте заметку к этому предмету и используйте фильтр \"notes:\" в поиске, чтобы найти его.", "Deequip": "Нечего надеть взамен {{itemname}}", "ExoticError": "Экзот в {{slot}} слоте не позволяет надеть {{itemname}}. ({{error}})", "NotEnoughRoom": "Больше нет предметов, которые мы можем убрать из {{store}}, чтобы освободить место для {{itemname}}", "NotEnoughRoomGeneral": "Недостаточно места для перемещения этого предмета.", "OnlyEquippedClassLevel": "Это может быть надето только на персонаже класса {{class}} с уровнем {{level}} или выше.", "OnlyEquippedLevel": "Это может быть надето только на персонаже с уровнем {{level}} или выше.", "PostmasterAlmostFull": "Почти полный!", "PostmasterFull": "Полный!", "PreviewVendor": "Предпросмотр {{type}}", "StackFull": "У вас уже есть полная пачка {{name}}", "StoreName": "{{genderRace}} {{className}}" }, "KillType": { "ClassAbilities": "Классовые способность", "Finisher": "Добивающий удар", "Grenade": "Граната", "Melee": "Умение ближнего боя", "Precision": "Прицельный", "Super": "Суперспособность" }, "LB": { "AddStack": "Добавить еще одну копию этого модификатора", "AdvancedOptions": "Расширенные настройки", "ChooseAMod": "Выберите свои моды", "ChooseASetBonus": "Choose your set bonuses", "ChooseAnExotic": "Выберите вашу экзоту", "ClearLocked": "Очистить \"Заблокированные\"", "ContainsVendorItems": "Этот набор содержит предметы вендоров", "Current": "Текущий", "Equip": "Надеть на {{character}}", "Exclude": "Исключённые Предметы", "ExcludeHelp": "[Shift]+[ЛКМ] по предмету (или перетащите его в этот список), добавляет его к Игнорируемым. Ни одного Игнорируемого предмета нет ни в одном сгенерированном наборе.", "ExistingBuildStats": "Характеристики Существующего Билда", "ExistingBuildStatsNote": "Only showing builds with strictly higher stats.", "FilterSets": "Наборы фильтров", "Help": { "And": "Будет использована броня со всеми этими перками (\"и\")", "ChangeNodes": "Изменяет характеристики Intellect, Discipline, или Strength в игре на отображаемые, чтобы создать каждый набор.", "Discipline": "Дисциплина уменьшает время восстановления гранат", "DragAndDrop": "Перетащите предметы к Обязательным - они будут во всех предлагаемых Наборах", "Help": "Нужна помощь?", "HigherTiers": "Чем больше Ранг - тем лучше", "Intellect": "Интеллект уменьшает время восстановления Суперспособности", "Lock": "Заблокируйте набор перков, нажав на замок и выбрав перки", "MultiPerk": "Чтобы использовать броню вместе с несколькими перками, используйте Shift+ЛКМ по желаемым перкам", "NoPerk": "Если перк не появляется, значит, у вас нет брони с этим перком", "Or": "Наборы генерируются из предметов с Любым Обязательным Бонусом (\"или\")", "ShiftClick": "[Shift]+[ЛКМ] уберёт предмет из всех предлагаемых Наборов", "StatsIncrease": "С увеличением уровня защиты предмета, также увеличиваются характеристики (int/dis/str) тоже увеличиваются.", "Strength": "Сила уменьшает время восстановления способности ближнего боя", "Synergy": "Пытайтесь найти броню с бонусами, увеличивающими резервы для типов оружия, которые вы используете.", "Tier11Example": "4/5/2 (Р11) - 4 Интелекта, 5 Дисциплины, 2 Силы (Р4+Р5+Р2 = Р11)" }, "HideAllConfigs": "Скрыть все настройки", "HideConfigs": "Скрыть настройки", "IncompatibleWithOptimizer": "Этот предмет несовместим с Конструктором Наборов. Пожалуйста, приобретите новую версию из Коллекций.", "LB": "Конструктор Наборов", "LightMode": { "HelpCurrent": "Рассчитывает наборы на текущих уровнях защиты брони.", "HelpScaled": "Рассчитать наборы, как будто у всех предметов 350 защиты.", "LightMode": "Светлый режим" }, "Loading": "Загрузка лучших наборов", "LockEquipped": "Заблокировать экипированное", "LockPerk": "Обязательный Бонус", "Locked": "Заблокированные Предметы", "LockedHelp": "Добавьте предмет к Обязательным перетащив в этот список. Список предлагаемых Наборов сократится до содержащих Обязательные предметы. [Shift]+[ЛКМ] по Обязательному предмету уберёт его из списка.", "Missing2": "Отсутствуют редкие, легендарные или экзотические предметы, чтобы собрать полный набор!", "ProcessingMode": { "Fast": "Быстрый", "Full": "Полный", "HelpFast": "Проверяет только лучшие вещи.", "HelpFull": "Проверяет больше вещей, но занимает больше времени.", "ProcessingMode": "Режим обработки" }, "RemoveStack": "Удалить копию этого модификатора", "Scaled": "Масштабированный", "SearchAMod": "Поиск по имени модификатора или описанию", "SearchASetBonus": "", "SearchAnExotic": "Поиск по имени экзота или описанию", "SelectExotic": "Выбрать экзот", "SelectMods": "Выбрать моды", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} Модификаторов Активности", "SelectSetBonus": "Select Set Bonuses", "SelectSubclassOptions": "Настройка подкласса", "ShowAllConfigs": "Показать все настройки", "ShowConfigs": "Показать настройки", "ShowGear": "{{class}} Броня", "Vendor": "Включить предметы торговцев" }, "Loading": { "Accounts": "Загрузка аккаунтов Destiny...", "Code": "Загрузка кода DIM...", "FilterHelp": "Загрузка помощи поиска...", "Profile": "Загрузка профиля Destiny...", "Vendors": "Загрузка вендоров Destiny..." }, "LoadoutAnalysis": { "Analyzed": "Проанализировано {{numLoadouts}} Комплектов", "Analyzing": "Анализ {{numAnalyzed}}/{{numLoadouts}} Комплектов", "BetterStatsAvailable": { "Description": "Choosing different armor or mods for this loadout will allow reaching higher stats. Choose \"$t(Loadouts.OpenInOptimizer)\" to view better builds.", "Name": "Доступны лучшие характеристики" }, "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat to exceed 200. DIM may identify better stats by reducing the amount of excess stats. If this is undesired, disable \"$t(Loadouts.IncludeRuntimeStatBenefits)\" in the Loadout.", "DoesNotRespectExotic": { "Description": "Настройки Оптимизатора Комплекта для этого комплекта задают выбор экзота, но комплект не совпадает с этим экзотом.", "Name": "Неверный Экзот" }, "DoesNotSatisfyStatConstraints": { "Description": "Loadout Optimizer settings for this Loadout specify stat minimums, but the Loadout does not reach them.", "Name": "Wrong Stat Minimums" }, "EmptyFragmentSlots": { "Description": "В этом подклассе есть пустые ячейки для фрагментов.", "Name": "Пустые Ячейки Фрагментов" }, "InvalidMods": { "Description": "Некоторые модификаторы в этом комплекте устарели или иным образом не помещаются ни в одну из ваших частей брони.", "Name": "Устаревшие Модификаторы" }, "InvalidSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer that is not valid.", "Name": "Неверный Поисковый Запрос" }, "ItemsDoNotMatchSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer, and that search query excludes at least one of the items in the loadout.", "Name": "Search Excludes Items" }, "MissingItems": { "Description": "Некоторые предметы из этого комплекта больше не находятся в вашем инвентаре.", "Name": "Отсутствующие Предметы" }, "ModsDontFit": { "Description": "Броня в этом комплекте не может вместить все модификаторы комплекта, даже если броня была улучшена.", "Name": "Неназначенные Модификаторы" }, "NeedsArmorUpgrades": { "Description": "Armor in this loadout needs to be upgraded to accommodate all mods or reach specified stats.", "Name": "Необходимо Улучшение Брони" }, "NotAFullArmorSet": { "Description": "Этот комплект не может быть проанализирован, поскольку он не включает в себя полный набор брони.", "Name": "Не Полный Набор Брони" }, "TooManyFragments": { "Description": "В подклассе выбрано больше фрагментов, чем это позволяют аспекты.", "Name": "Слишком Много Фрагментов" }, "UsesSeasonalMods": { "Description": "Этот комплект полагается на модификаторы, которые доступны только в некоторых сезонах. Когда сезон завершится, некоторые модификаторы будут недоступны или превысят энергетическую вместимость брони.", "Name": "Использует Сезонные Модификаторы" } }, "LoadoutBuilder": { "All": "Все", "AlwaysAutoMods": "Artifice and Tuning mods will always be chosen automatically.", "AnyExotic": "Любой экзот", "AnyExoticDescription": "Наборы должны содержать экзоты, но любой экзот подойдёт.", "Artifice": "Искусная", "AssumeMasterwork": "Предположить Абсолют", "AssumeMasterworkOptions": { "All": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nArmor 2.0 Exotics: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Улучшено для получения ячейки модификатора искусной брони.", "Current": "Текущие характеристики, предполагаемый уровень энергии как минимум {{minLoItemEnergy}}.", "Legendary": "Легендарная: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nЭкзотическая: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Full masterwork stat bonuses, assumed energy level at least 10.", "None": "Вся броня: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "Автоматически добавлять моды характеристик", "AutomaticallyPicked": "Этот модификатор был добавлен автоматически для улучшения характеристик сборки.", "CompareLoadout": "Сравнить Наборы", "ConfirmOverwrite": "Выбранные предметы заменят аналоги в Наборе \"{{name}}\". Продолжаем? ", "DecreaseStatPriority": "Понизить приоритет характеристики", "DisabledByAutoStatMods": "Модификаторы характеристик были автоматически выбраны Конструктором Комплектов.", "DisabledDueToMaintenance": "Конструктор Наборов сейчас отключён из-за техобслуживания Bungie API.", "EquipItems": "Надеть", "ExcludeItem": "Исключить", "ExcludeVendors": "Search \"not:vendor\" to exclude vendor items from Loadout Optimizer.", "ExcludedItems": "Исключённые Предметы", "ExistingLoadout": "Существующий Набор", "Exotic": "Экзотическая Броня", "ExoticClassItemPerks": "If you want specific perks, use searches like exactperk:\"spirit of verity\". Click perks in the Optimizer results to add or remove them from the item filter.", "ExoticSpecialCategory": "Специальные", "FOTLWildcardWarning": "This set contains a Festival of the Lost mask. Manually apply the correct mod to activate desired set bonuses.", "Filter": "Настройки", "IgnoreStat": "Если не отмечено, Оптимизатор Комплекта будет притворяться, что этой характеристики не существует при создании наборов", "IncreaseStatPriority": "Повысить приоритет характеристики", "Legendary": "Легендарный", "LimitToNewFeaturedGear": "Limit to new/featured gear", "LockItem": "Закрепить", "MissingClass": "Набор для: {{className}}", "MissingClassDescription": "Набор, который вы пытаетесь просмотреть, предназначен для отсутствующего у вас класса персонажа.", "MwExotic": "Экзотическая", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "Активный поисковой запрос ограничивает предметы, которые DIM может включить в комплекты", "AllowAutoStatMods": "Разрешить DIM автоматически добавлять дополнительные моды характеристик", "AlwaysInvalidMods": "Эти моды не помещаются ни в один из ваших предметов:", "AssumeMasterworked": "Разрешить DIM рекомендовать улучшать броню до Абсолюта", "AssumptionsRestricted": "DIM не может рекомендовать изменения энергии брони:", "BadSlot": "В слоте {{bucketName}}, ни один из разрешённых предметов не может вместить эти моды:", "ExoticDoesNotExist": "You don't have any of the selected exotic armor in your inventory.", "Header": "Не были найдены комплекты. Вот возможные причины почему DIM не смог найти какие-либо комплекты:", "LowerBoundsFailed": "Many sets did not meet minimum stat requirements", "MaybeAllowMoreItems": "Подумайте о том, чтобы разрешить другие предметы:", "MaybeDecreaseLowerBounds": "Consider reducing minimum stat requirements", "MaybeRemoveMods": "Подумайте об удалении некоторых модов:", "MaybeRemoveSearchQuery": "Подумайте об очистке или изменении фильтра в поисковой строке", "ModAssignmentFailed": "Многие наборы не могут вместить все запрошенные моды", "RemoveMods": "Убрать данные моды", "RemoveSetBonuses": "Consider removing some set bonuses", "SetBonuses": "You have chosen some set bonuses, maybe you don't have the right items to use them." }, "NoExotic": "Без экзота", "NoExoticDescription": "Эквивалентно поиску \"not:exotic\" в поисковой строке - наборы не будут использовать никакую экзотическую броню.", "NoExoticPreference": "Никакой Экзот Не Выбран", "NoExoticPreferenceDescription": "Экзотическая броня будет использоваться, если она максимизирует характеристики.", "NoLoadoutsToCompare": "Нет Наборов для сравнения", "None": "Отсутствует", "OptimizerExplanationGuide": "Прочтите руководство пользователя для получения дополнительной информации и видеоуроков.", "OptimizerExplanationMods": "Choose an exotic, mods, and a subclass. These will contribute stats to the build, while any mods already on the armor are ignored.", "OptimizerExplanationSearch": "Use the search bar to narrow down which armor to consider, e.g. {{example}}. If no armor in a slot matches the search, all items will be considered for that slot.", "OptimizerExplanationStats": "Drag the most important stats to the top, and uncheck stats you don't want to maximize.", "OptimizerSet": "Набор Оптимизатора", "PinnedItems": "Закрепленные Предметы", "PinnedItemsFinePrint": "Search filters are saved with Loadout Optimizer settings, but pinned and excluded items are not. Pins and exclusions will be ignored when DIM checks existing Loadouts for better stat builds.", "ProcessingSets": "Finding highest stat sets...", "SaveAs": "Сохранить как", "SetBonus": "Set Bonuses", "SpeedReport": "Evaluated {{combos, number}} combinations in {{time}} seconds using {{cpus}} CPU cores.", "StatConstraints": "Stat Priorities & Ranges", "StatMax": "Макс.", "StatMin": "Min", "StatRangeTooltip": "With the current min/max setting, loadouts exist which have {{min}} to {{max}} points in this stat. Double-click to set min to {{max}}.", "StatTotal": "Total: {{total}}", "TierNumber": "Т{{tier}}", "UnableToAddAllMods": "Не удалось добавить все моды.", "UnableToAddAllModsBody": "Не хватает слотов для модов для применения {{mods}}.", "UnlockItem": "Открепить" }, "LoadoutFilter": { "Contains": "Отображает комплекты, в которых есть предмет или модификатор, соответствующий тексту фильтра. Ищите предметы с пробелами в имени используя кавычки.", "FashionOnly": "Отображает комплекты, которые содержат только наряды (шейдеры или орнаменты).", "LoadoutLight": "Показывает комплекты на основе рассчитанного уровня силы. Используйте ключевое слово pinnaclecap или softcap вместо числа, чтобы ссылаться на ограничения силы текущего сезона.", "ModsOnly": "Отображает комплекты, которые содержат только модификаторы брони.", "Name": "Отображает комплекты, названия которых совпадает (exactname:) или частично совпадает (name:) с введённым текстом. Ищите целые фразы, используя кавычки.", "Notes": "Поиск комплектов по их полю заметок.", "PartialMatch": "Показывает комплекты, чьи имена или заметки частично совпадают с введённым текстом. Ищите целые фразы, используя кавычки.", "Season": "Отображает комплекты по тому, в каком сезоне Destiny 2 они были изменены в последний раз.", "Subclass": "Показывает комплекты, чьи названия подклассов или типов урона частично совпадают с текстом фильтра." }, "Loadouts": { "Abilities": "Способности", "Actions": "Действия для {{title}}", "AddEquippedItems": "Добавить экипированное", "AddNotes": "Добавить заметки", "AddUnequippedItems": "Добавить неэкипированное", "Any": "Любой класс", "Apply": "Применить", "ApplyInGameLoadoutInGame": "Ваш комплект готов к экипированию, но так как вы находитесь в активности, вам нужно будет экипировать его в игре.", "ApplyMods": "Применение модов", "ApplySearch": "Передаю запрос \"{{query}}\"", "ArmorStats": "Характеристики брони", "ArtifactUnlocks": "Разблокировка артефакта", "ArtifactUnlocksDesc": "Из-за ограничений Bungie.net, DIM не может автоматически сконфигурировать ваш артефакт. Перед применением комплекта вам будет необходимо применить эти изменения в игре.", "ArtifactUnlocksWithSeason": "Разблокировка артефакта – Сезон {{seasonNumber}}", "BadLoadoutShare": "Не удается загрузить общий набор", "BadLoadoutShareBody": "Набор, который вы пытаетесь загрузить, недействителен: {{error}}", "Before": "Перед '{{name}}'", "CancelEditing": "Отменить редактирование", "CannotCustomizeSubclass": "Этот подкласс не может быть настроен", "ChooseItem": "Добавить {{name}}", "ClassType": "Any class loadout", "ClassTypeMismatch": "Предмет {{className}} не может быть добавлен в этот набор", "ClassTypeMissing": "У вас нет {{className}} для создания набора для", "ClassType_female": "{{className}} loadout", "ClassType_male": "{{className}} loadout", "Classified": "Некоторые ваши предметы секретны, и не могут быть включены в расчет максимально возможной Силы.", "ClearLoadoutParameters": "Убрать настройки Конструктора Наборов", "ClearSection": "Убрать всё", "ClearSpace": "Переместить остальное", "ClearSpaceArmor": "Переместить другую броню", "ClearSpaceWeapons": "Переместить другие оружия", "ClearUnsetMods": "Убрать другие моды", "ClearingSpace": "Перемещение других предметов", "CopyAndEdit": "Изменить Копию", "Create": "Создать Набор", "CurrentlyEquipped": "В настоящее время экипировано", "Deequip": "Снятие экипировки с предметов других персонажей", "Delete": "Удалить", "DimLoadouts": "Наборы DIM", "Edit": "Изменить Набор", "EditBrief": "Изменить", "EquipInGameLoadout": "Экипирование внутриигрового комплекта", "EquipItems": "Экипирование предметов", "EquippableDifferent1": "Несколько экзотических предметов было использовано при подсчёте вашей максимальной Силы, потому отображаемое число может быть недостижимо при экипировании ваших предметов в игре.", "EquippableDifferent2": "Максимальная Сила не ограничена правилом \"Один Экзот\" при определении силы выпадающих вам предметов, мощных и сверхмощных наград.", "Failed": "Не удалось применить набор полностью", "Fashion": "Выбрать стиль", "FashionOnly": "Только Наряды", "FillFromEquipped": "Заполнить с экипированными предметами", "FillFromInventory": "Заполнить с неэкипированными предметами", "FilteredItems": "Отфильтрованные предметы", "FindAnother": "Найти следующий {{name}}", "FromEquipped": "Из экипированных", "Generated": "{{statTotal}} Stat Point Loadout", "HashtagTip": "Совет: Используйте #хэштеги в названиях ваших комплектов или в заметках и они будут отображаться здесь.", "Import": { "BadURL": "Недопустимая ссылка на комплект.", "Error": "Ошибка получения комплекта:", "Error404": "Этот комплект не существует.", "PasteHere": "Вставьте ссылку с комплектом, чтобы открыть комплект." }, "ImportLoadout": "Импортировать Комплект", "InGameActions": "Действия с Внутриигровым Комплектом", "InGameLoadouts": "Внутриигровые комплекты", "IncludeRuntimeStatBenefits": "Включать характеристики модификатора Источник", "IncludeRuntimeStatBenefitsDesc": "Модификаторы для брони \"Источник ...\" дают фиксированный прирост к характеристикам персонажа, пока у вас есть Заряды Брони.\n\nС этой настройкой DIM считает эти модификаторы активированными и добавляет их эффекты к характеристикам этого Комплекта при расчётах и оптимизации.", "ItemErrorSummary_few": "{{count}} ошибок предмета:", "ItemErrorSummary_many": "{{count}} ошибок предмета:", "ItemErrorSummary_one": "1 ошибка предмета:", "ItemErrorSummary_other": "{{count}} ошибок предмета:", "ItemLeveling": "Прокачивание предмета", "LoadoutName": "Назовите Набор", "LoadoutParameters": "Настройки Конструктора Наборов", "LoadoutParametersExotic": "Комплект должен включать этот экзот: {{exoticName}}", "LoadoutParametersQuery": "Предметы должны соответствовать этому фильтру поиска", "LoadoutParametersStats": "Stat priorities and minimum/maximum stat ranges", "Loadouts": "Наборы", "MakeRoom": "Освободить место для Почтмейстера", "MakeRoomDone_female_few": "Закончено освобождение места для {{count}} предметов Почтмейстера, перемещением {{movedNum}} предметов {{store}}.", "MakeRoomDone_female_many": "Закончено освобождение места для {{count}} предметов Почтмейстера, перемещением {{movedNum}} предметов {{store}}.", "MakeRoomDone_female_one": "Закончено освобождение места для 1 предмета Почтмейстера, перемещением 1 предмета {{store}}.", "MakeRoomDone_female_other": "Закончено освобождение места для {{count}} предметов Почтмейстера, перемещением {{movedNum}} предметов {{store}}.", "MakeRoomDone_few": "Закончено освобождение места для {{count}} предметов Почтмейстера, перемещением {{movedNum}} предметов {{store}}.", "MakeRoomDone_male_few": "Закончено освобождение места для {{count}} предметов Почтмейстера, перемещением {{movedNum}} предметов {{store}}.", "MakeRoomDone_male_many": "Закончено освобождение места для {{count}} предметов Почтмейстера, перемещением {{movedNum}} предметов {{store}}.", "MakeRoomDone_male_one": "Закончено освобождение места для 1 предмета Почтмейстера, перемещением 1 предмета {{store}}.", "MakeRoomDone_male_other": "Закончено освобождение места для {{count}} предметов Почтмейстера, перемещением {{movedNum}} предметов {{store}}.", "MakeRoomDone_many": "Закончено освобождение места для {{count}} предметов Почтмейстера, перемещением {{movedNum}} предметов {{store}}.", "MakeRoomDone_one": "Закончено освобождение места для 1 предмета Почтмейстера, перемещением 1 предмета {{store}}.", "MakeRoomDone_other": "Закончено освобождение места для {{count}} предметов Почтмейстера, перемещением {{movedNum}} предметов {{store}}.", "MakeRoomError": "Невозможно освободить место для всех предметов Почтмейстера: {{error}}.", "ManageLoadouts": "Управлять Наборами", "MaxSlots": "Вы можете иметь только {{slots}} {{bucketName}} в наборе.", "MaximizeLight": "Максимальный уровень Силы", "MaximizePower": "Максимум Силы", "MaximizeStat": "Максимизировать Характеристику", "MissingItemsWarning": "Некоторые предметы из этого комплекта больше не находятся в вашем инвентаре.", "ModErrorSummary_few": "{{count}} ошибки с модами:", "ModErrorSummary_many": "{{count}} ошибки с модами:", "ModErrorSummary_one": "1 ошибка мода:", "ModErrorSummary_other": "{{count}} ошибки с модами:", "ModPlacement": { "InvalidMods": "Недействительные моды", "InvalidModsDesc_few": "{{count}} модификаторов не вошли ни в одну из частей брони.", "InvalidModsDesc_many": "{{count}} модификаторов не вошли ни в одну из частей брони.", "InvalidModsDesc_one": "1 модификатор не вошел ни в одну из частей брони.", "InvalidModsDesc_other": "{{count}} модификаторов не вошли ни в одну из частей брони.", "ModPlacement": "Расположение модификаторов", "StackableMod": "Складываются", "UnassignedMods": "Неназначенные Модификаторы", "UnassignedModsDesc_few": "{{count}} модификаторов не вошло из-за нехватки энергии брони или слотов под модификаторы. Увеличение энергии для выбранной брони не устранит проблему.", "UnassignedModsDesc_many": "{{count}} модификаторов не вошло из-за нехватки энергии брони или слотов под модификаторы. Увеличение энергии для выбранной брони не устранит проблему.", "UnassignedModsDesc_one": "1 модификатор не вошел из-за нехватки энергии брони или слотов под модификаторы. Увеличение энергии для выбранной брони не устранит проблему.", "UnassignedModsDesc_other": "{{count}} модификаторов не вошло из-за нехватки энергии брони или слотов под модификаторы. Увеличение энергии для выбранной брони не устранит проблему.", "UnstackableMod": "Не Складываются", "UpgradeCosts": "Стоимость улучшений", "UpgradeCostsDesc": "Некоторой броне нужны улучшения энергоёмкости, чтобы вместить запрошенные модификаторы. Итого, эти улучшения стоят:" }, "Mods": "Моды", "ModsOnly": "Только Модификаторы", "MoveItems": "Перемещение предметов", "NoSpace": "Недостаточно места в хранилище и у остальных персонажей.", "NoneMatch": "Ни один из ваших комплектов не соответствует фильтрам.", "NotStarted": "Ожидание выполнения других действий или завершение загрузки обновления инвентаря", "NotesPlaceholder": "Напишите заметки об этом комплекте, или используйте #хэштеги для категоризации", "NotificationTitle": "Снаряжение: {{name}}", "OnWrongCharacterAdvice": "Нажмите здесь, чтобы найти предметы c наивысшей Силой этого персонажа.", "OnWrongCharacterWarning": "Это самая мощная броня персонажа на другом персонаже. Для того чтобы засчитываться в Силу падений, мощных и сверхмощных наград, броня должна быть на этом персонаже или в Хранилище.", "OnlyItems": "Только надеваемые предметы, материалы и расходные материалы могут быть добавлены к набору.", "OpenInOptimizer": "Оптимизировать броню", "OpenOnStreamDeck": "Open on Stream Deck", "PickArmor": "Выбрать броню", "PickMods": "Добавить моды брони", "Prismatic": { "Aspect": "Призматический Аспект", "Grenade": "Призматическая Граната", "Melee": "Призматическая Способность Ближнего Боя", "Super": "Суперспособность" }, "PullFromPostmaster": "Забрать предметы Почтмейстера", "PullFromPostmasterError": "Не удалось вытащить из Почтмейстера: {{error}}.", "PullFromPostmasterGeneralError": "Не удалось получить все предметы с Почтмейстера.", "PullFromPostmasterNotification_female_few": "Вытаскивание {{count}} предметов из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_female_many": "Вытаскивание {{count}} предметов из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_female_one": "Вытаскивание 1 предмета из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_female_other": "Вытаскивание {{count}} предметов из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_few": "Вытаскивание {{count}} предметов из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_male_few": "Вытаскивание {{count}} предметов из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_male_many": "Вытаскивание {{count}} предметов из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_male_one": "Вытаскивание 1 предмета из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_male_other": "Вытаскивание {{count}} предметов из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_many": "Вытаскивание {{count}} предметов из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_one": "Вытаскивание 1 предмета из Почтмейстера в {{store}}.", "PullFromPostmasterNotification_other": "Вытаскивание {{count}} предметов из Почтмейстера в {{store}}.", "PullFromPostmasterPopupTitle": "Вытащить из Почтмейстера", "Random": "Случайно", "Randomize": "Случайное снаряжение", "RandomizeButton": "Рандомизировать", "RandomizeNew": "Создать случайный", "RandomizeQueryHint": "Совет: сначала воспользуйтесь поиском, чтобы ограничить то, какие предметы могут быть выбраны случайно.", "RandomizeSearch": "Случайно на основе Поиска", "RandomizeSearchPrompt": "Надеть случайные предметы из результатов поискового запроса \"{{query}}\"?", "Redo": "Повторить", "RestoreAllItems": "Все предметы", "SalvationsEdgeMods": "Salvation's Edge Mods", "Save": "Сохранить", "SaveAsDIM": "Сохранить как Набор DIM", "SaveAsNew": "Сохранить как новый", "SaveAsNewTooltip": "Оставить исходный набор и сохранить его в качестве нового набора", "SaveDisabled": { "AlreadyExists": "Выберите новое имя для комплекта.", "Empty": "Этот комплект пуст.", "NoName": "Комплект должен иметь имя." }, "SaveLoadout": "Сохранить Набор", "Season": "Сезон {{season}}", "SetBonusesDesc": "Required set bonuses", "Share": { "Copied": "Ссылка набора скопирована в буфер обмена", "CopyButton": "Скопировать ссылку", "Error": "Ошибка при получении общей ссылки", "Fashion": "Стиль (шейдеры и орнаменты)", "LoadoutOptimizer": "Настройки Конструктора Наборов", "NativeShare": "Поделиться ссылкой", "Notes": "Заметки", "NumItems_few": "{{count}} предмета(-ов) - получателю будет предложено выбрать схожие предметы из его инвентаря", "NumItems_many": "{{count}} предмета(-ов) - получателю будет предложено выбрать схожие предметы из его инвентаря", "NumItems_one": "{{count}} предмет - получателю будет предложено выбрать схожий предмет из его инвентаря", "NumItems_other": "{{count}} предмета(-ов) - получателю будет предложено выбрать схожие предметы из его инвентаря", "NumMods_few": "{{count}} мода(-ов)", "NumMods_many": "{{count}} мода(-ов)", "NumMods_one": "{{count}} мод", "NumMods_other": "{{count}} мода(-ов)", "Placeholder": "Идёт загрузка общей ссылки", "Subclass": "Настройка подкласса", "Summary": "Поделиться этим набором содержащим:", "Title": "Поделиться \"{{name}}\"" }, "ShareLoadout": "Поделиться", "ShowModPlacement": "Показать расположение модов", "Snapshot": "Сохранить как Набор в игре", "SocketOverrides": "Изменение параметров подкласса", "SortByEditTime": "Сортировать по дате обновления", "SortByName": "Сортировать по имени", "SubclassOptions": "Параметры {{subclass}}", "SubclassOptionsSearch": "Искать параметры {{subclass}}", "Succeeded": "Набор применён успешно", "SyncFromEquipped": "Синхронизировать с экипированными предметами", "TooManyRequested": "В наличии {{total}} {{itemname}}; Набор запрашивает {{requested}}. Передали все замеченные.", "TuningMods": "Tuning Mods", "UnassignedModError": "Мод не поместился на вашей текущей броне", "Undo": "Отменить", "Update": "Сохранить изменения", "UpdateLoadout": "Обновить Набор", "VendorsCannotEquip": "Предметы не найдены. ЛКМ для выбора предмета на замену или Х для очистки:" }, "Manifest": { "Download": "Загрузка последней базы данных Destiny с Bungie.net...", "Error": "Ошибка загрузки базы данных Destiny:\n{{error}}\nОбновите страницу.", "Load": "Загрузка базы данных Destiny..." }, "Milestone": { "Daily": "Ежедневное Испытание", "OneTime": "Уникальное Испытание", "SeasonalRank": "Сезонный Ранг {{rank}}", "Special": "Испытание События", "Tutorial": "Вступительное Испытание", "Unknown": "Испытание", "Weekly": "Еженедельное Испытание" }, "Mods": { "HarmonicModDescription": "Эффект этого модификатора достигается при уменьшенной стоимости и меняется в зависимости от экипированного подкласса." }, "MoveAmount": { "Amount": "Количество:" }, "MovePopup": { "Acquired": "Предмет доступен в Коллекции.", "AcquiredMod": "Этот мод разблокирован в коллекции.", "AddNote": "Добавить заметки", "AddToLoadout": "В наборе", "AddToLoadoutTitle": "Добавить это в комплект", "All": "Все", "ArtifactBreaker": "This weapon has {{breaker}} because of an unlocked artifact perk.", "CannotCurrentlyRoll": "Этот перк не доступен в текущей версии этого предмета.", "CantPullFromPostmaster": "Вы должны посетить Почтмейстера в игре чтобы вытащить этот предмет.", "CatalystProgress": "Прогресс Катализатора", "CommunityData": "Мнение Сообщества", "Consolidate": "Собрать все", "DistributeEvenly": "Распределить равномерно", "EnhancementTier": "Ранг {{tier}}", "Equip": "Экипировать на:", "EquipWithName": "Надеть на {{character}}", "FavoriteUnFavorite": { "Favorite": "Добавить {{itemType}} в избранное", "Favorited": "В избранном", "Unfavorite": "Исключить {{itemType}} из избранного", "Unfavorited": "Исключено из избранных" }, "Infuse": "На синтез", "InfuseTitle": "Открыть поиск топлива для синтеза", "IntrinsicBreaker": "This weapon intrinsically has {{breaker}}.", "LoadingSockets": "Перки и подробности характеристик для этого предмета еще не загружены.", "LockUnlock": { "AutoLock": "Состояние блокировки синхронизировано с тегом предмета", "Lock": "Заблокировать {{itemType}}", "Locked": "Заблокировано", "Unlock": "Разблокировать {{itemType}}", "Unlocked": "Разблокировано" }, "MissingSockets": "Описания перков и модов недоступны во время обслуживания Bungie. Доступ появляется в течении нескольких часов после завершения обслуживания.", "Notes": "Заметки:", "OpenOnStreamDeck": "Open on Stream Deck", "OverviewTab": "Обзор", "Owned": "Этот предмет находится в вашем инвентаре.", "OwnedMod": "Этот мод находится в вашем инвентаре модификаций.", "PullItem": "Вытаскивание из {{bucket}} в {{store}}.", "PullPostmaster": "Вытащить из Почтмейстера", "ReadLore": "Читать лор на портале Ishtar Collective", "ReadLoreLink": "История", "Rewards": "Награды:", "SendToVault": "В Хранилище", "Store": "Передать:", "StoreWithName": "Передать к {{character}}", "Subtitle": { "QuestProgress": "Этап {{questStepNum}} из {{questStepsTotal}}", "Type": "{{classType}} {{typeName}}" }, "TabList": "Вкладки сведений о предмете", "ToggleSidecar": "Скрыть/Показать действия", "TrackUntrack": { "Track": "Следить за {{itemType}}", "Tracked": "Отслеживаем", "Untrack": "Не следить за {{itemType}}", "Untracked": "Отслеживание" }, "TriageTab": "Приоритет", "UnreliablePerkOption": "Этот перк отображается только при просмотре в Коллекции. Он не может случайным образом выпасть на этот предмет.", "Vault": "Хранилище", "WeaponLevel": "Уровень оружия {{level}}" }, "Notes": { "Error": "Ошибка! Макс 120 символов для заметок.", "Help": "Добавить заметки, #хештеги и :symbols:" }, "Notification": { "Cancel": "Отмена", "OK": "Закрыть" }, "Objectives": { "Complete": "Готово", "Incomplete": "Не готово" }, "Organizer": { "BulkMove": "Передать", "BulkMoveLoadoutName": "Выбранный в Органайзере", "BulkTag": "Тэг", "Columns": { "Ammo": "Ammo", "Archetype": "Архетип", "BaseStats": "Базовые Характеристики", "Breaker": "Воитель", "Crafted": "Дата Формирования", "CustomTotal": "Своя Сумма", "Damage": "Стихия", "Energy": "Энергия", "Event": "Мероприятие", "Featured": "New Gear", "Foundry": "Производитель", "Frame": "Frame", "Harmonizable": "Гармонизируемое", "Holofoil": "Holofoil", "Icon": "Значок", "ItemTier": "Tier", "KillTracker": "Kills", "Level": "Уровень", "Loadouts": "Наборы", "Location": "Где лежит", "Locked": "Заблокировано", "MasterworkStat": "MW Stat", "MasterworkTier": "MW Tier", "ModSlot": "Слот для модов", "Mods": "Моды", "Name": "Имя", "New": "Новый", "Notes": "Заметки", "OriginTraits": "Исходная Особенность", "OtherPerks": "Weapon Components", "PercentComplete": "% Выполнения", "Perks": "Бонусы", "PerksGrid": "Perks Grid", "Power": "Сила", "Quality": "Качество %", "Recency": "Новизна", "Season": "Сезон", "Shaders": "Украшения", "Source": "Где взять", "StatQuality": "Качество Характеристик", "StatQualityStat": "{{stat}}%", "Stats": "Характеристики", "Tag": "Тэг", "TertiaryStat": "3rd Stat", "Tier": "Rarity", "Traits": "Бонусы оружия", "TuningStat": "Tuner", "WishList": "Wish List", "WishListNotes": "Заметки к Списку Желаний", "Year": "Год" }, "EnabledColumns": "Выбор столбцов", "Lock": "Заблокировать", "NoItems": "Нет предметов, соответствующих фильтрам. Если введён поисковый запрос, попробуйте очистить его.", "NoMobile": "Для открытия Органайзера поверните телефон в альбомную ориентацию.", "Note": "Добавить заметки", "OpenIn": "Показать в Органайзере", "Organizer": "Органайзер", "SelectAll": "Выбрать всё", "SelectItem": "Выбрать/Снять выделение с {{name}}", "ShiftTip": "Совет: [Shift]+[ЛКМ] по значению (любому, кроме Значка) в полученном результате отфильтрует по нему выборку.", "Stats": { "Aim": "Помощь в прицеливании", "Airborne": "Эффективность в воздухе", "AmmoGeneration": "Ammo Gen", "Power": "Сила", "RPM": "Выстрелов в минуту", "Recoil": "Отдача", "Reload": "Скорость перезарядки" }, "Unlock": "Разблокировать" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "У Почтмейстера заканчивается место! ({{number}}/{{postmasterSize}})", "PostmasterFull": "У Почтмейстера нет места! ({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "Баунти", "CatalystSource": "Source: {{source}}", "CrucibleRank": "Ранги", "Items": "Предметы Заданий", "Milestones": "Испытания", "NoEventChallenges": "Вы выполнили все испытания мероприятия", "NoTrackedTriumph": "Нет Отслеживаемых Триумфов. Отслеживайте сколько угодно в DIM.", "PaleHeartPathfinder": "Навигатор Бледного Сердца", "PercentMax": "{{pct}}% to maximum", "PercentPrestige": "{{pct}}% обнуления", "PointsUsed_few": "{{count}} очков использовано", "PointsUsed_many": "{{count}} очков использовано", "PointsUsed_one": "1 очко использовано", "PointsUsed_other": "{{count}} очков использовано", "PowerBonusHeader": "+{{powerBonus}} Сила Наград", "PowerBonusHeaderUndefined": "Другие награды", "Progress": "Прогресс", "QueryFilteredTrackedTriumphs": "Нет Отслеживаемых триумфов удовлетворяющих условиям поиска", "QuestExpired": "Просрочен", "QuestExpires": "Устареет через:", "Quests": "Квесты", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}", "Resets_few": "{{count}} обнулений ранга", "Resets_many": "{{count}} обнулений ранга", "Resets_one": "1 сброс", "Resets_other": "{{count}} обнулений ранга", "RewardPassEndsIn": "Reward Pass ends in ", "RewardPassPrestigeRank": "Prestige Rank {{rank}}", "SeasonalHub": "Seasonal Hub", "StatTrackers": "Трекеры", "TrackedTriumphs": "Отслеживаемые Триумфы" }, "RecordBooks": { "HideCompleted": "Скрыть завершенные", "RecordBooks": "Таблица Рекордов" }, "Records": { "Title": "Записи", "UniversalOrnamentSetOther": "Прочее" }, "SearchHistory": { "Date": "Дата последнего Поиска", "DeleteAll": "Удалить все неизбранные поиски", "Description": "Избранные Поиски и их история. Можете удалять их на ваше усмотрение.", "Item": "Поиски Предметов", "Link": "Смотреть и редактировать запросы Поиска", "Loadout": "Поиски Комплектов", "Query": "Запрос Поиска", "Title": "История Поисков", "UsageCount": "# Раз" }, "Settings": { "Appearance": "Appearance", "ArmorArchetypeModslot": "Armor Archetype / Modslot", "AutoLockTagged": "Синхронизировать состояние блокировки с тегом", "AutoLockTaggedExplanation": "DIM будет автоматически блокировать и разблокировать предметы для соответствия с тегом. Изготовленные предметы останутся разблокированными, чтобы было доступно трансформирование. Когда эта настройка активирована, значок блокировки не будет отображен на ячейке с предметом для отмеченных тегом предметов.", "BadgePostmaster": "Показать количество предметов в почтмейстере для текущего персонажа на значке приложения", "BadgePostmasterExplanation": "Для этого необходимо установить DIM в качестве приложения, и ваша система должна поддерживать отображение значков", "BothDescriptions": "Оба описания", "BungieDescriptionOnly": "Описания от Bungie", "CharacterOrder": "Сортировать персонажей по", "CharacterOrderFixed": "Возраст персонажа (может глючить на ПК)", "CharacterOrderRecent": "активность", "CharacterOrderReversed": "активность (реверс)", "ColumnSize": "{{num}} предметов", "ColumnSizeAuto": "Авто", "CommunityData": "Мнения Перков Сообщества", "CommunityDescriptionOnly": "Описания от сообщества", "CsvImport": "Импорт CSV", "CustomErrorLabel": "Имя характеристики должно содержать символы слов и отличаться от других имён характеристик для этого класса стража.", "CustomErrorValues": "Веса характеристик должны быть положительными числами.\nКак минимум 2 веса характеристик должны быть больше нуля.", "CustomStatChooseName": "Введите название Пользовательской Характеристики", "CustomStatCreate": "Создать новую пользовательскую характеристику", "CustomStatDelete": "Удалить эту Пользовательскую Характеристику", "CustomStatDeleteConfirm": "Удалить эту Пользовательскую Характеристику?", "CustomStatDesc1": "Выберите желаемые характеристики брони для создания Пользовательской суммы характеристик.", "CustomStatDesc3": "Пользовательские характеристики будут отображаться во Всплывающем окне предмета, Органайзере и в Сравнении.", "CustomStatTitle": "Пользовательская Сумма Характеристик", "Data": "Таблицы", "DefaultItemSizeNote": "При 50px предметы выглядят максимально четко. Ни картинка, ни текст не будут искажаться или размываться.", "DontForgetDupes": "Не забывайте, что вы можете использовать is:dupe в поиске, чтобы быстро найти дубликаты предметов, и оценивать их при помощи инструмента сравнения или организатора.", "EnableAdvancedStats": "Показать рейтинг качества характеристик брони (D1)", "ExpandSingleCharacter": "Вернуть персонажей", "ExportLoadoutSS": "Loadout spreadsheets", "ExportLoadoutSSHelp": "Download a CSV list of your DIM Loadouts that can be easily viewed in the spreadsheet app of your choice.", "ExportProfile": "Экспорт ответа профиля API", "ExportSS": "Скачать список инвентаря в виде таблицы", "ExportSSHelp": "Скачать список ваших предметов в формате CSV, который может быть легко просмотрен в любом приложении для работы с таблицами по вашему выбору.", "HidePullFromPostmaster": "Скрыть кнопку \"$t(Loadouts.PullFromPostmaster)\"", "Inventory": "Инвентари", "InventoryColumns": "Ширина инвентарей", "InventoryColumnsMobile": "Ширина [в предметах] столбца персонажа в мобильной портретной ориентации", "InventoryColumnsMobileLine2": "Размер предметов будет изменён, чтобы соответствовать новым настройкам", "InventoryNumberOfSpacesToClear": "Number of empty spaces to make when using Farming Mode", "Items": "Предметы", "Language": "Язык", "LogOut": "Выйти", "Masterworked": "В абсолюте", "MaxParallelCores": "Maximum cores for parallel tasks", "MaxParallelCoresExplanation": "Controls how many CPU cores DIM can use for intensive tasks like Loadout Optimizer and Loadout Analyzer. Higher values may improve performance but use more system resources.", "OrnamentDisplay": "Show Ornaments on item tiles", "OrnamentDisplayExplanationDisabled": "Items will never display their ornaments", "OrnamentDisplayExplanationEnabled": "Hovering or long-pressing armor will hide its ornament", "OrnamentDisplayExplanationHide": "Hovering or long-pressing an item will hide its ornament", "OrnamentDisplayExplanationShow": "Hovering or long-pressing an item will show its ornament", "ResetToDefault": "Сбросить", "RestoreVaultSide": "Show vaulted items in their own column", "ReverseSort": "Включить сортировку вперёд/наоборот", "SetSort": "Порядок Сортировки предметов:", "SetVaultWeaponGrouping": "Группировать оружия из хранилища по:", "Settings": "Настройки", "ShowNewItems": "Показывать красную точку на новых предметах", "SingleCharacter": "Просмотр одного персонажа", "SingleCharacterExplanation": "DIM будет отображать только последнего персонажа, на котором вы играли. Предметы, принадлежащие скрытым персонажам, будут отображаться в хранилище, если они могут быть использованы текущим персонажем.\nДругие классовые предметы будут полностью скрыты.", "SizeItem": "Размер предмета", "SortByAmmoType": "Тип боеприпасов", "SortByAmount": "Количество", "SortByClassType": "Класс", "SortByCrafted": "Изготовленное (D2)", "SortByDeepsight": "Резонанс Глубинного зрения (D2)", "SortByFeatured": "New Gear / Featured (D2)", "SortByPrimary": "Уровень Cилы", "SortByRarity": "Редкость", "SortByRating": "Качество брони (D1)", "SortByRecent": "Получены недавно (D2)", "SortBySeason": "Сезон (D2)", "SortByTag": "Тэги ({{taglist}})", "SortByTier": "Tier (D2)", "SortByType": "Тип", "SortByWeaponElement": "Тип Урона", "SortCustom": "Свой порядок", "SortName": "Имя", "SpacesSize_few": "{{count}} spaces", "SpacesSize_many": "{{count}} spaces", "SpacesSize_one": "{{count}} space", "SpacesSize_other": "{{count}} spaces", "Theme": "Тема", "Troubleshooting": "Troubleshooting", "VaultArmorGroupingStyle": "Разделять броню на разные строки по классу", "VaultGroupingNone": "Отсутствует", "VaultUnder": "Show vaulted items under equipped items", "VaultWeaponGroupingStyle": "Разделять группы оружий по разным строкам", "WeaponFrame": "Weapon Frame", "WishlistRefreshNotificationBody": "If you do not see any updates, be sure the source (such as GitHub) reflects them!", "WishlistRefreshNotificationTitle": "Wishlists Reloaded" }, "Sockets": { "ApplyPerks": "Применить перки", "GridStyle": "Показывать перки в виде сетки", "Insert": { "Ability": "Выбрать способность", "Aspect": "Вставить аспект", "Fragment": "Вставить фрагмент", "Mod": "Вставить мод", "Ornament": "Применить орнамент", "Projection": "Применить проекцию Призрака", "Shader": "Применить шейдер", "Super": "Выбрать Суперспособность", "Transmat": "Применить эффект телепортации" }, "ListStyle": "Показать перки в виде списка", "Search": "Поиск по названию или описанию", "Select": { "Ability": "Предпросмотр способности", "Aspect": "Предпросмотр аспекта", "Fragment": "Предпросмотр фрагмента", "Mod": "Предпросмотр мода", "Ornament": "Предпросмотр орнамента", "Projection": "Предпросмотр проекции Призрака", "Shader": "Предпросмотр шейдера", "Super": "Предпросмотр Суперспособности", "Transmat": "Предпросмотр эффекта телепортации" }, "SelectWishlistPerks": "Предпросмотр перков из Списка Желаний" }, "Stats": { "CrouchingSpeed": "Присед", "Custom": "Своя Сумма", "CustomDesc": "Настраиваемая сумма выбранных базовых характеристик без учёта модов или Абсолюта. Выбрать, какие статистики будут включены, вы можете в Настройках.", "DamageResistance": "Сопротивление урону в PvE", "Discipline": "Дисциплина", "DropLevel": "Сила Аккаунта", "DropLevelExplanation1": "Account Power is the base power level when calculating the increased level of rewards.", "DropLevelExplanation2": "Сила Аккаунта использует предмет самого высокого уровня в каждом слоте, независимо от требуемого Класса или правила \"Один Экзотик\".", "EquippableGear": "Экипируемое Снаряжение", "FlinchResistance": "Сопротивление смещению прицела", "HP": "ХП", "Intellect": "Интеллект", "MaxGearPower": "Предметы наибольшей Силы, доступные персонажу", "MaxGearPowerAll": "Сила мощнейших предметов", "MaxGearPowerOneExoticRule": "Максимальная сила экипируемого снаряжения\n(надета только одна Экзотик броня)", "MaxTotalPower": "Максимальная Сила", "MetersPerSecond": "м/с", "Milliseconds": " ms", "NoBonus": "Без бонуса", "NotApplicable": "Н/Д", "OfMaxRoll": "{{range}} от максимального", "PercentHelp": "Дополнительная информация о \"Качестве характеристик\" (ЛКМ).", "Percentage": "%", "PowerModifier": "Бонус Силы сезонного Артефакта", "Prestige": "Уровень Престижа: {{level}}\n{{exp}} опыта до 5 частиц света.", "Quality": "Качество характеристик", "ShieldHP": "ХП Щита", "StrafingSpeed": "Движение в сторону", "Strength": "Стойкость", "TierProgress": "Т{{tier}} {{statName}} ({{progress}}/60 для Т{{nextTier}})\n", "TierProgress_Max": "Т{{tier}} {{statName}} ({{progress}}/300)\n", "TimeToFullHP": "Время до полного ХП", "Total": "Сумма", "TotalHP": "Общее ХП", "WalkingSpeed": "Ходьба", "WeaponPart": "Weapon Part" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM может хранить Тэги, Наборы и Настройки на своих серверах и синхронизировать данные между различными запущеными DIM, без дополнительной авторизации. Существующие данные можно импортировать в Настройках, если вы еще не включали DIM Sync. За возможность реализовать этот функционал благодарим наших спонсоров с OpenCollective!", "No": "Не сейчас", "Title": "Включить DIM Sync?", "Yes": "Включить Синхронизацию" }, "AutoBackup": "Создали Резервную копию в папке Загрузок. Файл dim-data.json, на всякий случай.", "BackUpFirst": "Скачайте Резервную копию перед удалением Всех Данных. На всякий случай.", "BrowserMayClearData": "Браузер может удалить данные, если у вас кончится свободное место или если не посещать DIM часто.", "DataIsLocal": "Только локальные данные тэгов и заметок", "DeleteAllData": "Удалить ВСЕ данные с серверов DIM Sync", "DeleteAllDataConfirm": "Удалить ВСЕ данные ваших аккаунтов с серверов DIM Sync? Эта операция Необратима.", "Details": { "IndexedDBStorage": "Локальное Хранилище Браузера хранит ваши данные в кэше браузера на данном устройстве. Очистка данных удалит данные." }, "DimApiFinePrint": "Сохраним данные Тэгов, Наборов, Настроек DIM и их изменения на своих серверах. \nСинхронизируем эти данные между разными браузерами, устройствами и версиями DIM.", "DimSyncDown": "DIM Sync не подключен из-за проблемы связи с сервером.", "DimSyncEnabled": "Синхронизация DIM Sync включена", "DimSyncNotEnabled": "DIM Sync не включен, поэтому ваши настройки, тэги, комплекты и поиски сохранены только локально и будут потеряны при очистке хранилища браузера. Включите DIM Sync в настройках для автоматического создания резервной копии ваших данных, либо регулярно сохраняйте свои данные вручную.", "EnableDimApi": "Включить DIM Sync (рекомендуется)", "Export": "Скачать Резервную копию", "ExportError": "Не удалось загрузить резервную копию из DIM Sync", "ExportErrorBody": "DIM Sync может быть отключен, или у вас возникли проблемы с подключением. Вместо этого, мы загрузим копию ваших локально сохранённых данных.", "Import": "Импорт из Резервной копии", "ImportConfirmDimApi": "Данные Тэгов, Наборов и Настроек обновятся, полностью заменив текущие. Продолжаем?", "ImportExport": "Резервное копирование и импорт", "ImportFailed": "Ошибка импорта! {{error}}", "ImportNoFile": "Не выбран файл!", "ImportNotification": { "FailedBody": "Невозможно импортировать данные. {{error}}", "FailedTitle": "Ошибка импорта", "NoData": "Наборы или тэги отсутствуют в резервной копии", "SuccessBodyForced": "Импортированы Настройки, {{loadouts}} Наборов и {{tags}} Тэгов предметов из резервной копии на сервера DIM, обновив существующие записи.", "SuccessBodyLocal": "Импортированы настройки, {{loadouts}} загрузок и {{tags}} отмеченных элементов из резервной копии в Локальное Хранилище, обновив существующие записи. Мы не можем гарантировать, что локальный накопитель не будет утерян - попробуйте включить синхронизацию DIM. Мы не гарантируем сохранность данных в Локальном Хранилище - рассмотрите возможность включения DIM Sync.", "SuccessTitle": "Импортирование выполнено успешно" }, "ImportTooManyFiles": "Выберите Один файл для импорта.", "ImportWrongFileType": "JSON не распознан. Скорее всего это не Резервная копия DIM.", "IndexedDBStorage": "Локальное Хранилище Браузера", "LearnMore": "Узнайте больше о синхронизации DIM Sync", "MenuTitle": "Резервные копии и DIM Sync", "ProfileErrorBody": "Произошла проблема связи с DIM Sync. Ваши последние настройки, тэги, наборы, и поиски могут быть не показаны. Ваши данные всё ещё находятся на наших серверах, и любые сделанные вами локально обновления будут сохранены при возобновлении подключения. Мы продолжим повторять попытку, пока DIM открыт.", "ProfileErrorTitle": "Ошибка загрузки DIM Sync", "RefreshDimSync": "Reload remote data from DIM Sync", "UpdateErrorBody": "Возникла проблема с сохранением ваших данных в DIM Sync. Мы продолжим повторять попытку, пока DIM открыт.", "UpdateErrorTitle": "Ошибка сохранения DIM Sync", "UpdateInvalid": "Не удалось сохранить данные в DIM Sync", "UpdateInvalidBody": "Отправленные в DIM Sync данные оказались недействительными и не будут сохранены.", "UpdateInvalidBodyLoadout": "Комплект \"{{name}}\" недопустим и не будет сохранён. Если вы импортировали его с другого сайта, пожалуйста сообщите автору, что они экспортируют недопустимые комплекты.", "UpdateQueueLength_few": "{{count}} новых изменений будут сохранены когда мы сможем переподключиться.", "UpdateQueueLength_many": "{{count}} новых изменений будут сохранены когда мы сможем переподключиться.", "UpdateQueueLength_one": "{{count}} новое изменение будет сохранено когда мы сможем переподключиться.", "UpdateQueueLength_other": "{{count}} новых изменений будут сохранены когда мы сможем переподключиться.", "Usage": "DIM использует {{usage, humanBytes}} из {{quota, humanBytes}} доступного ему места на этом устройстве. В это также входят загруженные базы данных предметов Destiny с Bungie.net." }, "StreamDeck": { "Authorize": "Подключить приложение", "Enable": "Плагин Stream Deck", "Error": { "Body": "There was an error sending data to the Stream Deck plugin. Please contact the plugin developer. {{error}}", "Title": "Stream Deck Plugin Error" }, "FinePrint": "Активировать соединение с плагином Stream Deck для DIM. Этот плагин — отдельный проект, который не был написан командой DIM и не поддерживается ею.", "Install": "Установить плагин", "MissingAuthorization": "Вы должны авторизовать приложение Stream Deck для подключения к DIM. Перейдите в настройки и нажмите «Подключить приложение».", "Tooltip": { "Application": "Приложение Stream Deck", "AuthRequired": "Нажмите эту кнопку или перейдите в настройки и нажмите «Подключить приложение».", "Error": "Ваш плагин Stream Deck больше не поддерживается. Пожалуйста, обновите до последней версии. Этот плагин требует по крайней мере:", "ErrorConnection": "если вы уже используете последнюю версию, проверьте, не блокирует ли соединение какое-либо расширение браузера.", "ExtensionIssue": "Проблема с Расширениями", "Plugin": "Плагин", "Title": "Плагин DIM Stream Deck", "Version": "Версия:" } }, "StripSockets": { "Action": "Очистить Ячейки", "ArmorMods": "{{count}}x Мод Брони", "Button": "Очистить {{numSockets}} Ячеек", "Cancel": "Отмена", "Choose": "Выберите Ячейки для очищения", "DiscountedMods": "{{count}}x Уценённый Мод", "Done": "Очищенные Ячейки", "NoSockets": "Нет Ячеек для очищения", "Ok": "Ок", "Ornaments": "{{count}}x Орнамент", "Others": "{{count}}x Проекция Призрака", "Running": "Очистка Ячеек", "Shaders": "{{count}}x Шейдер", "Subclass": "{{count}}x Параметры Подкласса", "WeaponMods": "{{count}}x Мод Оружия" }, "Tags": { "Archive": "Архив", "ClearTag": "Очистить тэг", "Favorite": "Избранное", "Infuse": "На синтез", "Junk": "Мусор", "Keep": "Оставить", "LockAll": "Заблокировать предметы", "TagItem": "Отметить предмет", "UnlockAll": "Разблокировать предметы" }, "Triage": { "AccountsForArtifice": "Это проверяет, может ли Искусная Броня быть лучше, если бы использовался мод на +3 характеристики.", "BetterArmor": "Строго лучшая Броня", "BetterArtificeArmor": "Лучшая Искусная Броня", "BetterStatArmor": "Броня с лучшими характеристиками", "BetterStatArtificeArmor": "Искусная Броня с лучшими характеристиками", "BetterWorseArmor": "Лучшая/Худшая Броня", "BetterWorseIncludes": "Находит части брони с:", "HighStats": "Высокие хар-ки", "InLoadouts": "В Наборах", "OwnedCount": "# В инвентаре", "PerkBetterArmorDesc": "Одинаковое или большее количество изначальных перков или слотов под специальные моды.", "PerkWorseArmorDesc": "Одинаковый изначальный перк, либо он отсутствует.", "SimilarItems": "Похожие предметы", "StatBetterArmorDesc": "Все характеристики по крайней мере такие же высокие, и хотя бы одна характеристика лучше.", "StatNotPerkArmorDesc": "Это проверяет только характеристики. Худший предмет всё ещё может иметь слоты под специальные моды или изначальные перки.", "StatWorseArmorDesc": "Нет лучших характеристик, и есть хотя бы одна худшая характеристика.", "ThisItem": "Этот предмет", "WorseArmor": "Строго худшая Броня", "WorseArtificeArmor": "Худшая не-искусная Броня", "WorseStatArmor": "Броня с худшими характеристиками", "WorseStatArtificeArmor": "Худшая по характеристикам не-искусная Броня", "YourBestItem": "Ваш лучший предмет" }, "Triumphs": { "GildingTriumph": "Золотой триумф", "HideCompleted": "Скрыть выполненные триумфы", "RevealRedacted": "Измененные Триумфы", "SortRecords": "Сортировать триумфы по завершённости" }, "Vendors": { "Collections": "Коллекция", "Engram": "Ранг", "FilterToUnacquired": "Отсутствующие в Коллекции", "HideSilverItems": "Скрыть предметы за Серебро", "NoItems": "Этот вендор в настоящее время не предлагает никаких предметов.", "RefreshTime": "Инвентарь обновится через:", "Vendors": "Вендоры" }, "Views": { "About": { "APIHistory": "История API действий аккаунта bungie.net (Все совершенные через DIM и другие веб-приложения)", "BungieCopyright": "Изображения и содержимое являются собственностью Bungie.", "CommunityInsight": "Мнения Сообщества для перков и характеристик персонажа любезно предоставлены {{clarityLink}}. Если вы заметите неточности или у вас возникли вопросы, присоединитесь к {{clarityDiscordLink}}.", "Discord": "Discord", "DiscordHelp": "Задавайте вопросы, пишите отзывы и получайте помощь в каналах нашего Discord.", "FAQ": "Часто Задаваемые Вопросы (FAQ)", "FAQAccess": "Как DIM получает доступ к моим данным в Destiny?", "FAQAccessAnswer": "Мы используем аутентификацию для приложений Bungie, чтобы дать доступ DIM к просмотру и перемещению ваших предметов. DIM никогда не видит вашего логина или пароля. Точно так же работает официальное приложение-компаньон.", "FAQKeyboard": "Поддерживает ли DIM сочетания клавиш?", "FAQKeyboardAnswer": "А то! Напечатайте \"?\" для просмотра доступных команд.", "FAQLogout": "Как я могу выйти из DIM?", "FAQLogoutAnswer": "\"Выход\" доступен в меню (левый верхний угол)", "FAQLostItem": "Я потерял предмет используя DIM!", "FAQLostItemAnswer": "Bungie не разрешает приложениям удалять предметы (нет метода в API). Скорее всего произошла ошибка при перемещении и предмет остался в Хранилище или на другом персонаже. Вы можете поискать этот предмет. Если это не работает, перезагрузите страницу. Проверьте на {{link}} или в самой игре, что предмет ещё существует. Мы уверены, что он на месте.", "FAQMobile": "DIM поддерживает телефон? Будет ли приложение?", "FAQMobileAnswer": "Открыв сайт DIM на телефоне или планшете, вы можете добавить его на домашний экран и пользоваться, как любым другим приложением из магазина вашей платформы.", "GitHub": "GitHub", "GitHubHelp": "Если вы заинтересованы внести вклад в проект, посетите страницу нашего проекта на {{link}}.", "Header": "DIM (Destiny Item Manager)", "HowItsMade": "DIM - бесплатный open source проект от разработчиков сообщества Destiny использующий аналогичные для сайта и официального приложения сервисы Bungie.Net.", "Schedule": { "beta": "Бета-версия DIM обновляется каждый раз, когда мы изменяем код - она получает самые последние возможности и исправления, но так же и свежие баги!", "release": "Эта версия DIM обновляется раз в неделю, приблизительно по Воскресеньям, по тихоокеанскому времени США." }, "Translation": "Присоединяйтесь к команде перевода!", "TranslationText": "Мы используем {{link}} для облегчения процесса перевода. Если вы хотите улучшить один из переводов DIM, присоединяйтесь к команде.", "Version": "Версия {{version}} ({{flavor}}), собрана {{date}}", "Wiki": "Руководство DIM", "WikiHelp": "Узнайте, как использовать функции DIM." }, "Login": { "Auth": "Авторизоваться с помощью Bungie.net", "EnableDimSyncWarning": "Ранее вы отключили синхронизацию DIM Sync и использовали только локальное хранилище данных. Включение DIM Sync заменит любые локальные данные на данные DIM Sync. Вы должны сделать резервную копию данных перед включением DIM Sync. Вы можете восстановить из этой резервной копии в настройках.", "Explanation": "Разрешите DIM просматривать и модифицировать ваших персонажей, хранилище и прогресс в Destiny.", "LearnMore": "Узнать больше об аккаунтах и авторизации", "NewAccount": "Авторизоваться с другого аккаунта Bungie.net", "Permission": "Нам нужно ваше разрешение..." }, "Support": { "BackersDetail": "Поддержи нас одноразовым или ежемесячным пожертвованием, чтобы помочь нам продолжать активную разработку.", "FreeToDownload": "DIM это продукт, бесплатный для скачивания и использования. Исходный код DIM находится в открытом доступе и доступен каждому для улучшения. Вы никогда не увидите рекламу в DIM. Это наше обязательство.", "OpenCollective": "Мы используем {{link}} как сервис для компенсации наших разработчиков за их самоотверженность и потраченное время на этот проект.", "Store": "Мерч с логотипом DIM (и не только!) можно приобрести по ссылке: {{link}}", "Support": "Поддержать DIM" } }, "WishListRoll": { "BestRatedTip_few": "Эти перки точно соответствуют ролл оружия в вашем Списке Желаний.", "BestRatedTip_many": "Эти перки точно соответствуют ролл оружия в вашем Списке Желаний.", "BestRatedTip_one": "Этот перк точно соответствует ролл оружия в вашем Списке Желаний.", "BestRatedTip_other": "Эти перки точно соответствуют ролл оружия в вашем Списке Желаний.", "Clear": "Очистить Список Желаний", "CopiedLine": "Желанный Ролл скопирован в буфер обмена", "CopyLine": "Скопировать выбранные перки как Желанный Ролл", "DupeRolls": " (+{{num, number}} ignored dupes)", "ExternalSource": "Добавить другой список желаний", "ExternalSourcePlaceholder": "Вставьте сюда URL списка пожеланий", "Header": "Wish List", "Import": "Загрузить роллы Списка Желаний", "ImportError": "Error loading wish list from \"{{url}}\": {{error}}", "ImportFailed": "None of your wish lists contained any valid rolls.", "ImportNoFile": "Не выбран файл!", "InvalidExternalSource": "Пожалуйста укажите действительную ссылку на ваш внешний источник Списка Желаний. Ссылка должна начинаться с:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "Последнее обновление: {{lastUpdatedDate}} в {{lastUpdatedTime}}", "Num": "{{num, number}} роллов в вашем Списке Желаний", "NumRolls": "{{num, number}} роллов", "Refresh": "Refresh Wishlist", "SourceAlreadyAdded": "Список Желаний уже добавлен", "UpdateExternalSource": "Добавить Список Желаний", "Voltron": "voltron (стандартный)", "WishListNotes": "Заметки к Списку Желаний:", "WorstRatedTip_few": "Эти перки точно соответствуют ролл оружия в вашей корзине Списка Желаний.", "WorstRatedTip_many": "Эти перки точно соответствуют ролл оружия в вашей корзине Списка Желаний.", "WorstRatedTip_one": "Этот перк точно соответствует ролл оружия в вашей корзине Списка Желаний.", "WorstRatedTip_other": "Эти перки точно соответствуют ролл оружия в вашей корзине Списка Желаний." }, "no-space": "нет-места", "wrong-level": "неверный-уровень" } ================================================ FILE: src/locale/zhCHS.json ================================================ { "AWA": { "ConfirmDescription": "请使用\"命运2同伴应用\"授予DIM修改您物品的权限。", "ConfirmTitle": "确认操作", "Error": "更改模组或特性时出错", "ErrorMessage": "我们无法将{{plug}}装备到{{item}}。\n\n{{error}}", "FailedToken": "无权限更换物品", "IrreversiblePlugging": "你不拥有{{plug}},所以我们不会覆盖它。" }, "Accounts": { "Choose": "{{bungieName}} 的档案", "ErrorLoadInventory": "无法加载你的《命运 {{version}}》角色和库存", "ErrorLoadManifest": "无法从 Bungie 加载命运信息数据库", "ErrorLoading": "无法从 Bungie.net 加载命运账户", "MissingAccountWarning": "如果你的账户没有显示,你可能登录了错误的 Bungie.net 账户,或 Bungie.net 可能正在停机维护。", "MissingDescription": "你试图查看的账户并未连接到你的 Bungie.net 账户。请在下方选择你的账户。", "MissingTitle": "找不到账户", "NoCharacters": "没有找到与此 Bungie.net 账号相关联的《命运》角色,登录其它账号试试看。", "NoCharactersTitle": "未找到角色", "SwitchAccounts": "你可以稍后在标题栏菜单里切换账户。", "Title": "账户" }, "Activities": { "Activities": "活动", "Hard": "困难", "Nightfall": "日落打击", "Normal": "普通", "WeeklyHeroic": "每周英雄突击" }, "Armory": { "AlternateItems": "其他版本", "Armory": "军械库", "DifferentSeason": "其他赛季的版本", "NoNotes": "无注释", "OpenInArmory": "查看武器详情", "Season": "年 {{year}},第 {{season}} 赛季", "TrashlistedRolls_other": "{{count, number}} 个垃圾列表特性组合", "Unknown": "未知物品", "UnknownPerkHash": "特性 hash {{hash}} ({{perkName}}) 未出现在此物品上,故该愿望单组合无效。请联系愿望单作者更正。注意:愿望单仅会显示未强化版的特性。", "WishlistedRolls_other": "{{count, number}} 个愿望清单特性组合", "YourItems": "你的物品" }, "Browsercheck": { "Samsung": "启用深色模式时,三星浏览器可能会导致网页显示过于黑暗。在设置 > 通用 > 使用网页深色主题或使用其他浏览器来解决此问题。", "Steam": "Steam 游戏内叠加浏览器版本较老,DIM 的部分或全部功能可能无法正常工作,且我们无法为其提供支持。", "Unsupported": "DIM 团队未对当前浏览器作出支持。DIM 可能部分或完全不可用。" }, "Bucket": { "Armor": "防具", "Class": "子职业", "General": "杂项", "Ghost": "机灵", "Inventory": "背包", "Postmaster": "邮政官", "Progress": "进度", "Reputation": "声望", "Unknown": "未知", "Vault": "保险库", "Weapons": "武器" }, "BulkNote": { "Append": "附加到备注 / 添加 #标签", "Confirm": "更新备注", "Remove": "从备注中删除 / 删除 #标签", "Replace": "替换备注", "Title_other": "更改 {{count}} 个物品的备注" }, "BungieAlert": { "Title": "一则来自Bungie的信息:" }, "BungieService": { "AppNotPermitted": "DIM 没有权限执行此操作。", "DestinyCannotPerformActionAtThisLocation": "你不能在活动中装备物品或更换模组。回到轨道或前往社交区域再试试看。这是 Bungie.net API 而不是 DIM 的限制。", "DestinyItemUnequippable": "你不能装备此物品。如果此角色的上一场活动锁定了其装备,请尝试重新登录此角色。", "DestinyLegacyPlatform": "目前 Bungie 的服务出现了一个 Bug,如果您在上一代主机上游玩过 Destiny 1,那么 DIM 将无法加载您的 Destiny 2 账户信息。Bungie 将会修复这个 Bug,但直到 Bug 修复完成前,您必须在当前世代主机上游玩 Destiny 1 以供 DIM 读取您的账户信息。", "DevVersion": "您是否正在运行 DIM 的开发版本?您必须先将您的 Chrome 扩展与 Bungie.net 关联。", "Difficulties": "Bungie.net 目前存在问题。", "ErrorTitle": "Bungie.net 出现一个错误", "ItemUniquenessExplanation": "每个角色只能拥有一件 '{{name}}'。", "Maintenance": "Bungie.net 服务器停机维护中。", "MissingInventory": "Bungie.net 没有返回你的物品栏数据,可能是因为你的隐私设置阻止了它。请尝试重新登录。", "NetworkError": "网络错误 - {{status}} {{statusText}}", "NoAccount": "没有找到任何关联的 Destiny 账户,请确保您选择了正确的平台。", "NoAccountForPlatform": "在 {{platform}} 上没有找到您的 Destiny 账户。", "NotConnected": "您可能没有连接到互联网。", "NotConnectedOrBlocked": "你可能没有连接到互联网,或是广告拦截和隐私扩展阻止了 Bungie.net。", "NotLoggedIn": "请授权 DIM 以使用应用程序。", "Slow": "Bungie.net 目前响应缓慢", "SlowDetails": "Bungie.net 响应缓慢。这可能是因为玩家过多,或 Bungie.net 目前存在问题。还有可能是你的网络连接问题。我们会继续等待响应。", "SlowResponse": "Bungie.net 响应极为缓慢。", "Throttled": "Bungie.net 限制了 DIM 可以发起的请求数。", "Twitter": "查看服务器更新状态:", "UnknownError": "来自 Bungie.net 的消息:{{message}}", "VendorNotFound": "商贩数据不可用。" }, "Compare": { "Archetype": "固有特性", "AssumeMasterworked": "假定大师之作", "AssumeMasterworkedDescription": "属性假定为完整大师杰作,不含当前模组", "BaseStatsDescription": "基础属性,不含大师杰作及模组", "Button": "对比", "ButtonHelp": "对比物品", "CompareBaseStats": "显示基础属性", "CurrentStats": "当前属性", "CurrentStatsDescription": "当前模组,包含模组及大师杰作等级", "Error": { "Invalid": "没有可供对比的物品。", "Unmatched": "此物品和要比较的物品类别不同。" }, "InitialItem": "比较工具从这个物品启动", "IsVendorItem": "你不拥有此物品,但{{vendorName}}售卖它。", "NoModArmor": "旧版模组" }, "Cooldown": { "Grenade": "手雷冷却:{{cooldown}}", "Melee": "近战冷却:{{cooldown}}", "Super": "超能冷却:{{cooldown}}" }, "Countdown": { "Days_compact_other": "{{count}}天", "Days_other": "{{count}} 天" }, "Csv": { "EmptyFile": "文件中没有行", "ImportConfirm": "您确定要从CSV导入标签/笔记?这将覆盖您电子表格中所包含的所有项目/标记。", "ImportFailed": "从 CSV 导入标签/注释失败: {{error}}", "ImportSuccess_other": "为{{count}} 个物品加载的标签/注释。", "ImportWrongFileType": "此文件不是一个CSV文件。", "WrongFields": "CSV文件中必须有'Id', 'Notes', 'Tag', 'Hash'列 (编号, 注释, 标签, 哈希)" }, "Dialog": { "Cancel": "取消", "OK": "确定" }, "EnergyMeter": { "Energy": "能量", "Unused": "未使用", "UpgradeNeeded": "此物品当前的能量容量为 {{energyCapacity}}。需要 {{energyUsed}} 容量才能装下所选的模组。", "Used": "已使用" }, "ErrorBoundary": { "Title": "发生了一些错误" }, "ErrorPanel": { "BrowserTooOld": "您的浏览器过旧,无法使用 DIM。请更新您的浏览器到最新版本。", "BrowserTooOldTitle": "不兼容的浏览器", "Description": "请尝试在命运同伴 App 中加载你的物品栏,以检测是否 Bungie.net 出现了问题。", "ReadTheGuide": "阅读我们的用户指南(链接位于菜单中)以了解故障排除步骤。", "SystemDown": "这一问题影响所有命运应用程序,DIM团队也无法修复或绕过。", "Troubleshooting": "故障排除指南" }, "FarmingMode": { "D2Desc_female_other": "为防止有物品被寄送到邮政官处,DIM 正确保 {{store}} 上的每个物品种类都留有 {{count}} 个空位。", "D2Desc_male_other": "为防止有物品被寄送到邮政官处,DIM 正确保 {{store}} 上的每个物品种类都留有 {{count}} 个空位。", "D2Desc_other": "为防止有物品被寄送到邮政官处,DIM 正确保 {{store}} 上的每个物品种类都留有 {{count}} 个空位。", "Desc_female_other": "为防止有物品被寄送到邮政官处,DIM 正将 {{store}} 上的记忆水晶和微光物品移动到保险库,并确保每个物品种类都留有 {{count}} 个空位。", "Desc_male_other": "为防止有物品被寄送到邮政官处,DIM 正将 {{store}} 上的记忆水晶和微光物品移动到保险库,并确保每个物品种类都留有 {{count}} 个空位。", "Desc_other": "为防止有物品被寄送到邮政官处,DIM 正将 {{store}} 上的记忆水晶和微光物品移动到保险库,并确保每个物品种类都留有 {{count}} 个空位。", "FarmingMode": "收集模式", "FarmingModeNote": "(为掉落物留出空间)", "MakeRoom": { "Desc": "DIM将从{{store}} 上移动记忆水晶与微光币物品到保险库或其它角色上,以防止有物品被寄送到邮政官处。", "Desc_female": "DIM将从{{store}} 上移动记忆水晶与微光币物品到保险库或其它角色上,以防止有物品被寄送到邮政官处。", "Desc_male": "DIM将从{{store}} 上移动记忆水晶与微光币物品到保险库或其它角色上,以防止有物品被寄送到邮政官处。", "MakeRoom": "移动装备腾出空间以便拾取物品", "Tooltip": "如果勾选,DIM将在保险库中移动武器和防具来给记忆水晶腾出空间。" }, "OutOfRoom": "你的空间不足,无法再为{{character}}腾出空间了。该清理垃圾装备了!", "OutOfRoomTitle": "空间不足", "Stop": "停止", "Vault": "会将物品移到保险库来腾出空间。" }, "FashionDrawer": { "Accept": "保存外貌", "CannotFitOrnament": "此物品没有皮肤插槽,或你没有此物品的皮肤。", "CannotFitShader": "此物品不能使用着色器", "ClearOrnaments": "清除皮肤", "ClearOrnamentsTitle": "将所有皮肤重置为“无偏好”", "ClearShaders": "清除着色器", "ClearShadersTitle": "将所有着色器重置为“无偏好”", "NoPreference": "无偏好 - 不会更改此插槽", "Reset": "清除外貌", "Sync": "同步", "SyncOrnaments": "同步皮肤", "SyncOrnamentsTitle": "如整套皮肤已解锁,则应用到所有物品", "SyncShaders": "同步着色器", "SyncShadersTitle": "为所有物品应用同一着色器", "Title": "选择着色器和皮肤", "UseEquipped": "使用已装备的外貌" }, "FileUpload": { "Instructions": "选择或拖拽档案" }, "Filter": { "Adept": "(专家)", "AmmoType": "按照弹药类型显示物品。", "Armor": "显示护甲物品。", "Armor3": "显示使用从《宿命边缘》起引入的护甲 3.0 模组系统的装备。", "ArmorCategory": "按照类别显示护甲。", "ArmorIntrinsic": "显示带有固有特性的传说护甲,例如诡计护甲。", "Artifice": "显示诡计护甲。", "Ascended": "显示可提升光等且已提升的物品。", "Breaker": "根据反勇士类型或对应的勇士类型过滤。使用 breaker:instrinsic 搜索具有内置反勇士能力的装备。", "BulkClear_other": "移除 {{count}} 件物品标签。", "BulkRevert_other": "恢复 {{count}} 件物品标签。", "BulkTag_other": "将 {{count}} 件选择物品标记为 {{tag}}。", "Catalyst": "按状态显示催化。catalyst:complete 显示你已解锁并应用的催化,catalyst:incomplete 显示你已解锁但尚未完成目标且应用的催化,而 catalyst:missing 显示你尚未解锁催化的物品。", "Class": "按照职业类别显示.", "Combine": "可以用括号来组合过滤器,或用“or”和“and”来进一步筛选你的搜索,例如“{{example}}”。", "ContributePower": "显示拥有光等的物品。", "Cosmetic": "显示饰品或装饰物品。", "Craftable": "显示可制作的物品。", "CraftedDupe": "显示包括至少一件锻造武器在内的重复武器。", "Curated": "显示拥有指定特性的物品。", "CurrentClass": "显示当前登录的守护者可装备的物品。", "CustomStatLower": "显示相比其他同类别护甲而言属性全面更低的护甲,但只考虑任意自定义属性总和里的属性。", "DamageType": "按照伤害类型显示.", "Deepsight": "显示可提取模式或可通过深视协调器激活深视共振的武器。", "Deprecated": "此过滤器已不受支持。", "Description": "描述", "DescriptionFilter": "显示部分描述与之匹配的物品。使用引号来搜索整个文本。", "DisabledModSlot": "显示包含已被禁用模组的物品。", "Dupe": "显示重复的物品,包含不同赛季版本", "DupeArchetype": "搜索多件具有重复属性框架的护甲。", "DupeCount": "拥有指定重复数量的物品。", "DupeLower": "非最高光等的重复和再版物品。重复物品中只有一件会被视为最高光等,其余均视为较低光等。", "DupePerks": "显示特性重复或已被其他同类物品所包含的物品。", "DupeSetBonus": "搜索多件具有重复套装加成的护甲。", "DupeStats": "显示具有相同基础属性和属性调整器的护甲,属性调整器包含诡计模组和调整模组。", "DupeTertiary": "搜索多件具有重复第三属性的护甲。", "DupeTraits": "显示特性重复或已被其他同类武器所包含的武器。", "DupeTunedStat": "搜索多件具有相同调整属性的护甲。", "DupeUntunedStats": "搜索多件具有相同基础属性的护甲,不含属性调整模组。", "DupeZeroStats": "搜索多件具有相同 3 个非零属性的护甲。", "Energy": "显示使用从《暗影要塞》起引入的护甲 2.0 模组系统的装备。", "EnergyCapacity": "根据护甲模组能量上限显示装备。", "Engrams": "显示记忆水晶。", "Enhanceable": "显示可被强化的武器。", "Enhanced": "按强化等级显示武器。", "EnhancedPerk": "显示具有指定列数的强化特性的武器。", "EnhancementReady": "显示已达到特性强化要求的武器。", "Equipment": "可以装备的物品。", "Equipped": "目前已装备的物品。", "Event": "显示命运2特定活动中的物品。", "ExtraPerk": "显示带有额外可选特性的随机特性传说武器。", "Featured": "显示在当前赛季被列入“新装备”或“特色装备”的物品。", "Filter": "过滤器", "FilterWith": "筛选:", "Focusable": "显示可在商人处聚焦的物品", "Foundry": "按武器铸造厂显示物品。", "Glimmer": "显示与增加微光获取有关的消耗品。", "Harrowed": "(痛苦)", "HasNotes": "显示已有备注的物品", "HasOrnament": "显示已应用皮肤的物品。", "HasShader": "显示已应用着色器的物品", "Holofoil": "显示带有全息箔的武器。", "InDimLoadout": "is:indimgameloadout 指令会显示全部DIM配装中的物品。", "InInGameLoadout": "is:iningameloadout 指令会显示任何游戏内配装系统保存的物品。", "InInventory": "显示在库存中已拥有的物品。仅在商贩和记录页面中有效。", "InLoadout": "is:inloadout 指令显示在全部配装中包含的物品。在 inloadout: 指令下搜索可显示所有配装中的与搜索名称相符的物品。在 inloadout: 指令后使用#标签可显示配装中有含有该标签的物品或注释。若指定范围,则显示在多个配装中的物品。", "Infusable": "显示可以灌注的物品.", "InfusionFodder": "显示可用于灌注同类物品且只消耗微光的物品。", "IsAdept": "显示可安装专家模组的武器。", "IsCrafted": "显示制作获取的武器。", "ItemHash": "显示具有给定物品 hash 的物品。此为高级用户选项。", "ItemId": "显示具有给定物品 ID 的物品。此为高级用户选项。", "Leveling": { "Complete": "{{term}}-显示完全完成的物品-每次升级均解锁.", "Incomplete": "{{term}}-显示尚未完成的物品-至少升一级才能解锁.", "NeedsXP": "{{term}} - 显示仍能获得经验值的物品.", "Upgraded": "{{term}}-显示具有足够 XP 解锁其所有节点的物品, 但并非所有节点都已解锁。", "XPComplete": "{{term}}-显示无法获得更多XP的物品 (无论升级是否完成)。" }, "Location": "根据物品在App中的位置排列。左/中/右显示您的角色信息,您的角色会显示在最左边,中间和右边是否会显示角色取决于您一共创建了几位角色。现在显示的您已经登陆的角色(有黄色三角的那个)。", "LockAllFailed": "锁定道具失败", "LockAllSuccess": "已经锁定 {{num}} 件道具", "Locked": "根据物品锁定状态显示.", "Masterwork": "按大师杰作属性或等级显示物品。", "MasterworkKills": "按大师杰作击杀追踪器显示物品。", "MaxPower": "显示每个槽位最高光等的物品。", "MaxPowerLoadout": "显示每个职业中能最大化光等的配装物品。", "Memento": "显示包含纪念物插槽的武器。", "ModSlot": "显示带有特定类型模组插槽的护甲。", "Mods": { "Y3": "显示已应用模组的装备。" }, "Name": "使用 (exactname:) 精确搜索某名称的物品,或使用 (name:) 搜索部分匹配某名称的物品。使用引号搜索整个词条。", "NamedStat": "显示重要防具.", "Negate": "要否定搜索,为搜索添加一个减号或单词“not”前缀,例如“{{notexample}}”或“{{notexample2}}”。", "NewItems": "显示新获得的物品.", "Notes": "搜索已添加了自定义备注的物品。", "OriginTrait": "显示具有原始perk的武器。", "Ornament": "显示带有装饰物和过滤器的物品.", "PartialMatch": "显示名称/描述/特性/模组与筛选器部分匹配的物品。使用引号来搜索包含空格的文本。", "PatternUnlocked": "显示制作样式已解锁的武器,即使该武器并非制造而来。", "Perk": "显示带有特性或模组的名称或描述与筛选器部分匹配的物品。使用引号来搜索带有空格的文本。", "PerkName": "使用 (exactperk:) 精确搜索某特性或模组的物品,或使用 (perkname:) 搜索部分匹配某特性或模组的物品。使用引号搜索整个词条。", "PinnacleReward": "显示提供巅峰奖励的追猎目标。", "Postmaster": "目前在邮政官处的物品.", "PowerKeywords": "使用 pinnaclecap 或 softcap 关键字而不是数字来表达当前赛季的光等限制。", "PowerLevel": "按能量等级显示物品。$t(Filter.PowerKeywords)", "PowerfulReward": "显示提供强力奖励的追猎目标。", "PrismaticDamageType": "根据伤害类型为光能或暗影来显示物品。光能类型为电弧,烈日和虚空。暗影类型为冰影和缚丝。", "Quality": "根据其统计品质百分比来显示物品'{{percentage}}'别名'{{quality}}'.", "RandomRoll": "显示可以随机特性掉落的物品", "RarityTier": "按照稀有度显示.", "Reforgeable": "显示可以在造枪匠那里重新锻造的物品.", "Release": "显示在特定版本或事件中可用的物品。", "RequiredLevel": "根据等级需求显示物品。", "RetiredPerk": "显示带有已无法获取特性的武器。", "SearchPrompt": "搜索可用的过滤器命令", "Season": "根据物品推出的赛季显示。", "StackFull": "显示堆叠已满的物品(强化核心、奇异硬币、枪匠材料等)", "StackLevel": "根据堆叠数量来显示物品。", "Stackable": "显示可堆叠的物品(弹药合成器、奇异硬币等)", "StatLower": "显示相比其他同类别护甲而言属性全面更低的护甲。", "Stats": "按某个属性值显示物品。$t(Filter.StatsExtras)", "StatsBase": "按基础属性值筛选护甲,不包含附加模组或大师杰作属性。$t(Filter.StatsExtras)", "StatsExtras": "可使用 + 或 & 符号来筛选多种属性组合。也可根据装备属性高低顺序使用关键字筛选最高 (highest)、次高 (secondhighest)、第三高 (thirdhighest) 等。每个自定义属性也有各自的搜索关键词,在自定义属性设置中显示。", "StatsLoadout": "针对特定属性,筛选出最大化数值的一系列装备。", "StatsMax": "针对特定属性,筛选出最大化数值的一系列装备。包括所有最高属性值的物品。", "StatsOrdinal": "显示具有特定主要/次要专注属性的 3.0 护甲。", "Tags": { "Tag": "显示具有特定标签的物品。", "Tagged": "显示具有任何标签的物品。" }, "Tier": "根据从 0 到 5 的阶级显示装备。", "Timelost": "(失时)", "Tracked": "根据追踪状态显示任务/悬赏。", "Transferable": "可在角色间移动的物品。", "Trashlist": "显示处于您的愿望单垃圾清单中的物品。", "TunedStat": "显示具有特定调整属性的装备。", "Unascended": "显示可提升光等但尚未提升的物品。", "Undo": "撤销", "UnlockAllFailed": "解锁物品失败", "UnlockAllSuccess": "已解锁 {{num}} 件物品", "Vendor": "物品可从特定的商贩获得。", "VendorItem": "物品由商贩售卖,不在你的背包中。用于在配装优化器中排除商贩物品。", "Weapon": "显示武器物品。", "WeaponLevel": "按武器等级显示武器。", "WeaponType": "按武器类型显示武器。", "Wishlist": "显示处于您的愿望单中的物品。", "WishlistDupe": "显示有至少一个复制品在你愿望单中的物品堆叠", "WishlistEnabled": "显示与愿望清单特性组合相匹配的物品。", "WishlistNotes": "显示与搜索相匹配的愿望清单物品", "WishlistUnknown": "显示愿望清单中没有特性推荐的物品。", "Year": "按照物品推出年份显示." }, "General": { "ClickForDetails": "点击查看详情", "Close": "关闭", "Confirm": "确认?", "UserGuideLink": "用户指南" }, "Glyphs": { "Axe": "暮光战斧", "DarkAbility": "暗影技能", "Gilded": "镀金", "Harmonic": "谐振", "HiveSword": "邪魔族刀剑", "LightAbility": "光能技能", "LightLevel": "光等", "Misadventure": "意外致死", "Missing": "缺失", "OpenSymbolsPicker": "打开符号选单", "Prismatic": "棱镜", "Quickfall": "急坠", "RespawnRestricted": "重生受限", "ScorchCannon": "灼烧发射器", "SearchSymbols": "搜索符号...", "Smoke": "烟幕" }, "Header": { "About": "关于DIM", "AutoRefresh": "只要你仍在游戏,DIM 就会自动刷新。", "BulkTag": "批量标记物品", "BungieNetAlert": "Bungie提示您", "Clear": "清除搜索筛选器", "CompareMatching": "对比物品", "DeleteSearch": "删除搜索", "FilterHelp": "搜索物品、特性、{{example}},和更多", "FilterHelpBrief": "搜索物品", "FilterHelpLoadouts": "搜索配装名称和备注", "FilterHelpMenuItem": "过滤器帮助...", "FilterHelpOptimizer": "过滤包含在配装中的护甲,如 {{example}}", "FilterHelpProgress": "搜索里程碑和悬赏", "FilterHelpRecords": "搜索成就和收藏品", "FilterMatchCount_other": "{{count}} 个物品", "Filters": "过滤器", "InstallDIM": "安装为 App", "InstallDIMBanner": "将 DIM 安装为主屏幕上的 App", "Inventory": "背包", "IosPwaPrompt": "在 Safari 中,轻触分享图标(位于屏幕底部中央),并选择“添加到主屏幕”。", "KeyboardShortcuts": "键盘快捷键", "LaunchDIMAlone": "独立窗口", "MaterialCounts": "材料数量", "Menu": "菜单", "ProfileAge": "数据由《命运》服务器最后更新于 {{age}} 之前。\n在 DIM 中刷新可能会获得最新数据,但 Bungie.net 也可能会重复发送缓存的数据。", "Refresh": "刷新命运数据 [R]", "ReloadApp": "重新加载应用", "ReportBug": "报告错误", "SaveSearch": "保存搜索", "SearchActions": "打开搜索操作", "SearchResults": "显示物品", "Shop": "商店", "TagAs": "标记为{{tag}}", "UpgradeDIM": "更新DIM", "WhatsNew": "新增内容" }, "Help": { "CannotMove": "无法从该角色中移出物品.", "NoStorage": "DIM无法存储数据", "NoStorageMessage": "DIM 无法在你的浏览器中存储数据。这可能是因为你在使用无痕或隐身模式,或磁盘空间不足。请尝试重启你的电脑!在修解决此问题前你将无法登录或使用 DIM。" }, "Hotkey": { "Armory": "显示物品详情", "CheatSheetTitle": "键盘快捷键", "ClearDialog": "关闭对话", "ClearNewItems": "清除新物品提示", "Enter": "回车", "ItemPopupTab": "切换至物品细节页面", "LockUnlock": "锁定或解锁物品", "MarkItemAs": "将物品标记为 “{{tag}}”", "Menu": "切换菜单", "Note": "添加备注", "Pull": "为当前角色取回物品", "RefreshInventory": "刷新背包", "ShowHotkeys": "显示键盘快捷键", "StartSearch": "开始搜索", "StartSearchClear": "重新搜索", "Tab": "TAB", "Vault": "将物品存入保险库" }, "InGameLoadout": { "ClearSlot": "清空槽位 {{index}}", "Create": "创建配装", "CreateTitle": "从当前装备创建游戏内配装", "CurrentlyEquipped": "当前装备", "DeleteFailed": "删除配装失败", "Deleted": "配装已删除", "DeletedBody": "已清除槽位 {{index}} 的游戏内配装", "EditFailed": "更新配装失败", "EditIdentifiers": "编辑标识", "EditTitle": "编辑配装名字和图标", "EquipNotReady": "游戏内装备尚未就绪", "EquipReady": "游戏内装备已就绪", "LoadoutDetails": "配装详情", "MatchingLoadouts": "匹配配装:", "PrepareEquip": "准备配装", "Replace": "替换配装槽位 {{index}}", "Save": "更新配装", "SaveIdentifiers": "更新标识", "SnapshotFailed": "为当前配装创建快照时失败" }, "Infusion": { "Filter": "过滤物品", "InfuseSource": "选择需要用 {{name}} 进行灌注的物品", "InfuseTarget": "选择用于灌注 {{name}} 的物品", "InfusionMaterials": "灌注材料", "NoItems": "没有可灌注的物品。", "NoTransfer": "移动灌注材料\n{{target}} 无法移动。", "SwitchDirection": "切换", "TransferItems": "转移" }, "Inventory": { "ClickToExpand": "(点击展开)", "MissingSilver": "您的银币余额仅在您当前游玩的平台可用。" }, "Item": { "SetBonus": { "NPiece_other": "{{count}} 件" }, "ThumbsDown": "不推荐", "ThumbsUp": "推荐" }, "ItemFeed": { "ClearFeed": "清空物品流", "Description": "物品流", "HideTagged": "隐藏已标记的物品", "NoNewItems": "没有新物品", "ShowOlderItems": "显示之前的物品" }, "ItemMove": { "Consolidate": "合并{{name}}", "Distributed": "拆分{{name}}\n{{name}} 已平均拆分于所有角色上.", "MovingItem": "转移到保险柜", "MovingItem_female": "转移到{{target}}", "MovingItem_male": "转移到{{target}}", "ToStore": "所有{{name}} 都已移动至您的 {{store}} 之中.", "ToVault": "所有 {{name}} 都已移动至您的保险库之中." }, "ItemPicker": { "ChooseItem": "选择一件物品:", "SearchPlaceholder": "搜索物品" }, "ItemService": { "BucketFull": { "Guardian": "您的{{store}} 上有太多 “{{itemtype}}” 的物品.", "Guardian_female": "您的{{store}} 上有太多 “{{itemtype}}” 的物品.", "Guardian_male": "您的{{store}} 上有太多 “{{itemtype}}” 的物品.", "Vault": "您的{{store}} 上有太多 “{{itemtype}}” 的物品." }, "Classified": "该物品是加密的,现在无法被转移。", "Classified2": "加密物品。Bungie 尚未提供该物品的相关信息。为该物品添加备注,以方便您使用搜索过滤器找到它。", "Deequip": "无法找到另一个物品来卸下{{itemname}}", "ExoticError": "无法装备“{{itemname}}”,因为在{{slot}} 栏位中无法装备异域物品。({{error}})", "NotEnoughRoom": "我们无法在{{store}} 上移动物品, 来为{{itemname}} 腾出空间", "NotEnoughRoomGeneral": "没有足够的空间来移动此物品。", "OnlyEquippedClassLevel": "该物品仅可以装备在{{class}} 上,且至少大于{{level}} 级。", "OnlyEquippedLevel": "该物品仅可以装备在至少{{level}} 级的角色上。", "PostmasterAlmostFull": "快要满了!", "PostmasterFull": "已经满了!", "PreviewVendor": "预览 {{type}} 内容", "StackFull": "您已经达到 {{name}} 的堆叠上限", "StoreName": "{{genderRace}} {{className}} 角色" }, "KillType": { "ClassAbilities": "职业技能", "Finisher": "终结技", "Grenade": "手雷", "Melee": "近战", "Precision": "精准", "Super": "超能" }, "LB": { "AddStack": "追加此模组", "AdvancedOptions": "高级选项", "ChooseAMod": "选择你的模组", "ChooseASetBonus": "选择你的套装加成", "ChooseAnExotic": "选择你的异域装备", "ClearLocked": "清除锁定", "ContainsVendorItems": "该配装包含商贩物品", "Current": "当前", "Equip": "装备在{{character}}", "Exclude": "已排除的物品", "ExcludeHelp": "Shift + 单击一个物品(或直接拖动)用以创建一个不包括特定装备的套装组.", "ExistingBuildStats": "现有配装属性", "ExistingBuildStatsNote": "仅显示更高属性的配装。", "FilterSets": "组合过滤", "Help": { "And": "装备了所有这些特性(Perk) 的防具将被使用(\"and\")", "ChangeNodes": "如要创建一个配装,请改变智力,训练和力量的比例,如图所示.", "Discipline": "训练可以加速手榴弹的充能时间", "DragAndDrop": "拖放一个物品到选择框内以创建一个具有该特定装甲的配装", "Help": "需要帮助吗?", "HigherTiers": "越高阶越好", "Intellect": "智力会加速大招的充能时间", "Lock": "通过点击对话框并选择特性(Perk) 来锁定一套额外(Perks)", "MultiPerk": "如果你想同时使用多种防具特性(Perk), 请按Shift +单击所需的特性(Perk)", "NoPerk": "如果没有显示特性(Perk),则代表你未拥有带有这个特性(Perk) 的防具", "Or": "将使用具有任何这些特性(Perk) 的防具(\"or\")", "ShiftClick": "Shift+单击创建一项空白的套装组", "StatsIncrease": "当一项物品的防御等级增加时,该物品的(智力/训练/力量)数据也会增加.", "Strength": "力量会加速近战的充能时间", "Synergy": "寻找能增加当前武器载弹量的防具特性(Perk).", "Tier11Example": "4/5/2(11级)代表智力4、训练5、力量2(4+5+2=11级)" }, "HideAllConfigs": "隐藏所有配置", "HideConfigs": "隐藏配置", "IncompatibleWithOptimizer": "此物品与优化器不兼容。请从收藏中重新获取一个新版本。", "LB": "配装优化器", "LightMode": { "HelpCurrent": "计算当前防御等级.", "HelpScaled": "计算使所有角色都都拥有350防御.", "LightMode": "光亮模式" }, "Loading": "正在载入最佳套装", "LockEquipped": "锁定装备", "LockPerk": "锁定特性", "Locked": "锁定的物品", "LockedHelp": "拖放物品到选择框中用以创建一个套装组.使用Shift +单击可移除物品.", "Missing2": "缺少稀有,传奇,或异域物品用来打造套装!", "ProcessingMode": { "Fast": "快速", "Full": "完全", "HelpFast": "只看你最好的装备.", "HelpFull": "查看更多装备,但是需要更长时间。", "ProcessingMode": "处理模式" }, "RemoveStack": "递减此模组", "Scaled": "缩放", "SearchAMod": "搜索模组名称或描述", "SearchASetBonus": "", "SearchAnExotic": "搜索异域装备名称或描述", "SelectExotic": "选择异域装备", "SelectMods": "选择模组", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} 个活动模组", "SelectSetBonus": "选择套装加成", "SelectSubclassOptions": "自定义子职业", "ShowAllConfigs": "显示所有配置", "ShowConfigs": "显示配置", "ShowGear": "{{class}} 防具", "Vendor": "包括商贩物品" }, "Loading": { "Accounts": "正在加载命运账户...", "Code": "正在加载 DIM 代码...", "FilterHelp": "正在加载搜索帮助...", "Profile": "正在加载命运档案...", "Vendors": "正在加载命运商人..." }, "LoadoutAnalysis": { "Analyzed": "已分析 {{numLoadouts}} 个配装", "Analyzing": "正在分析配装 {{numAnalyzed}}/{{numLoadouts}}", "BetterStatsAvailable": { "Description": "为此配装选用不同的护甲或模组将能达到更高的属性。选择“$t(Loadouts.OpenInOptimizer)”来查看更优的配装。", "Name": "更优属性可用" }, "BetterStatsAvailableFontNote": "提示:此配装使用了属性洗礼模组,导致属性溢出 200。DIM 可降低多出的基础属性以优化。若不需要,请在配装界面中禁用“$t(Loadouts.IncludeRuntimeStatBenefits)”。", "DoesNotRespectExotic": { "Description": "此配装由配装优化器指定了一件异域装备,但在该配装中未找到它。", "Name": "异域装备错误" }, "DoesNotSatisfyStatConstraints": { "Description": "此配装由配装优化器指定了属性下限,但在该配装中未能达到。", "Name": "错误的属性下限" }, "EmptyFragmentSlots": { "Description": "分支职业中有空的碎片插槽。", "Name": "空碎片插槽" }, "InvalidMods": { "Description": "配装中的某些模组已被弃用或不适合你的护甲。", "Name": "已弃用的模组" }, "InvalidSearchQuery": { "Description": "此配装通过配装优化器中的无效搜索查询创建。", "Name": "搜索条件无效" }, "ItemsDoNotMatchSearchQuery": { "Description": "此配装通过配装优化器中的搜索查询创建,且搜索查询至少排除了一个在配装中的物品。", "Name": "搜索要排除的物品" }, "MissingItems": { "Description": "该配装中部分物品已不在你的背包中。", "Name": "物品缺失" }, "ModsDontFit": { "Description": "即使升级该配装中的护甲,也无法插入全部模组。", "Name": "未分配的模组" }, "NeedsArmorUpgrades": { "Description": "该配装中的护甲需要升级以插入全部所需模组或达到指定属性值。", "Name": "需要升级护甲" }, "NotAFullArmorSet": { "Description": "由于该配装未使用整套护甲,故无法进一步分析。", "Name": "非整套护甲" }, "TooManyFragments": { "Description": "指定的分支职业碎片数量超过星相限制。", "Name": "太多分支职业碎片" }, "UsesSeasonalMods": { "Description": "该配装依赖于某些赛季特定的模组。当赛季结束后,一些模组将不可用或超出护甲能量上限。", "Name": "使用赛季模组" } }, "LoadoutBuilder": { "All": "全部", "AlwaysAutoMods": "诡计和调整模组总会被自动选取。", "AnyExotic": "任何异域奖励", "AnyExoticDescription": "组合里必须有任意异域装备。", "Artifice": "诡计", "AssumeMasterwork": "假定大师之作", "AssumeMasterworkOptions": { "All": "所有护甲:$t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "所有护甲:$t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\n异域护甲 2.0:$t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "强化以激活诡计护甲属性模组。", "Current": "当前属性,假定能量等级至少为 {{minLoItemEnergy}}。", "Legendary": "传说:$t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\n异域:$t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "完整大师杰作属性加成,假定能量等级至少为 10。", "None": "所有护甲:$t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "自动添加属性模组", "AutomaticallyPicked": "此模组已被自动添加以优化配装属性。", "CompareLoadout": "对比配装", "ConfirmOverwrite": "你确定要用这套新的护甲来替换配装“{{name}}”里的护甲吗?", "DecreaseStatPriority": "降低属性优先级", "DisabledByAutoStatMods": "配装优化器正在自动选择属性模组。", "DisabledDueToMaintenance": "由于 Bungie API 维护,配装优化器目前不可用。", "EquipItems": "装备", "ExcludeItem": "排除项目", "ExcludeVendors": "搜索 \"not:vendor\" 以在配装优化器中排除来自商人的物品。", "ExcludedItems": "已排除的物品", "ExistingLoadout": "已有配装", "Exotic": "异域护甲", "ExoticClassItemPerks": "如果您想要指定特性,请使用形如 exactperk:\"真理之灵\" 的方式搜索。在优化器条目中点击特性来筛选/移除它们。", "ExoticSpecialCategory": "特殊", "FOTLWildcardWarning": "组合中包含英灵节面具。手动应用正确的模组以激活所需的套装加成。", "Filter": "设置", "IgnoreStat": "若未选中,则配装优化器将会在配装组合中无视此属性。", "IncreaseStatPriority": "提高属性优先级", "Legendary": "传说", "LimitToNewFeaturedGear": "仅限新装备/特色装备", "LockItem": "固定物品", "MissingClass": "配装适用于:{{className}}", "MissingClassDescription": "你正试图查看的配装适用于一个你未拥有的角色职业。", "MwExotic": "异域", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "搜索条件导致 DIM 不能将部分物品包含在配装中", "AllowAutoStatMods": "允许 DIM 自动添加额外的属性模组", "AlwaysInvalidMods": "这些模组无法装上你拥有的任何物品:", "AssumeMasterworked": "允许 DIM 建议护甲大师之作", "AssumptionsRestricted": "DIM 无法建议更改护甲能量:", "BadSlot": "这些模组无法装上{{bucketName}}槽位所允许的任何物品:", "ExoticDoesNotExist": "你的物品栏中没有选定的异域护甲。", "Header": "找不到配装。以下是 DIM 找不到配装的可能原因:", "LowerBoundsFailed": "配装组合中所能达到的属性低于选择的下限", "MaybeAllowMoreItems": "尝试允许其他物品:", "MaybeDecreaseLowerBounds": "尝试降低属性下限", "MaybeRemoveMods": "尝试移除一些模组:", "MaybeRemoveSearchQuery": "尝试清除搜索栏或更改过滤器条件", "ModAssignmentFailed": "很多配装无法装下所有请求的模组", "RemoveMods": "移除这些模组", "RemoveSetBonuses": "尝试移除一些套装加成:", "SetBonuses": "没有合适的装备用于激活选中的套装加成" }, "NoExotic": "非异域", "NoExoticDescription": "等同于在搜索栏中搜索“not:exotic” ,即配装将不含任何异域护甲。", "NoExoticPreference": "未选择异域装备", "NoExoticPreferenceDescription": "如果异域护甲可大幅提高属性,则选取异域护甲。", "NoLoadoutsToCompare": "没有要对比的配装", "None": "无", "OptimizerExplanationGuide": "用户指南里有更多信息和视频教程。", "OptimizerExplanationMods": "选择异域装备、模组和分支职业,为配装贡献属性。忽略已安装在护甲上的模组。", "OptimizerExplanationSearch": "使用搜索栏缩小护甲的筛选范围,如 {{example}}。若有槽位无符合筛选的护甲,则该槽位的搜索无效。", "OptimizerExplanationStats": "将最重要的属性拖动到顶部,并取消选择没有最大化需求的属性。", "OptimizerSet": "优化器组合", "PinnedItems": "固定物品", "PinnedItemsFinePrint": "搜索过滤器已被保存在配装优化器设置中,但不含固定物品和排除物品。当 DIM 发现存在更高属性的配装时,固定物品和排除物品将被忽略。", "ProcessingSets": "寻找最高属性配装…", "SaveAs": "保存为", "SetBonus": "套装加成", "SpeedReport": "在 {{time}} 秒内使用 {{cpus}} 个 CPU 核心生成了 {{combos, number}} 个组合", "StatConstraints": "属性优先级和范围", "StatMax": "最大值", "StatMin": "最小值", "StatRangeTooltip": "根据当前的上下限设置,存在所选属性值为 {{min}} 至 {{max}} 的配装。双击将下限改为 {{max}}。", "StatTotal": "总计:{{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "无法添加所有模组。", "UnableToAddAllModsBody": "模组插槽不足,无法装下 {{mods}}。", "UnlockItem": "取消固定物品" }, "LoadoutFilter": { "Contains": "显示包含过滤器文本中指定的物品或模组的配装。使用引号包裹名称包含空格的物品以便搜索。", "FashionOnly": "显示仅包含外观的配装(着色器或皮肤)。", "LoadoutLight": "显示基于光等计算值的配装。使用 pinnaclecap 或 softcap 关键字而非数字来指定当前赛季的能量限制。", "ModsOnly": "显示仅包含护甲模组的配装。", "Name": "使用 exactname: 精确搜索某名称的配装,或使用 name: 搜索部分匹配某名称的配装。使用引号来搜索包含空格的文本。", "Notes": "按注释搜索配装。", "PartialMatch": "显示名称或注释与过滤器部分匹配的配装。使用引号来搜索包含空格的文本。", "Season": "显示自特定赛季起就未有更改的配装。", "Subclass": "显示分支职业名称或伤害类型与过滤器部分匹配的配装。" }, "Loadouts": { "Abilities": "技能", "Actions": "{{title}} 的操作", "AddEquippedItems": "添加已装备", "AddNotes": "添加备注", "AddUnequippedItems": "添加未装备的", "Any": "全部职业", "Apply": "应用", "ApplyInGameLoadoutInGame": "你的配装已准备就绪,但由于你正在进行活动,你需要在游戏内装备此配装。", "ApplyMods": "正在应用模组", "ApplySearch": "转移搜索 \"{{query}}\"", "ArmorStats": "护甲属性", "ArtifactUnlocks": "神器解锁", "ArtifactUnlocksDesc": "由于 Bungie.net 的限制,DIM无法自动配置你的神器模组。你需要在游戏中进行解锁,然后再应用到配装。", "ArtifactUnlocksWithSeason": "神器解锁 - 第 {{seasonNumber}} 赛季", "BadLoadoutShare": "无法载入共享配装", "BadLoadoutShareBody": "你试图载入的配装无效:{{error}}", "Before": "撤销 '{{name}}'", "CancelEditing": "取消编辑", "CannotCustomizeSubclass": "无法配置此子职业", "ChooseItem": "添加 {{name}}", "ClassType": "全职业配装", "ClassTypeMismatch": "不能将{{className}}物品添加到此配装", "ClassTypeMissing": "你没有{{className}},因此不能为其创建配装", "ClassType_female": "{{className}} 配装", "ClassType_male": "{{className}} 配装", "Classified": "你的某些物品是隐藏的,因此不能用于到最大光等的计算之中。", "ClearLoadoutParameters": "清除配装优化器设置", "ClearSection": "清除所有", "ClearSpace": "移除其他物品", "ClearSpaceArmor": "移除其他护甲", "ClearSpaceWeapons": "移除其他武器", "ClearUnsetMods": "移除其他模组", "ClearingSpace": "正在移走其他物品", "CopyAndEdit": "编辑副本", "Create": "创建配装", "CurrentlyEquipped": "当前装备", "Deequip": "正在从其他角色取回物品", "Delete": "删除", "DimLoadouts": "DIM 配装", "Edit": "编辑配装", "EditBrief": "编辑", "EquipInGameLoadout": "正在装备游戏内配装", "EquipItems": "正在装备物品", "EquippableDifferent1": "计算你的最大能量等级时使用了多件异域装备,所以在游戏内配装时可能达不到这个数值。", "EquippableDifferent2": "在计算你的掉落、强力、巅峰装备能量等级时,最大能量等级不受“一件异域装备”规则的限制。", "Failed": "配装未能完全应用", "Fashion": "选择外貌", "FashionOnly": "仅限外观", "FillFromEquipped": "填入已装备物品", "FillFromInventory": "填入未装备物品", "FilteredItems": "已过滤的物品", "FindAnother": "查找另一个 {{name}}", "FromEquipped": "当前装备", "Generated": "{{statTotal}} 属性点配装", "HashtagTip": "提示:在配装名称或备注中使用的#标签会显示在这里。", "Import": { "BadURL": "配装分享链接无效。", "Error": "获取配装失败:", "Error404": "此配装不存在。", "PasteHere": "粘贴链接以打开配装。" }, "ImportLoadout": "导入配装", "InGameActions": "游戏内配装操作", "InGameLoadouts": "游戏内配装", "IncludeRuntimeStatBenefits": "包括洗礼属性加成", "IncludeRuntimeStatBenefitsDesc": "属性洗礼护甲模组会在你拥有护甲充能时提供固定的属性增益。\n在该设置下,DIM 将视此类模组为生效状态,并将其增益用于配装属性的计算和优化。", "ItemErrorSummary_other": "{{count}} 个物品出错:", "ItemLeveling": "物品等级", "LoadoutName": "配装名称", "LoadoutParameters": "配装优化器设置", "LoadoutParametersExotic": "配装必须包含此异域装备:{{exoticName}}", "LoadoutParametersQuery": "物品必须匹配搜索筛选结果", "LoadoutParametersStats": "属性优先级和最低/最高属性范围", "Loadouts": "配装", "MakeRoom": "为邮政官腾出空间", "MakeRoomDone_female_other": "通过从{{store}} 上移动了{{movedNum}} 项物品,成功为邮政官腾出了{{count}} 个物品空间。", "MakeRoomDone_male_other": "通过从{{store}} 上移动了{{movedNum}} 项物品,成功为邮政官腾出了{{count}} 个物品空间。", "MakeRoomDone_other": "通过从{{store}} 上移动了{{movedNum}} 项物品,成功为邮政官腾出了{{count}} 个物品空间。", "MakeRoomError": "无法为邮政官腾出物品空间:{{error}}.", "ManageLoadouts": "管理配装", "MaxSlots": "一个配装里最多只能有 {{slots}} 个{{bucketName}}。", "MaximizeLight": "最大光等", "MaximizePower": "最大能量等级", "MaximizeStat": "最高属性", "MissingItemsWarning": "该配装中部分物品已不在你的背包中。", "ModErrorSummary_other": "{{count}} 个模组出错:", "ModPlacement": { "InvalidMods": "无效模组", "InvalidModsDesc_other": "有 {{count}} 个模组不能安装到任何护甲上。", "ModPlacement": "模组位置", "StackableMod": "可叠加", "UnassignedMods": "未分配的模组", "UnassignedModsDesc_other": "由于能量容量或模组插槽不足,{{count}} 个模组无法安装。即便为所选护甲升级能量也是如此。", "UnstackableMod": "不可叠加", "UpgradeCosts": "升级消耗成本", "UpgradeCostsDesc": "有些护甲需要能量容量升级才能装下所选的模组。升级总共会花费:" }, "Mods": "模组", "ModsOnly": "仅限模组", "MoveItems": "正在移动物品", "NoSpace": "你的保险库和其他角色均已满。", "NoneMatch": "没有与过滤器匹配的配装。", "NotStarted": "正在等待其他操作完成或物品栏刷新完毕", "NotesPlaceholder": "为此配装添加备注,或使用 #标签 进行分类", "NotificationTitle": "配装:{{name}}", "OnWrongCharacterAdvice": "点此查找当前角色的最高光等物品。", "OnWrongCharacterWarning": "当前角色光等最高的护甲在其他角色上。要使强力装备和巅峰装备掉落时的光等达到最高,这些护甲必须存储在当前角色上或是保险库中。", "OnlyItems": "只有可装备物品、材料和消耗品才可以被添加到配装。", "OpenInOptimizer": "优化护甲", "OpenOnStreamDeck": "在 Stream Deck 上打开", "PickArmor": "选择护甲", "PickMods": "添加护甲模组", "Prismatic": { "Aspect": "棱镜星相", "Grenade": "棱镜手雷", "Melee": "棱镜近战", "Super": "超能" }, "PullFromPostmaster": "从邮政官处取回物品", "PullFromPostmasterError": "无法从邮政官处取回物品:{{error}}.", "PullFromPostmasterGeneralError": "无法从邮政官拉取所有物品。", "PullFromPostmasterNotification_female_other": "正在从邮政官处取回{{count}} 项物品至{{store}}。", "PullFromPostmasterNotification_male_other": "正在从邮政官处取回{{count}} 项物品至{{store}}。", "PullFromPostmasterNotification_other": "正在从邮政官处取回{{count}} 项物品至{{store}}。", "PullFromPostmasterPopupTitle": "从邮政官处取回物品", "Random": "随机", "Randomize": "随机配装", "RandomizeButton": "随机搭配", "RandomizeNew": "随机创建", "RandomizeQueryHint": "提示:请先搜索物品以限制随机物品池", "RandomizeSearch": "从搜索结果随机选取", "RandomizeSearchPrompt": "从“{{query}}”的搜索结果随机配装?", "Redo": "重做", "RestoreAllItems": "所有物品", "SalvationsEdgeMods": "救赎的边缘模组", "Save": "保存", "SaveAsDIM": "保存为 DIM 配装", "SaveAsNew": "另存为", "SaveAsNewTooltip": "保留原始配装并将其保存为新配装", "SaveDisabled": { "AlreadyExists": "为该配装选择一个新的名称。", "Empty": "配装为空。", "NoName": "配装需要名称。" }, "SaveLoadout": "保存配装", "Season": "第 {{season}} 赛季", "SetBonusesDesc": "需要的套装加成", "Share": { "Copied": "已复制配装链接到剪贴板", "CopyButton": "复制链接", "Error": "获取分享链接时出错", "Fashion": "外貌(着色器和皮肤)", "LoadoutOptimizer": "配装优化器设置", "NativeShare": "分享链接", "Notes": "备注", "NumItems_other": "{{count}} 个物品 - 被分享者需要从他们的库存中选择替代物品", "NumMods_other": "{{count}} 个模组", "Placeholder": "正在加载分享链接", "Subclass": "自定义子职业", "Summary": "分享包含如下内容的配装:", "Title": "分享“{{name}}”" }, "ShareLoadout": "分享", "ShowModPlacement": "显示模组位置", "Snapshot": "保存为游戏内配装", "SocketOverrides": "正在更改子职业选项", "SortByEditTime": "按最后编辑时间排序", "SortByName": "按名称排序", "SubclassOptions": "{{subclass}}选项", "SubclassOptionsSearch": "搜索{{subclass}}选项", "Succeeded": "配装成功", "SyncFromEquipped": "同步已装备物品", "TooManyRequested": "你有{{total}} 个{{itemname}}, 但完成配装需要{{requested}} 个. 我们已经处理所有已拥有的", "TuningMods": "调整模组", "UnassignedModError": "模组不适合你当前的护甲", "Undo": "撤销", "Update": "保存更改", "UpdateLoadout": "更新配装", "VendorsCannotEquip": "您没有这些项目。点击来选择一个替换或点击 X 来删除:" }, "Manifest": { "Download": "正在从 Bungie 下载最新的命运信息数据库...", "Error": "加载命运信息数据库时出错:\n{{error}}\n请刷新重试。", "Load": "正在加载命运信息数据库..." }, "Milestone": { "Daily": "每日挑战", "OneTime": "一次性挑战", "SeasonalRank": "赛季等级 {{rank}}", "Special": "特殊事件挑战", "Tutorial": "挑战教程", "Unknown": "挑战", "Weekly": "每周挑战" }, "Mods": { "HarmonicModDescription": "该模组生效仅需更少的护甲能量,并会根据所使用的职业分支不同改变元素类型。" }, "MoveAmount": { "Amount": "数量:" }, "MovePopup": { "Acquired": "此物品已在收藏中解锁。", "AcquiredMod": "此模组已在收藏中解锁。", "AddNote": "添加备注", "AddToLoadout": "配装", "AddToLoadoutTitle": "添加此物品到配装", "All": "全部", "ArtifactBreaker": "此武器由已解锁的神器特性获得了 {{breaker}}。", "CannotCurrentlyRoll": "此特性不会出现在此物品的当前版本上。", "CantPullFromPostmaster": "你必须去邮政官处取回此物品。", "CatalystProgress": "催化剂进度", "CommunityData": "社区见解", "Consolidate": "合并", "DistributeEvenly": "均匀分配", "EnhancementTier": "{{tier}} 阶", "Equip": "装备到:", "EquipWithName": "装备在{{character}}", "FavoriteUnFavorite": { "Favorite": "收藏{{itemType}}", "Favorited": "已收藏", "Unfavorite": "取消收藏{{itemType}}", "Unfavorited": "已取消收藏" }, "Infuse": "灌注", "InfuseTitle": "打开灌注材料查找器", "IntrinsicBreaker": "此武器固有 {{breaker}}。", "LoadingSockets": "尚未加载此物品的特性和属性详情。", "LockUnlock": { "AutoLock": "该物品的锁定状态已与其标签同步", "Lock": "锁定 {{itemType}}", "Locked": "已锁定", "Unlock": "解锁{{itemType}}", "Unlocked": "已解锁" }, "MissingSockets": "在 Bungie 维护其服务器时,无法查看特性和模组的详细信息,直到维护完成。这通常需要几个小时。", "Notes": "备注:", "OpenOnStreamDeck": "在 Stream Deck 上打开", "OverviewTab": "概述", "Owned": "此物品在您的库存中。", "OwnedMod": "该模组在您的模组库存中。", "PullItem": "从 {{bucket}} 移到 {{store}}", "PullPostmaster": "从邮政官处取回物品", "ReadLore": "在Ishtar Collective阅读背景故事", "ReadLoreLink": "阅读传奇故事", "Rewards": "奖励:", "SendToVault": "发送到保险库", "Store": "移到:", "StoreWithName": "移到 {{character}}", "Subtitle": { "QuestProgress": "第 {{questStepNum}} / {{questStepsTotal}} 步", "Type": "{{classType}} {{typeName}}" }, "TabList": "物品细节页面", "ToggleSidecar": "展开或折叠物品操作", "TrackUntrack": { "Track": "跟踪{{itemType}}", "Tracked": "已跟踪", "Untrack": "取消跟踪{{itemType}}", "Untracked": "未跟踪" }, "TriageTab": "分类", "UnreliablePerkOption": "该特性仅出现在收藏品界面中。随机掉落的物品可能不会带有该特性。", "Vault": "保险库", "WeaponLevel": "武器等级 {{level}}" }, "Notes": { "Error": "错误! 备注不能超过120个字符.", "Help": "添加备注、#标签、和 :符号:" }, "Notification": { "Cancel": "取消", "OK": "关闭" }, "Objectives": { "Complete": "已完成", "Incomplete": "未完成" }, "Organizer": { "BulkMove": "移动到", "BulkMoveLoadoutName": "已在管理器中选中", "BulkTag": "标签", "Columns": { "Ammo": "弹药", "Archetype": "固有特性", "BaseStats": "基础属性", "Breaker": "对抗勇士", "Crafted": "铸造日期", "CustomTotal": "自定义总和", "Damage": "伤害", "Energy": "能量", "Event": "活动", "Featured": "新装备", "Foundry": "铸造厂", "Frame": "框架", "Harmonizable": "可协调", "Holofoil": "全息箔", "Icon": "图标", "ItemTier": "阶级", "KillTracker": "击杀数", "Level": "等级", "Loadouts": "配装", "Location": "位置", "Locked": "已锁定", "MasterworkStat": "大师杰作属性", "MasterworkTier": "大师杰作等级", "ModSlot": "模组槽位", "Mods": "模组", "Name": "名称", "New": "新", "Notes": "备注", "OriginTraits": "原始特性", "OtherPerks": "武器组件", "PercentComplete": "% 已完成", "Perks": "特性", "PerksGrid": "特性表格", "Power": "光等", "Quality": "质量 %", "Recency": "新旧", "Season": "赛季", "Shaders": "外观", "Source": "来源", "StatQuality": "属性质量", "StatQualityStat": "{{stat}}%", "Stats": "属性", "Tag": "标签", "TertiaryStat": "第三属性", "Tier": "稀有度", "Traits": "武器特性", "TuningStat": "调整", "WishList": "愿望清单", "WishListNotes": "愿望清单备注", "Year": "年" }, "EnabledColumns": "已启用的列", "Lock": "锁定", "NoItems": "没有符合过滤器条件的结果。如有搜索建议,请将其删除。", "NoMobile": "将手机横放来使用管理器。", "Note": "添加备注", "OpenIn": "在管理器中显示", "Organizer": "管理器", "SelectAll": "全选", "SelectItem": "选择或取消选择 {{name}}", "ShiftTip": "提示:按住 Shift 键并点击单元格以筛选项目", "Stats": { "Aim": "瞄准", "Airborne": "空中效率", "AmmoGeneration": "弹药生成", "Power": "光等", "RPM": "弹夹数每分钟", "Recoil": "后坐力", "Reload": "重新装弹" }, "Unlock": "解锁​​​​" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "邮政官快满了!({{number}}/{{postmasterSize}})", "PostmasterFull": "邮政官全满了!({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "赏金", "CatalystSource": "来源:{{source}}", "CrucibleRank": "排名", "Items": "任务物品", "Milestones": "里程碑和挑战", "NoEventChallenges": "你已经完成了所有活动挑战", "NoTrackedTriumph": "你没有正在追踪的成就,您可以在 DIM 中追踪任意数量。", "PaleHeartPathfinder": "苍白之心寻路者", "PercentMax": "{{pct}}% 最大值", "PercentPrestige": "重置百分比 {{pct}} %", "PointsUsed_other": "已用 {{count}} 点", "PowerBonusHeader": "+{{powerBonus}} 光等奖励", "PowerBonusHeaderUndefined": "其他奖励", "Progress": "进度", "QueryFilteredTrackedTriumphs": "你正在追踪的成就均不匹配此搜索", "QuestExpired": "已过期", "QuestExpires": "有效期至 ", "Quests": "任务", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}点", "Resets_other": "{{count}} 次重置", "RewardPassEndsIn": "奖励通票结束于 ", "RewardPassPrestigeRank": "威望等级 {{rank}}", "SeasonalHub": "赛季中心", "StatTrackers": "数据追踪器", "TrackedTriumphs": "跟踪的成就" }, "RecordBooks": { "HideCompleted": "隐藏完成事件", "RecordBooks": "记事本" }, "Records": { "Title": "记录", "UniversalOrnamentSetOther": "其他" }, "SearchHistory": { "Date": "最近使用", "DeleteAll": "删除所有未加星标的搜索", "Description": "这些是你过去和保存的搜索。你可以在这里删除它们。", "Item": "物品搜索历史", "Link": "查看和编辑搜索历史", "Loadout": "配装搜索历史", "Query": "搜索​​​​", "Title": "搜索历史", "UsageCount": "使用次数" }, "Settings": { "Appearance": "外观", "ArmorArchetypeModslot": "护甲框架 / 模组槽位", "AutoLockTagged": "将物品锁定状态与标签同步", "AutoLockTaggedExplanation": "DIM 将自动锁定和解锁物品以匹配它们的标签。制作获得的武器将不会锁定,以便你进行重塑。当启用此设置时,锁定图标不会显示在带标签的物品图标上。", "BadgePostmaster": "在 App 图标上显示当前角色的邮政官物品数量", "BadgePostmasterExplanation": "若要此功能生效,你必须将 DIM 安装为 App,且操作系统必须支持显示标记", "BothDescriptions": "Bungie 与社区描述", "BungieDescriptionOnly": "Bungie 描述", "CharacterOrder": "排序方式", "CharacterOrderFixed": "角色年龄(PC平台有错误)", "CharacterOrderRecent": "最近使用的角色", "CharacterOrderReversed": "最近使用的角色(倒序)", "ColumnSize": "{{num}} 个物品", "ColumnSizeAuto": "自动", "CommunityData": "社区特性见解", "CommunityDescriptionOnly": "社区描述", "CsvImport": "导入 CSV", "CustomErrorLabel": "属性名称必须包含单词字符,且不得与所属职业的其他自定义属性名称相同。", "CustomErrorValues": "属性值必须是正数。\n至少两项属性值大于零。", "CustomStatChooseName": "为自定义属性命名", "CustomStatCreate": "创建新的自定义属性", "CustomStatDelete": "删除该自定义属性", "CustomStatDeleteConfirm": "删除该自定义属性?", "CustomStatDesc1": "选择所需的护甲属性来创建自定义属性总和。", "CustomStatDesc3": "自定义属性将出现在弹窗、配装优化器和多件装备的比较上。", "CustomStatTitle": "自定义属性总和", "Data": "电子表格", "DefaultItemSizeNote": "50px 的物品大小看起来最清晰,而且不会模糊物品图片或文本。", "DontForgetDupes": "别忘了,你可以搜索 is:dupe 来快速找到重复的物品,也可以用对比工具或管理器来比较相关物品。", "EnableAdvancedStats": "在装备上显示质量指数(D1)", "ExpandSingleCharacter": "显示所有角色", "ExportLoadoutSS": "配装电子表格", "ExportLoadoutSSHelp": "下载您 DIM 配装的 CSV 列表,可以在您选择的电子表格应用程序中轻松查看。", "ExportProfile": "导出 API 返回的账户资料", "ExportSS": "背包表格", "ExportSSHelp": "下载您物品的 CSV 列表, 可以在您选择的电子表格应用程序中轻松查看。", "HidePullFromPostmaster": "隐藏“$t(Loadouts.PullFromPostmaster)”按钮", "Inventory": "物品栏显示模式", "InventoryColumns": "物品栏显示宽度", "InventoryColumnsMobile": "移动端物品栏显示宽度", "InventoryColumnsMobileLine2": "将自动调整物品大小以适配当前设置", "InventoryNumberOfSpacesToClear": "启用收集模式时要留出的空位数量", "Items": "物品显示", "Language": "语言", "LogOut": "登出", "Masterworked": "大师杰作", "MaxParallelCores": "并行任务最大核心数", "MaxParallelCoresExplanation": "控制 DIM 可参与密集型计算(如配装优化和分析任务)的 CPU 核心数量。较高的数值将提升性能,但会占用更多的系统资源。", "OrnamentDisplay": "在物品格上显示幻化", "OrnamentDisplayExplanationDisabled": "不显示幻化", "OrnamentDisplayExplanationEnabled": "悬停或长按护甲时隐藏幻化", "OrnamentDisplayExplanationHide": "悬停或长按装备时隐藏幻化", "OrnamentDisplayExplanationShow": "悬停或长按装备时显示幻化", "ResetToDefault": "重置", "RestoreVaultSide": "保险库中的物品单独成列", "ReverseSort": "切换正向/反向排序", "SetSort": "物品排序方式:", "SetVaultWeaponGrouping": "保险库武器分组方式:", "Settings": "设置", "ShowNewItems": "在新物品上显示红点", "SingleCharacter": "单角色视图", "SingleCharacterExplanation": "DIM 将只显示最近一次游玩的角色。\n如果隐藏的角色持有当前角色可用的物品,它们会被显示在保险库里。\n其他职业独有的物品会被完全隐藏。", "SizeItem": "物品显示大小", "SortByAmmoType": "弹药类型", "SortByAmount": "堆叠大小", "SortByClassType": "职业要求", "SortByCrafted": "已制造(D2)", "SortByDeepsight": "深视共振(D2)", "SortByFeatured": "新装备 / 特色装备(D2)", "SortByPrimary": "光等", "SortByRarity": "稀有度", "SortByRating": "护甲质量(D1)", "SortByRecent": "最近获得(D2)", "SortBySeason": "赛季(D2)", "SortByTag": "标签{{taglist}}", "SortByTier": "阶级(D2)", "SortByType": "类型", "SortByWeaponElement": "伤害类型", "SortCustom": "自定义排序", "SortName": "名称", "SpacesSize_other": "{{count}} 个空位", "Theme": "主题", "Troubleshooting": "故障排除", "VaultArmorGroupingStyle": "将不同职业护甲隔行分开", "VaultGroupingNone": "无", "VaultUnder": "保险库物品显示移至装备下方", "VaultWeaponGroupingStyle": "将不同类型武器隔行分开", "WeaponFrame": "武器框架", "WishlistRefreshNotificationBody": "如果您没有看到任何更新,请检查愿望清单的来源(例如 GitHub)是否有反应!", "WishlistRefreshNotificationTitle": "愿望清单已重载" }, "Sockets": { "ApplyPerks": "应用特性", "GridStyle": "按网格显示特性", "Insert": { "Ability": "装备技能", "Aspect": "插入星相", "Fragment": "插入碎片", "Mod": "插入模组", "Ornament": "应用皮肤", "Projection": "应用机灵投影", "Shader": "应用着色器", "Super": "装备超能", "Transmat": "应用传送效果" }, "ListStyle": "按列表显示特性", "Search": "搜索名称或说明", "Select": { "Ability": "预览技能", "Aspect": "预览星相", "Fragment": "预览碎片", "Mod": "预览模组", "Ornament": "预览皮肤", "Projection": "预览机灵投影", "Shader": "预览着色器", "Super": "预览超能", "Transmat": "预览传送效果" }, "SelectWishlistPerks": "预览愿望单特性" }, "Stats": { "CrouchingSpeed": "蹲伏移动速度", "Custom": "自定义总和", "CustomDesc": "所选基础属性的自定义总和,未计入模组或大师杰作。可在设置里配置自定义总和所包含的属性。", "DamageResistance": "PvE伤害减免", "Discipline": "训练", "DropLevel": "账户光等", "DropLevelExplanation1": "账户光等是计算奖励包裹提升光等时的基准值", "DropLevelExplanation2": "账户光等取决于每个装备栏最高光等的物品,而不考虑装备的职业要求或只能同时装备一件异域护甲/武器的规则", "EquippableGear": "可用装备", "FlinchResistance": "退缩抗性", "HP": "生命值", "Intellect": "智慧", "MaxGearPower": "当前装备最大光等", "MaxGearPowerAll": "所有装备最高光等", "MaxGearPowerOneExoticRule": "当前装备最高光等\n(只装备一件异域护甲的情况下)", "MaxTotalPower": "最大光等", "MetersPerSecond": "米/秒", "Milliseconds": "毫秒", "NoBonus": "无加成", "NotApplicable": "不可用", "OfMaxRoll": "最大下拉范围{{range}}", "PercentHelp": "点击获取更多关于品质属性的信息.", "Percentage": "%", "PowerModifier": "赛季神器光等加成", "Prestige": "威望等级: {{level}}\n{{exp}}xp 于5明亮之尘之前.", "Quality": "品质", "ShieldHP": "护盾容量", "StrafingSpeed": "侧向移动速度", "Strength": "力量", "TierProgress": "{{tier}} 阶{{statName}} (距 {{nextTier}} 阶 {{progress}}/60)", "TierProgress_Max": "{{tier}} 阶{{statName}} ({{progress}}/300)", "TimeToFullHP": "恢复满生命值耗时", "Total": "总计", "TotalHP": "总生命值", "WalkingSpeed": "步行移动速度", "WeaponPart": "武器部分" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM 现在可以将你的标签、配装和设置存储在云端,并将其同步给不同版本的 DIM,且无需另外创建账号。如果你从未启用过 DIM 同步,你可以在设置里导入现有数据。这个功能离不开我们 OpenCollective 支持者的帮助!", "No": "现在不行", "Title": "启用 DIM 同步吗?", "Yes": "启用同步" }, "AutoBackup": "为了以防万一,我们已经把你的数据备份到了下载文件夹里的 dim-data.json 文件。", "BackUpFirst": "为了以防万一,在全部删除数据前,你必须先将其备份。", "BrowserMayClearData": "如果硬盘空间不足或长时间未访问DIM,浏览器有可能会删除这些数据信息。", "DataIsLocal": "标签和备注数据仅在本地保存", "DeleteAllData": "从 DIM 同步服务器删除所有数据", "DeleteAllDataConfirm": "你确定要从 DIM 同步中删除你的所有账号的所有数据吗?这无法撤销。", "Details": { "IndexedDBStorage": "本地存储仅在此浏览器中存储信息。如果删除浏览器数据,也会删除这些信息。" }, "DimApiFinePrint": "DIM 会把你的标签、配装和设置保存到云端,并同步给 DIM 的不同版本。", "DimSyncDown": "与服务器通信时出现问题,DIM 同步尚未连接。", "DimSyncEnabled": "DIM 同步已启用", "DimSyncNotEnabled": "DIM 同步未启用,所以你的设置、标签、配装和搜索将仅在本地保存,会在清理浏览器存储时丢失。在设置中启用 DIM 同步便可自动或随时手动备份你的数据。", "EnableDimApi": "启用 DIM 同步(推荐)", "Export": "下载数据备份", "ExportError": "无法从 DIM 同步下载备份", "ExportErrorBody": "DIM 同步或者你的网络连接可能出现了问题。我们会下载你本地保存的数据。", "Import": "导入数据备份", "ImportConfirmDimApi": "你确定要用此版本的数据覆盖标签、配装和设置吗?旧数据将被完全覆盖。", "ImportExport": "备份和导入", "ImportFailed": "导入失败!{{error}}", "ImportNoFile": "没有选择文件!", "ImportNotification": { "FailedBody": "无法导入数据。 {{error}}", "FailedTitle": "导入失败", "NoData": "未在备份中找到配装或标签", "SuccessBodyForced": "已从你的备份中导入了 {{loadouts}} 个配装和 {{tags}} 个已标记的物品到 DIM 同步,并覆盖了旧内容。", "SuccessBodyLocal": "已从你的备份向本地存储导入并覆盖了设置、{{loadouts}} 个配装,和 {{tags}} 个已标记的物品。我们不能保证本地存储里的数据完好——请考虑启用 DIM 同步。", "SuccessTitle": "导入成功" }, "ImportTooManyFiles": "请选择一个文件导入。", "ImportWrongFileType": "文件不是 JSON 文件。它可能不是 DIM 备份。", "IndexedDBStorage": "本地浏览器存储", "LearnMore": "了解 DIM 同步", "MenuTitle": "同步和备份", "ProfileErrorBody": "与 DIM 同步通信时出错。可能无法显示你最新的设置、标签、配装和搜索。你的数据仍在我们的服务器上,而我们会在能重新连接时保存你的本地更改。我们会在 DIM 开启时自动重试。", "ProfileErrorTitle": "DIM 同步下载错误", "RefreshDimSync": "从 DIM 同步服务重载远程数据", "UpdateErrorBody": "向 DIM 同步保存你的数据时出了点问题。我们会在 DIM 开启时自动重试。", "UpdateErrorTitle": "DIM同步保存错误", "UpdateInvalid": "未能将数据保存到 DIM 同步", "UpdateInvalidBody": "发送至 DIM 同步的数据无效,未被保存。", "UpdateInvalidBodyLoadout": "配装 \"{{name}}\" 无效,未被保存。若您从其他网站导入了此配装,请告知发布者其配装无效。", "UpdateQueueLength_other": "我们将在能重新连接时保存 {{count}} 个新更改。", "Usage": "DIM 使用了此设备{{quota, humanBytes}}可用空间中的{{usage, humanBytes}}.这包括从 bungie. net 下载的命运2物品数据库。" }, "StreamDeck": { "Authorize": "连接应用程序", "Enable": "直播控制台插件", "Error": { "Body": "在向 Stream Deck 插件传输数据时出现错误。请联系插件开发者。{{error}}", "Title": "Stream Deck 插件错误" }, "FinePrint": "启用与DIM Stream Deck插件的连接。这个插件是一个独立的项目,既不是由DIM团队编写的,也不是由DIM团队支持的。", "Install": "安装插件", "MissingAuthorization": "您必须授权 Stream Deck 应用程序连接 DIM。请在设置中点击“连接应用程序”。", "Tooltip": { "Application": "Stream Deck 应用程序", "AuthRequired": "点击此按钮或在设置中点击“连接应用程序”", "Error": "您的 Stream Deck 插件已不受支持,请更新到最新版本。此插件的最低版本要求:", "ErrorConnection": "如果您已经在使用最新版本,请检查某些浏览器扩展是否正在阻止连接。", "ExtensionIssue": "扩展问题", "Plugin": "插件", "Title": "DIM Stream Deck 插件", "Version": "版本:" } }, "StripSockets": { "Action": "清空插槽", "ArmorMods": "{{count}}x 护甲模组", "Button": "清空 {{numSockets}} 个插槽", "Cancel": "取消", "Choose": "选择要清空的插槽", "DiscountedMods": "{{count}}x 已享受折扣的模组", "Done": "已清空的插槽", "NoSockets": "无插槽可清空", "Ok": "确定", "Ornaments": "{{count}}x 皮肤", "Others": "{{count}}x 机灵投影", "Running": "清空插槽", "Shaders": "{{count}}x 着色器", "Subclass": "{{count}}x 子职业选项", "WeaponMods": "{{count}}x 武器模组" }, "Tags": { "Archive": "归档", "ClearTag": "清除标签", "Favorite": "青睐", "Infuse": "灌注", "Junk": "垃圾", "Keep": "保留", "LockAll": "锁定物品", "TagItem": "标记物品", "UnlockAll": "解锁物品" }, "Triage": { "AccountsForArtifice": "判断诡计护甲在使用 +3 属性模组后是否会变得更好。", "BetterArmor": "全面更优的护甲", "BetterArtificeArmor": "更优的诡计护甲", "BetterStatArmor": "属性更优的护甲", "BetterStatArtificeArmor": "属性更优的诡计护甲", "BetterWorseArmor": "更优/更劣的护甲", "BetterWorseIncludes": "分析护甲所用条件:", "HighStats": "高属性", "InLoadouts": "在配装中", "OwnedCount": "# 已拥有", "PerkBetterArmorDesc": "相同、更多特性,或特殊模组槽位的护甲", "PerkWorseArmorDesc": "固有特性相同或不存在。", "SimilarItems": "相似物品", "StatBetterArmorDesc": "所有属性均持平,并有至少一个属性更高。", "StatNotPerkArmorDesc": "仅检查护甲属性。低属性护甲可能仍有特殊模组槽位或固有特性。", "StatWorseArmorDesc": "所有属性均持平或更低。", "ThisItem": "当前物品", "WorseArmor": "全面更劣的护甲", "WorseArtificeArmor": "更劣的非诡计护甲", "WorseStatArmor": "属性更劣的护甲", "WorseStatArtificeArmor": "属性更劣的非诡计护甲", "YourBestItem": "你的最佳物品" }, "Triumphs": { "GildingTriumph": "镀金成就", "HideCompleted": "隐藏已完成的成就", "RevealRedacted": "显示隐藏的成就", "SortRecords": "优先显示未完成成就" }, "Vendors": { "Collections": "收藏品", "Engram": "等级", "FilterToUnacquired": "仅显示未收集物品", "HideSilverItems": "隐藏银币物品", "NoItems": "此商人目前没有提供任何物品。", "RefreshTime": "库存刷新倒计时:", "Vendors": "商贩" }, "Views": { "About": { "APIHistory": "查看DIM(及其他命运2app)的历史记录", "BungieCopyright": "所有图像和内容都归Bungie所有。", "CommunityInsight": "特性社区见解及角色属性数据由 {{clarityLink}} 贡献。如果你发现不准确之处或有疑问,欢迎加入 {{clarityDiscordLink}}。", "Discord": "Discord", "DiscordHelp": "报告问题,反馈建议,或在Discord频道中获得支援。", "FAQ": "常见问题", "FAQAccess": "DIM是如何得到我的Destiny(命运)数据的?", "FAQAccessAnswer": "我们使用Bungie的APP身份验证获取权限以便查看和移动您的物品,这与命运同伴应用(Companion APP)的工作原理相同。因此DIM绝不可能窃取您的用户名或密码。", "FAQKeyboard": "我可以在DIM上使用键盘快捷键吗?", "FAQKeyboardAnswer": "当然!按下 \"Shift+?\" 键查看所有可用的快捷键。", "FAQLogout": "如何注销DIM?", "FAQLogoutAnswer": "从左上角图标打开菜单,并选择“登出”", "FAQLostItem": "在使用你们网站的过程中我丢失了我的物品!", "FAQLostItemAnswer": "Bungie Api不允许APP删除物品(包括他们自己的官方APP)。这看起来似乎更像是一次数据传输错误,把您的物品遗留在了另一个角色的保险库里。你可以试试看在其它角色上能否找到。如果其它角色上也没有,请刷新下页面。然后打开此链接{{link}} 查看,或者在游戏中查看物品是否存在。不过我们肯定东西没有丢失。", "FAQMobile": "DIM支持移动端吗?是否会提供一个APP?", "FAQMobileAnswer": "DIM网站已可以在手机和平板上加载,您可以将其添加至主页以获得应用一样的体验。", "GitHub": "Github", "GitHubHelp": "如果您有兴趣为该项目做出一份贡献,请访问我们在{{link}} 上的项目地址!", "Header": "DIM (命运物品管理器)", "HowItsMade": "DIM是一个由社区开发者在Bungie.net和Destiny Companion 应用程序使用的同类服务基础上开发的免费、开源的应用", "Schedule": { "beta": "每当我们更改代码时这个测试版的DIM就会自动应用更新 - 它将应用最新的功能和修复补丁,当然咯 也会有最新的Bug哦~", "release": "此版本的DIM将于太平洋时间的每周日午夜进行例行版本更新。" }, "Translation": "加入翻译小组!", "TranslationText": "我们使用 {{link}} 工具来使得翻译工作更加简便。如果您希望为 DIM 的语言翻译出一份力,那么请加入我们的翻译小组吧( • ̀ω•́)✧!", "Version": "当前版本 {{version}} ({{flavor}}),编译于:{{date}}", "Wiki": "DIM 用户指南", "WikiHelp": "了解 DIM 各项功能的使用方法。" }, "Login": { "Auth": "用Bungie.Net授权", "EnableDimSyncWarning": "你曾禁用过 DIM 同步,仅使用本地数据存储。启用 DIM 同步将用 DIM 同步数据替换本地数据。在启用 DIM 同步前,应该备份你的数据。你可以在设置中恢复备份。", "Explanation": "允许DIM查看并修改您的命运角色,保险库和进度。", "LearnMore": "了解账户与登录", "NewAccount": "使用其他 Bungie.net 账户登录", "Permission": "我们需要您的授权" }, "Support": { "BackersDetail": "您可以选择一次性捐助或者每月捐助,以便我们能更加积极地开发。", "FreeToDownload": "DIM是一款免费下载使用的产品。同时DIM也是一个开源项目,任何人都可以帮助完善它。您永远不会在DIM上看到任何一条广告。这是我们的承诺!", "OpenCollective": "我们使用{{link}} 服务为我们的开发者提供酬劳,以答谢他们在这个项目上的贡献。", "Store": "我们将我们的标志与其他设计结合,并在{{link}}出售", "Support": "赞助DIM" } }, "WishListRoll": { "BestRatedTip_other": "这些特性与您愿望清单上的某个武器完全匹配。", "Clear": "清除愿望清单", "CopiedLine": "已复制愿望清单特性组合到剪贴板", "CopyLine": "复制选定的特性为愿望清单特性组合", "DupeRolls": " (+{{num, number}} 个被忽略的重复项)", "ExternalSource": "添加另一个愿望清单", "ExternalSourcePlaceholder": "在此粘贴愿望清单URL", "Header": "愿望清单", "Import": "导入愿望清单物品", "ImportError": "从 \"{{url}}\" 加载愿望清单时发生错误: {{error}}", "ImportFailed": "您的愿望清单中没有任何有效的特性组合。", "ImportNoFile": "未选择文件。", "InvalidExternalSource": "请为你的外部愿望清单来源输入一个有效的 URL。URL 的开头必须为以下之一:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "最后更新于 {{lastUpdatedDate}} {{lastUpdatedTime}}", "Num": "在您的愿望单中有 {{num, number}} 种组合", "NumRolls": "{{num, number}} 种组合", "Refresh": "刷新愿望清单", "SourceAlreadyAdded": "愿望清单已添加", "UpdateExternalSource": "添加愿望清单", "Voltron": "voltron(默认)", "WishListNotes": "愿望清单备注:", "WorstRatedTip_other": "这些特性与您垃圾清单上的某个武器完全匹配。" }, "no-space": "空间不足", "wrong-level": "等级错误" } ================================================ FILE: src/locale/zhCHT.json ================================================ { "AWA": { "ConfirmDescription": "請使用\"命運2同伴應用\"授予DIM修改您物品的許可權。", "ConfirmTitle": "確認操作", "Error": "更改模組或特性時出錯", "ErrorMessage": "我們無法將{{plug}} 裝備到{{item}}。\n\n\n{{error}}", "FailedToken": "無權限更換物品", "IrreversiblePlugging": "不擁有{{plug}},所以我們不會覆蓋它。" }, "Accounts": { "Choose": "{{bungieName}} 的檔案", "ErrorLoadInventory": "無法加載你的《命運 {{version}}》角色和庫存", "ErrorLoadManifest": "無法從 Bungie 加載命運信息數據庫", "ErrorLoading": "無法從 Bungie.net 加載命運賬戶", "MissingAccountWarning": "如果你的賬戶沒有在此處顯示,你可能登入了錯誤的 Bungie.net 賬戶,或 Bungie.net 可能正在停機維修。", "MissingDescription": "你試圖查看的賬戶並未連接到你的 Bungie.net 賬戶。請在下方選取你的賬戶。", "MissingTitle": "找不到帳戶", "NoCharacters": "沒有找到與此Bungie.net賬戶相關聯的Destiny角色,登入其它賬戶試試看。", "NoCharactersTitle": "未找到角色", "SwitchAccounts": "你可以稍後在選單切換賬戶。", "Title": "帳戶" }, "Activities": { "Activities": "活動", "Hard": "困難", "Nightfall": "日暮", "Normal": "綜合", "WeeklyHeroic": "每週英雄突擊" }, "Armory": { "AlternateItems": "Alternate Versions", "Armory": "軍械庫", "DifferentSeason": "Reissue from a different season", "NoNotes": "無注釋", "OpenInArmory": "查看武器詳情", "Season": "年 {{year}},第 {{season}} 賽季", "TrashlistedRolls_other": "{{count, number}} 個垃圾列表特性組合", "Unknown": "未知物品", "UnknownPerkHash": "The perk hash {{hash}} ({{perkName}}) does not appear on this item, so this wish list roll is invalid. Please contact the wish list author to correct this. Note that wish lists should always specify the non-enhanced version of perks.", "WishlistedRolls_other": "{{count, number}} 個願望清單特性組合", "YourItems": "你的物品" }, "Browsercheck": { "Samsung": "Samsung Internet can make sites look too dark when dark mode is on. Enable Settings > Labs > Use website dark theme or switch to another browser.", "Steam": "Steam覆蓋層的瀏覽器已經非常老了,可能無法使用一些DIM的功能,我們無法提供相關支援。", "Unsupported": "DIM團隊不支持使用此瀏覽器。 DIM可能部分或完全不可用。" }, "Bucket": { "Armor": "防具", "Class": "子職業", "General": "一般", "Ghost": "機靈", "Inventory": "背包", "Postmaster": "郵政官", "Progress": "進度", "Reputation": "聲望", "Unknown": "未知", "Vault": "保險庫", "Weapons": "武器" }, "BulkNote": { "Append": "附加到備注 / 添加 #標簽", "Confirm": "更新説明", "Remove": "從備注中移除 / 移除 #標簽", "Replace": "替換備注", "Title_other": "更改 {{count}} 個物品的備注" }, "BungieAlert": { "Title": "一則來自Bungie的資訊:" }, "BungieService": { "AppNotPermitted": "DIM 沒有權限執行此操作。", "DestinyCannotPerformActionAtThisLocation": "你不能在活動中裝備物品或更換模組。 回到軌道或前往社交區域再試試看。 這是Bungie. net API而不是DIM的限制。", "DestinyItemUnequippable": "你不能裝備此物品。 如果此角色的上一場活動鎖定了其裝備,請嘗試重新登入此角色。", "DestinyLegacyPlatform": "目前Bungie的服務出現了一個Bug,如果您在上一代主機上遊玩過Destiny1,那麼DIM將無法載入您的Destiny2帳戶資訊。Bungie將很快修復這個Bug。但是,直到Bug修復完成前,您必須在目前世代主機上遊玩Destiny1以便DIM能讀取您的帳戶資訊。", "DevVersion": "您是否正在運行DIM的開發版本?您必須先將您的Chrome擴展與Bungie.net關聯。", "Difficulties": "Bungie.net目前存在問題。", "ErrorTitle": "Bungie.net 出現一個錯誤", "ItemUniquenessExplanation": "A character can only have one of '{{name}}' on it.", "Maintenance": "Bungie.net伺服器停機維護中.", "MissingInventory": "Bungie.net 沒有返回你的物品欄,可能是因為你的隱私設置阻止了它。請嘗試重新登錄。", "NetworkError": "網路錯誤 - {{status}}{{statusText}}", "NoAccount": "沒有找到任何關聯的 Destiny 帳戶,請確保您選擇了正確的平台。", "NoAccountForPlatform": "在{{platform}}上沒有找到Destiny帳戶.", "NotConnected": "您可能沒有連接到網路.", "NotConnectedOrBlocked": "你可能沒有連接到網路,或是廣告攔截插件和隱私拓展阻止了Bungie.net.", "NotLoggedIn": "請授權DIM以使用此應用程式", "Slow": "Bungie.net 目前緩慢", "SlowDetails": "Bungie.net 響應緩慢。這可能是因為玩家過多,或 Bungie.net 目前存在問題。還有可能是你的網絡連接問題。我們會繼續等待響應。", "SlowResponse": "Bungie.net響應緩慢。", "Throttled": "Bungie.net限制了DIM可以發起多少個請求。", "Twitter": "查看伺服器更新狀態:", "UnknownError": "來自Bungie.net的消息:{{message}}", "VendorNotFound": "商販資料不可用." }, "Compare": { "Archetype": "原型", "AssumeMasterworked": "假定大師之作", "AssumeMasterworkedDescription": "Stats if fully Masterworked, without current Mods", "BaseStatsDescription": "Base stats, without Masterwork or Mods", "Button": "對比", "ButtonHelp": "對比物品", "CompareBaseStats": "顯示基礎屬性", "CurrentStats": "Current Stats", "CurrentStatsDescription": "Current stats, including Mods and Masterwork level", "Error": { "Invalid": "沒有可以比較的物品。", "Unmatched": "此物品和要比較的物品類別不同。" }, "InitialItem": "比較工具從這個物品啟動", "IsVendorItem": "這個物品不在你的物品欄裏,但是在 {{vendorName}} 処出售。", "NoModArmor": "Pre-mods" }, "Cooldown": { "Grenade": "手榴彈冷卻:{{cooldown}}", "Melee": "近戰冷卻:{{cooldown}}", "Super": "超能冷卻:{{cooldown}}" }, "Countdown": { "Days_compact_other": "{{count}}天", "Days_other": "{{count}} 天" }, "Csv": { "EmptyFile": "檔案中沒有行", "ImportConfirm": "您確定要從CSV導入標籤/筆記?這將覆蓋您試算表中所包含的所有項目/標記。", "ImportFailed": "從CSV導入標籤/注釋失敗:{{error}}", "ImportSuccess_other": "為{{count}} 個物品加載的標籤/注釋。", "ImportWrongFileType": "此檔案不是一個CSV檔案。", "WrongFields": "CSV檔案中必須有'Id','Notes','Tag','Hash'列(編號,注釋,標籤,雜湊)" }, "Dialog": { "Cancel": "取消", "OK": "确定" }, "EnergyMeter": { "Energy": "能量", "Unused": "未使用", "UpgradeNeeded": "此物品當前的容量為 {{energyCapacity}}。需要 {{energyUsed}} 容量才能裝下所選的模組。", "Used": "已使用" }, "ErrorBoundary": { "Title": "發生了一些錯誤" }, "ErrorPanel": { "BrowserTooOld": "Your browser is too old to use DIM. Please update your browser to the latest version.", "BrowserTooOldTitle": "Incompatible browser", "Description": "嘗試使用命運2同伴App來載入你的物品欄以測試Bungie.net是否出現問題。", "ReadTheGuide": "閲讀我們的用戶指南(連接在菜單)以瞭解故障排除步驟。", "SystemDown": "這一問題影響所有的命運應用程序,DIM團隊也無法修復或繞過。", "Troubleshooting": "故障排除指南" }, "FarmingMode": { "D2Desc_female_other": "為防止有物品被寄送到郵政官處,DIM 正確保 {{store}} 上的每個物品種類都留有 {{count}} 個空位。", "D2Desc_male_other": "為防止有物品被寄送到郵政官處,DIM 正確保 {{store}} 上的每個物品種類都留有 {{count}} 個空位。", "D2Desc_other": "為防止有物品被寄送到郵政官處,DIM 正確保 {{store}} 上的每個物品種類都留有 {{count}} 個空位。", "Desc_female_other": "為防止有物品被寄送到郵政官處,DIM 正將 {{store}} 上的記憶水晶和微光物品移動到保險庫,並確保每個物品種類都留有 {{count}} 個空位。", "Desc_male_other": "為防止有物品被寄送到郵政官處,DIM 正將 {{store}} 上的記憶水晶和微光物品移動到保險庫,並確保每個物品種類都留有 {{count}} 個空位。", "Desc_other": "為防止有物品被寄送到郵政官處,DIM 正將 {{store}} 上的記憶水晶和微光物品移動到保險庫,並確保每個物品種類都留有 {{count}} 個空位。", "FarmingMode": "收集模式", "FarmingModeNote": "(為掉落物留出空間)", "MakeRoom": { "Desc": "DIM將從{{store}} 上移動記憶水晶與微光幣物品到保險庫或其它角色上,以防止有物品被寄送到郵政官處。", "Desc_female": "DIM將從{{store}} 上移動記憶水晶與微光幣物品到保險庫或其它角色上,以防止有物品被寄送到郵政官處。", "Desc_male": "DIM將從{{store}} 上移動記憶水晶與微光幣物品到保險庫或其它角色上,以防止有物品被寄送到郵政官處。", "MakeRoom": "移動裝備騰出空間以便拾取物品", "Tooltip": "如果勾選,DIM將在保險庫中移動武器和防具來給記憶水晶騰出空間。" }, "OutOfRoom": "你的空間不足,無法再為{{character}}騰出空間了。該清理垃圾裝備了!", "OutOfRoomTitle": "空間不足", "Stop": "停止", "Vault": "會將物品移到保險庫來騰出空間。" }, "FashionDrawer": { "Accept": "保存外貌", "CannotFitOrnament": "此物品沒有裝飾品插槽,或你沒有此物品的裝飾品。", "CannotFitShader": "此物品無法使用著色器", "ClearOrnaments": "清除皮膚", "ClearOrnamentsTitle": "將所有皮膚重置為“無偏好”", "ClearShaders": "清除著色器", "ClearShadersTitle": "將所有著色器重置為“無偏好”", "NoPreference": "無偏好-不會更改此插槽", "Reset": "清除外貌", "Sync": "同步", "SyncOrnaments": "同步皮膚", "SyncOrnamentsTitle": "如整套皮膚已解鎖,則應用到所有物品", "SyncShaders": "同步著色器", "SyncShadersTitle": "為所有物品應用同一著色器", "Title": "選擇著色器和皮膚", "UseEquipped": "使用已裝備的外貌" }, "FileUpload": { "Instructions": "選擇或拖拽檔案" }, "Filter": { "Adept": "(精通)", "AmmoType": "按照彈藥類型顯示物品", "Armor": "顯示護甲物品。", "Armor3": "Shows items that use the Armor 3.0 stat system introduced in Edge of Fate.", "ArmorCategory": "按照類別顯示防具.", "ArmorIntrinsic": "顯示帶有固有特性的傳説防具,例如詭計護甲。", "Artifice": "Shows Artifice armor.", "Ascended": "顯示可提升光等且已提升的物品。", "Breaker": "Filter by breaker type or corresponding champion type. breaker:instrinsic shows items with intrinsic breaker ability.", "BulkClear_other": "移除 {{count}} 物品標籤", "BulkRevert_other": "恢復 {{count}} 件物品標籤", "BulkTag_other": "將 {{count}} 件選擇物品標記為 {{tag}}.", "Catalyst": "按狀態顯示催化。catalyst:complete 顯示已解鎖并已套用的催化, catalyst:incomplete 顯示已解鎖但尚未完成目標或套用的催化,而 catalyst:missing 顯示你尚未解鎖催化的物品。", "Class": "按照職業類別顯示.", "Combine": "可以用括號來組合過濾器,或用「or」和「and」來進一步篩選你的搜索,例如「{{example}}」。", "ContributePower": "顯示擁有光等的物品.", "Cosmetic": "顯示飾品或裝飾物品。", "Craftable": "顯示可製作的物品。", "CraftedDupe": "顯示包括至少一件鍛造武器在内的重複武器。", "Curated": "顯示擁有指定特性的物品。", "CurrentClass": "顯示當前登錄的守護者可裝備的物品。", "CustomStatLower": "Shows armor whose stats are strictly lower than another of the same type of armor, only taking into account stats that are in any of that class' custom stat total list.", "DamageType": "按照傷害類型顯示.", "Deepsight": "Shows weapons with Deepsight Resonance, which can have their pattern extracted, or which can have Deepsight Resonance enabled using a Deepsight Harmonizer.", "Deprecated": "This filter is no longer supported.", "Description": "描述", "DescriptionFilter": "Shows items whose description has a partial match to the filter text. Search for entire phrases using quotes.", "DisabledModSlot": "Shows items with a disabled mod.", "Dupe": "顯示重複的物品,包含不同賽季版本", "DupeArchetype": "Groups armor with the same stat Archetype.", "DupeCount": "擁有指定重複數量的物品。", "DupeLower": "非最高光等的重複和再版物品。重複物品中只有一件會被視為最高光等,其餘均視為較低光等。", "DupePerks": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "DupeSetBonus": "Groups armor with the same set bonus.", "DupeStats": "Shows armor with identical base stats, and matching stat adjustment mods like Artifice or Tuners.", "DupeTertiary": "Groups armor with the same tertiary stat.", "DupeTraits": "Weapons whose traits are either a duplicate of, or a subset of, another weapon of the same type.", "DupeTunedStat": "Groups armor with the same Tuned stat.", "DupeUntunedStats": "Groups armor with identical base stats, ignoring stat adjustment mods.", "DupeZeroStats": "Groups armor with the same 3 non-zero base stats.", "Energy": "Shows items that use the Armor 2.0 mod system introduced in Shadowkeep.", "EnergyCapacity": "Shows items based on their current energy capacity.", "Engrams": "顯示記憶水晶.", "Enhanceable": "Shows weapons that can be enhanced.", "Enhanced": "Shows weapons based on their enhancement tier.", "EnhancedPerk": "Shows weapons that have the specified number of enhanced perk columns.", "EnhancementReady": "Shows weapons that have reached level thresholds for perk enhancement.", "Equipment": "可以裝備的物品.", "Equipped": "當前已裝備的物品.", "Event": "顯示命運2中特定活動中的物品", "ExtraPerk": "顯示帶有額外可選特性的隨即特性傳説武器。", "Featured": "Items that count as one of the \"New Gear\" or \"Featured Items\" in the current season.", "Filter": "篩檢程式", "FilterWith": "篩選:", "Focusable": "顯示可以在商販處聚焦的物品", "Foundry": "按武器製造商顯示物品。", "Glimmer": "顯示與增加微光獲取有關的消耗品。", "Harrowed": "\\(痛苦\\)", "HasNotes": "顯示已有備註的物品", "HasOrnament": "顯示已應用皮膚的物品", "HasShader": "顯示已應用著色器的物品", "Holofoil": "Shows holofoil weapons.", "InDimLoadout": "is:indimloadout shows items that are included in any DIM loadout.", "InInGameLoadout": "is:iningameloadout 顯示任何游戲内配裝包含的物品。", "InInventory": "Shows items that you have at least one copy of in your inventory. Only really useful in the Vendors and Records screens.", "InLoadout": "is:inloadout shows items that are included in any loadout. Searching with inloadout: shows items that are included in loadouts with matching titles. When used with a hashtag, inloadout: shows items whose loadouts have the hashtag in the title or notes. When used with a range, it shows items that are in that many loadouts.", "Infusable": "顯示可以灌注的物品.", "InfusionFodder": "顯示可用於灌注同類物品且只消耗微光的物品。", "IsAdept": "顯示可安裝專家模組的武器。", "IsCrafted": "顯示製作獲取的武器。", "ItemHash": "顯示具有給定物品 hash 的物品。此為高級用戶選項。", "ItemId": "顯示具有給定物品 ID 的物品。此為高級用戶選項。", "Leveling": { "Complete": "{{term}}-顯示完全完成的物品-每次升級解鎖.", "Incomplete": "{{term}}-顯示尚未完成的物品-至少升一級才能解鎖.", "NeedsXP": "{{term}} - 顯示仍能獲得經驗值的物品.", "Upgraded": "{{term}}-顯示具有足夠 XP 解鎖其所有節點的項目, 但並非所有節點都已解鎖.", "XPComplete": "{{term}}-顯示無法將XP放入其中的項目(無論其陞級是否已解鎖)." }, "Location": "根據物品在App中的位置排列。左/中/右顯示您的角色資訊,您的角色會顯示在最左邊,中間和右邊是否會顯示角色取決於您一共創建了幾位角色。現在顯示的您已經登陸的角色(有黃色三角的那個)。", "LockAllFailed": "鎖定道具失敗", "LockAllSuccess": "已經鎖定 {{num}} 件道具", "Locked": "根據物品鎖定狀態顯示.", "Masterwork": "按大師傑作屬性或等級顯示物品。", "MasterworkKills": "按大師傑作擊殺追蹤器顯示物品。", "MaxPower": "顯示每個槽位最高光等的物品。", "MaxPowerLoadout": "顯示每個職業中能最大化光等的配裝物品。", "Memento": "Shows weapons that have a memento socket.", "ModSlot": "Shows armor with a specific mod type slot.", "Mods": { "Y3": "Shows items with any mods applied." }, "Name": "Shows items whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.", "NamedStat": "顯示重要防具.", "Negate": "要否定搜索,為搜索添加一個減號或單詞「not」前綴,例如「{{notexample}}」或「{{notexample2}}」。", "NewItems": "顯示新獲得的物品.", "Notes": "搜尋已添加了自訂備註的物品", "OriginTrait": "Shows weapons that have an origin trait perk.", "Ornament": "顯示帶有裝飾物和篩檢程式的物品.", "PartialMatch": "顯示名稱/描述/特性/模組與篩選器部分匹配的物品。使用引號來搜索包含空格的文本。", "PatternUnlocked": "顯示製作樣式已解鎖的武器,即使該武器并非製造而來。", "Perk": "顯示帶有特性或模組的名稱或描述與篩選器部分匹配的物品。使用引號來搜索帶有空格的文本。", "PerkName": "Shows items with a perk or mod whose name matches (exactperk:) or partially matches (perkname:) the filter text. Search for entire phrases using quotes.", "PinnacleReward": "顯示提供巔峰獎勵的追獵目標。", "Postmaster": "目前在郵政官處的物品.", "PowerKeywords": "使用 pinnaclecap 或 softcap 關鍵字而不是數字來表達當前賽季的光等限制。", "PowerLevel": "按能量等級顯示物品。$t(Filter.PowerKeywords)", "PowerfulReward": "顯示提供強力獎勵的追獵目標。", "PrismaticDamageType": "Shows items based on if they are a light or darkness damage type. Light types are arc, solar, and void. Darkness types are stasis and strand.", "Quality": "根據其統計品質百分比來顯示物品'{{percentage}}'別名'{{quality}}'.", "RandomRoll": "顯示可以隨機特性掉落的物品", "RarityTier": "按照稀有度顯示.", "Reforgeable": "顯示可以在槍匠那裡重新鍛造的物品.", "Release": "Shows items available from a specific release or event.", "RequiredLevel": "根據需要的等級顯示物品.", "RetiredPerk": "显示带有已无法获取的特性的武器。", "SearchPrompt": "搜索可用的過濾器命令", "Season": "按照物品推出季度顯示.", "StackFull": "顯示堆疊已滿的物品 (强化覈心、奇异硬幣、槍匠資料等)", "StackLevel": "根據堆疊數量來顯示物品.", "Stackable": "顯示可以堆疊的物品(合成彈藥,奇怪硬幣等)", "StatLower": "顯示相比其他同類別護甲而言屬性全面更低的護甲。", "Stats": "按某個屬性值顯示物品。$t(Filter.StatsExtras)", "StatsBase": "按基礎屬性值篩選護甲,不包含附加模組或大師傑作内容。$t(Filter.StatsExtras)", "StatsExtras": "Supports stat addition by connecting multiple stat names with the + or & symbol. There are also special keywords highest, secondhighest, thirdhighest, etc. which match stats based on their rank within an item's stats. Each custom stats also has its own search term, shown in the Custom Stats settings.", "StatsLoadout": "針對特定屬性, 篩選出最大化數值的一系列裝備", "StatsMax": "針對特定屬性, 篩選出最大化數值的一系列裝備. 包括那些最高屬性值物品", "StatsOrdinal": "Finds armor 3.0 with the specified stat focusing.", "Tags": { "Tag": "顯示具有特定標籤的物品。", "Tagged": "顯示具有任何標籤的物品。" }, "Tier": "Shows items based on their tier from 0-5.", "Timelost": "(失時)", "Tracked": "根據追蹤狀態顯示任務/賞金.", "Transferable": "可在角色間移動的物品.", "Trashlist": "顯示你願望單中廢棄列表物品", "TunedStat": "Shows items with tuning mods for the specified stat.", "Unascended": "顯示可提升光等但尚未提升的物品。", "Undo": "撤銷", "UnlockAllFailed": "鎖定物品失敗", "UnlockAllSuccess": "已經解鎖{{num}} 物品", "Vendor": "物品可從特定的商販獲得。", "VendorItem": "Item is from a vendor, not in your inventory. Useful for excluding vendor items from Loadout Optimizer.", "Weapon": "顯示武器物品。", "WeaponLevel": "按武器等級顯示武器。", "WeaponType": "按照武器類型顯示.", "Wishlist": "顯示你願望單中的物品", "WishlistDupe": "顯示有至少一個複製品在你願望單中的物品堆疊", "WishlistEnabled": "Shows items that are eligible to have wish list rolls.", "WishlistNotes": "顯示與搜索相匹配的願望清單物品", "WishlistUnknown": "在加載的願望清單中顯示沒有特性推薦的項目。", "Year": "按照物品推出年份顯示." }, "General": { "ClickForDetails": "點擊查看詳情", "Close": "關閉", "Confirm": "確認?", "UserGuideLink": "使用說明" }, "Glyphs": { "Axe": "Axe", "DarkAbility": "Darkness Ability", "Gilded": "镀金", "Harmonic": "Harmonic", "HiveSword": "Hive Sword", "LightAbility": "Light Ability", "LightLevel": "Light Level", "Misadventure": "意外致死", "Missing": "缺失", "OpenSymbolsPicker": "打開符號選單", "Prismatic": "Prismatic", "Quickfall": "急坠", "RespawnRestricted": "Respawn Restricted", "ScorchCannon": "灼燒加農炮", "SearchSymbols": "搜索符號...", "Smoke": "烟霧" }, "Header": { "About": "關於DIM", "AutoRefresh": "游玩時,DIM 會自動刷新。", "BulkTag": "批量標記物品", "BungieNetAlert": "Bungie提示您", "Clear": "清除蒐索篩選器", "CompareMatching": "對比物品", "DeleteSearch": "刪除搜索", "FilterHelp": "搜索項目/特性,{{example}} 等", "FilterHelpBrief": "搜索物品", "FilterHelpLoadouts": "蒐索配裝名稱和備註", "FilterHelpMenuItem": "過濾器幫助...", "FilterHelpOptimizer": "Filter armor included in builds, e.g.: {{example}}", "FilterHelpProgress": "搜索里程碑和懸賞", "FilterHelpRecords": "搜索成就和收藏品", "FilterMatchCount_other": "{{count}} 個物品", "Filters": "篩檢程式", "InstallDIM": "安裝為 App", "InstallDIMBanner": "將 DIM 安裝為主屏幕上的 App", "Inventory": "背包", "IosPwaPrompt": "在 Safari 中,輕觸分享圖標(位於屏幕底部中央),並選擇「添加到主屏幕」。", "KeyboardShortcuts": "鍵盤快捷鍵", "LaunchDIMAlone": "獨立視窗", "MaterialCounts": "材料數量", "Menu": "菜單", "ProfileAge": "數據由《命運》伺服器最後更新於 {{age}} 之前。\n在 DIM 中刷新可能會獲得最新數據,但 Bungie.net 也可能會重複發送緩存的數據。", "Refresh": "刷新命運數據 [R]", "ReloadApp": "重新載入應用", "ReportBug": "報告錯誤", "SaveSearch": "保存搜索", "SearchActions": "Open Search Actions", "SearchResults": "顯示物品", "Shop": "商店", "TagAs": "標記為{{tag}}", "UpgradeDIM": "更新DIM", "WhatsNew": "新增內容" }, "Help": { "CannotMove": "無法從該角色中移出物品.", "NoStorage": "DIM無法存儲資料", "NoStorageMessage": "DIM can't store data in your browser. This can be caused by browsing in private or incognito mode, or when you have low disk space, or a browser bug. Try restarting your computer! You won't be able to log in to or use DIM until you fix this." }, "Hotkey": { "Armory": "顯示物品詳情", "CheatSheetTitle": "鍵盤快速鍵", "ClearDialog": "關閉對話", "ClearNewItems": "清除新物品提示", "Enter": "回車", "ItemPopupTab": "Switch item details tab", "LockUnlock": "鎖定或解鎖物品", "MarkItemAs": "將物品標記為 “{{tag}}”", "Menu": "切換菜單", "Note": "添加備注", "Pull": "為當前角色取回物品", "RefreshInventory": "重新載入背包", "ShowHotkeys": "顯示鍵盤快速鍵", "StartSearch": "開始搜尋", "StartSearchClear": "重新搜尋", "Tab": "TAB", "Vault": "將物品存入保險庫" }, "InGameLoadout": { "ClearSlot": "清空槽位 {{index}}", "Create": "創建配裝", "CreateTitle": "從當前已裝備創建游戲内配裝。", "CurrentlyEquipped": "當前裝備", "DeleteFailed": "刪除配裝失敗", "Deleted": "配裝已刪除", "DeletedBody": "已清除槽位 {{index}} 的游戲内配裝", "EditFailed": "更新配裝失敗", "EditIdentifiers": "編輯標識", "EditTitle": "編輯配裝名稱和圖標", "EquipNotReady": "游戲内裝備未就緒", "EquipReady": "游戲内裝備已就緒", "LoadoutDetails": "配裝詳情", "MatchingLoadouts": "匹配配裝:", "PrepareEquip": "準備配裝", "Replace": "替換配裝 {{index}}", "Save": "更新配裝", "SaveIdentifiers": "Update Identifiers", "SnapshotFailed": "為已裝備的配裝創建快照時失敗" }, "Infusion": { "Filter": "過濾物品", "InfuseSource": "選擇需要用 {{name}} 進行灌注的物品", "InfuseTarget": "選擇用於灌注 {{name}} 的物品", "InfusionMaterials": "灌注材料", "NoItems": "沒有可灌注的物品。", "NoTransfer": "移動灌注材料\n{{target}} 無法移動。", "SwitchDirection": "切換", "TransferItems": "轉移" }, "Inventory": { "ClickToExpand": "(點擊展開)", "MissingSilver": "Your Silver balance is only available while you are playing the game." }, "Item": { "SetBonus": { "NPiece_other": "{{count}} Piece" }, "ThumbsDown": "Thumbs Down", "ThumbsUp": "Thumbs Up" }, "ItemFeed": { "ClearFeed": "清空物品流", "Description": "物品流", "HideTagged": "隱藏已標記的物品", "NoNewItems": "沒有新物品", "ShowOlderItems": "顯示之前的物品" }, "ItemMove": { "Consolidate": "合併{{name}}", "Distributed": "拆分{{name}}\n{{name}} 已平均拆分於所有角色上.", "MovingItem": "轉移到保險櫃", "MovingItem_female": "轉移到{{target}}", "MovingItem_male": "轉移到{{target}}", "ToStore": "所有{{name}} 都已移動至您的 {{store}} 之中.", "ToVault": "所有 {{name}} 都已移動至您的保險庫之中." }, "ItemPicker": { "ChooseItem": "選擇一件物品:", "SearchPlaceholder": "搜索物品" }, "ItemService": { "BucketFull": { "Guardian": "您的{{store}} 上有太多 “{{itemtype}}” 的物品.", "Guardian_female": "您的{{store}} 上有太多 “{{itemtype}}” 的物品.", "Guardian_male": "您的{{store}} 上有太多 “{{itemtype}}” 的物品.", "Vault": "您的{{store}} 上有太多 “{{itemtype}}” 的物品." }, "Classified": "該物品已被分類並且現在無法被轉移.", "Classified2": "分類物品。Bungie尚未提供該物品的相關資訊。為該物品添加備註,以方便您使用搜尋篩選器找到它。", "Deequip": "無法找到另一個物品來卸下{{itemname}}", "ExoticError": "無法裝備“{{itemname}}”,因為在{{slot}} 欄位中無法裝備異域物品。({{error}})", "NotEnoughRoom": "我們無法在{{store}} 上移動物品, 來為{{itemname}} 騰出空間", "NotEnoughRoomGeneral": "沒有足够的空間來移動此物品。", "OnlyEquippedClassLevel": "該物品僅可以裝備在{{class}} 上,且至少大於{{level}} 級。", "OnlyEquippedLevel": "該物品僅可以裝備在至少{{level}} 級的角色上。", "PostmasterAlmostFull": "幾乎已滿!", "PostmasterFull": "滿了!", "PreviewVendor": "預覽 {{type}} 內容", "StackFull": "您已經達到 {{name}} 的堆疊上限", "StoreName": "{{genderRace}} {{className}} 角色" }, "KillType": { "ClassAbilities": "職業技能", "Finisher": "終結技", "Grenade": "手雷", "Melee": "近戰", "Precision": "精準", "Super": "超能" }, "LB": { "AddStack": "Add another copy of this mod", "AdvancedOptions": "進階選項", "ChooseAMod": "選擇你的模組", "ChooseASetBonus": "Choose your set bonuses", "ChooseAnExotic": "選擇你的異域裝備", "ClearLocked": "清除鎖定", "ContainsVendorItems": "該配裝包含商販物品.", "Current": "當前", "Equip": "裝備在{{character}}", "Exclude": "已排除的物品", "ExcludeHelp": "Shift + 按一下一個物品(或直接拖動)用以創建一個不包括特定裝備的套裝組.", "ExistingBuildStats": "Existing Build Stats", "ExistingBuildStatsNote": "Only showing builds with strictly higher stats.", "FilterSets": "組合篩選", "Help": { "And": "裝備了所有這些特性(Perk) 的防具將被使用(\"and\")", "ChangeNodes": "如要創建一個配裝,請改變智力,訓練和力量的比例,如圖所示.", "Discipline": "訓練可以加速手榴彈的充能時間", "DragAndDrop": "拖放一個物品到選擇框內以創建一個具有該特定裝甲的配裝", "Help": "需要幫助嗎?", "HigherTiers": "越高階越好", "Intellect": "智力會加速大招的充能時間", "Lock": "通過點擊對話方塊並選擇特性(Perk) 來鎖定一套額外(Perks)", "MultiPerk": "如果你想同時使用多種防具特性(Perk), 請按Shift +按一下所需的特性(Perk)", "NoPerk": "如果沒有顯示特性(Perk),則代表你未擁有帶有這個特性(Perk) 的防具", "Or": "將使用具有任何這些特性(Perk) 的防具(\"or\")", "ShiftClick": "Shift+按一下創建一項空白的套裝組", "StatsIncrease": "當一項物品的防禦等級增加時,該物品的(智力/訓練/力量)資料也會增加.", "Strength": "力量會加速近戰的充能時間", "Synergy": "尋找能增加當前武器載彈量的防具特性(Perk).", "Tier11Example": "4/5/2(11級)代表智力4、訓練5、力量2(4+5+2=11級)" }, "HideAllConfigs": "隱藏所有配置", "HideConfigs": "隱藏配置", "IncompatibleWithOptimizer": "此物品與優化器不兼容。請從收藏中重新獲取一個新版本。", "LB": "配裝優化器", "LightMode": { "HelpCurrent": "計算當前防禦等級.", "HelpScaled": "計算使所有角色都都擁有350防禦.", "LightMode": "光亮模式" }, "Loading": "裝備最佳套裝", "LockEquipped": "鎖定裝備", "LockPerk": "鎖定特性", "Locked": "鎖定的物品", "LockedHelp": "拖放物品到選擇框中用以創建一個套裝組.使用Shift +按一下可移除物品.", "Missing2": "缺少稀有,傳奇,或異域物品用來打造套裝!", "ProcessingMode": { "Fast": "快速", "Full": "完全", "HelpFast": "只看你最好的裝備.", "HelpFull": "查看更多裝備,但是需要更長時間。", "ProcessingMode": "處理模式" }, "RemoveStack": "Remove a copy of this mod", "Scaled": "縮放", "SearchAMod": "搜索模組或描述", "SearchASetBonus": "", "SearchAnExotic": "搜索異域裝備或描述", "SelectExotic": "選擇異域裝備", "SelectMods": "選擇模組", "SelectModsCount": "{{selected}}/{{maxSelectable}}", "SelectModsCountActivityMods": "{{selected}}/{{maxSelectable}} 個活動模組", "SelectSetBonus": "Select Set Bonuses", "SelectSubclassOptions": "自定義子職業", "ShowAllConfigs": "顯示所有配置", "ShowConfigs": "顯示配置", "ShowGear": "{{class}} 防具", "Vendor": "包括商販物品" }, "Loading": { "Accounts": "載入帳號中...", "Code": "正在加載 DIM 代碼...", "FilterHelp": "正在加載搜索幫助...", "Profile": "正在加載命運檔案...", "Vendors": "正在加載命運商人..." }, "LoadoutAnalysis": { "Analyzed": "Analyzed {{numLoadouts}} Loadouts", "Analyzing": "Analyzing {{numAnalyzed}}/{{numLoadouts}} Loadouts", "BetterStatsAvailable": { "Description": "Choosing different armor or mods for this loadout will allow reaching higher stats. Choose \"$t(Loadouts.OpenInOptimizer)\" to view better builds.", "Name": "Better Stats Available" }, "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat to exceed 200. DIM may identify better stats by reducing the amount of excess stats. If this is undesired, disable \"$t(Loadouts.IncludeRuntimeStatBenefits)\" in the Loadout.", "DoesNotRespectExotic": { "Description": "This loadout's Loadout Optimizer settings specify an exotic choice, but the loadout does not match that exotic.", "Name": "Wrong Exotic" }, "DoesNotSatisfyStatConstraints": { "Description": "Loadout Optimizer settings for this Loadout specify stat minimums, but the Loadout does not reach them.", "Name": "Wrong Stat Minimums" }, "EmptyFragmentSlots": { "Description": "There are empty fragment slots in this subclass.", "Name": "空碎片插槽" }, "InvalidMods": { "Description": "Some mods in this loadout are deprecated or do not otherwise fit into any of your armor pieces.", "Name": "荒廢的模組" }, "InvalidSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer that is not valid.", "Name": "Invalid Search Query" }, "ItemsDoNotMatchSearchQuery": { "Description": "This loadout was created with a search query in Loadout Optimizer, and that search query excludes at least one of the items in the loadout.", "Name": "Search Excludes Items" }, "MissingItems": { "Description": "此配裝中的某些項目不再在您的庫存中。", "Name": "遺失物品" }, "ModsDontFit": { "Description": "Armor in this loadout cannot accommodate all loadout mods, even if the armor was upgraded.", "Name": "未分配的模組" }, "NeedsArmorUpgrades": { "Description": "Armor in this loadout needs to be upgraded to accommodate all mods or reach specified stats.", "Name": "Needs Armor Upgrades" }, "NotAFullArmorSet": { "Description": "This loadout could not be analyzed further because it does not include a full set of armor.", "Name": "Not A Full Armor Set" }, "TooManyFragments": { "Description": "There are more fragments configured on the subclass than granted by aspects.", "Name": "碎片過多" }, "UsesSeasonalMods": { "Description": "This loadout relies on mods that are only available in some seasons. When the season ends, some mods will be unavailable or exceed armor energy capacity.", "Name": "Uses Seasonal Mods" } }, "LoadoutBuilder": { "All": "全部", "AlwaysAutoMods": "Artifice and Tuning mods will always be chosen automatically.", "AnyExotic": "任何異域獎勵", "AnyExoticDescription": "組合里必須有任意異域裝備。", "Artifice": "Artifice", "AssumeMasterwork": "假定大師傑作", "AssumeMasterworkOptions": { "All": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)", "AllWithArtificeExotic": "All Armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nArmor 2.0 Exotics: $t(LoadoutBuilder.AssumeMasterworkOptions.ArtificeExotic)", "ArtificeExotic": "Enhanced to accept Artifice stat mods.", "Current": "Current stats, assumed energy level at least {{minLoItemEnergy}}.", "Legendary": "Legendary: $t(LoadoutBuilder.AssumeMasterworkOptions.Masterworked)\nExotic: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)", "Masterworked": "Full masterwork stat bonuses, assumed energy level at least 10.", "None": "All armor: $t(LoadoutBuilder.AssumeMasterworkOptions.Current)" }, "AutoStatMods": "自動添加屬性模組", "AutomaticallyPicked": "此模組已被自動添加以優化配裝屬性。", "CompareLoadout": "對比配裝", "ConfirmOverwrite": "你確定要用這套新的護甲來替換配裝「{{name}}」里的護甲嗎?", "DecreaseStatPriority": "Decrease stat priority", "DisabledByAutoStatMods": "屬性模組由配裝優化器自動選取。", "DisabledDueToMaintenance": "由於 Bungie API 維護,配裝優化器目前不可用。", "EquipItems": "裝備", "ExcludeItem": "排除項目", "ExcludeVendors": "Search \"not:vendor\" to exclude vendor items from Loadout Optimizer.", "ExcludedItems": "已排除的物品", "ExistingLoadout": "已有配裝", "Exotic": "Exotic Armor", "ExoticClassItemPerks": "If you want specific perks, use searches like exactperk:\"spirit of verity\". Click perks in the Optimizer results to add or remove them from the item filter.", "ExoticSpecialCategory": "特殊", "FOTLWildcardWarning": "This set contains a Festival of the Lost mask. Manually apply the correct mod to activate desired set bonuses.", "Filter": "設定", "IgnoreStat": "If unchecked, Loadout Optimizer will pretend this stat doesn't exist when building sets", "IncreaseStatPriority": "Increase stat priority", "Legendary": "传说", "LimitToNewFeaturedGear": "Limit to new/featured gear", "LockItem": "固定物品", "MissingClass": "配裝適用於:{{className}}", "MissingClassDescription": "你正試圖查看的配裝適用於一個你未擁有的角色職業。", "MwExotic": "Exotic", "NoBuildsFoundExplainer": { "ActiveSearchQuery": "搜索條件導致 DIM 無法將部分物品包含在配裝中", "AllowAutoStatMods": "允許 DIM 自動添加額外的屬性模組", "AlwaysInvalidMods": "這些模組無法裝備于你擁有的任何物品上:", "AssumeMasterworked": "允許DIM建議防禦大師之作", "AssumptionsRestricted": "DIM 無法建議更改防具能量:", "BadSlot": "在 {{bucketName}} 槽位,沒有合適的物品可以容納以下模組:", "ExoticDoesNotExist": "You don't have any of the selected exotic armor in your inventory.", "Header": "找不到配裝,以下是 DIM 無法找到配裝的可能原因:", "LowerBoundsFailed": "Many sets did not meet minimum stat requirements", "MaybeAllowMoreItems": "允許嘗試其它物品:", "MaybeDecreaseLowerBounds": "Consider reducing minimum stat requirements", "MaybeRemoveMods": "嘗試移除一些模組:", "MaybeRemoveSearchQuery": "嘗試清除或修改過濾器條件", "ModAssignmentFailed": "多個裝配無法容納所請求的模組", "RemoveMods": "移除這些模組", "RemoveSetBonuses": "Consider removing some set bonuses", "SetBonuses": "You have chosen some set bonuses, maybe you don't have the right items to use them." }, "NoExotic": "非異域", "NoExoticDescription": "Equivalent to searching \"not:exotic\" in the search bar - sets will not use any exotic armor.", "NoExoticPreference": "No Exotic Selected", "NoExoticPreferenceDescription": "Exotic armor will be used if it maximizes stats.", "NoLoadoutsToCompare": "沒有要對比的配裝", "None": "無", "OptimizerExplanationGuide": "用戶指南里有更多信息和視頻教程。", "OptimizerExplanationMods": "Choose an exotic, mods, and a subclass. These will contribute stats to the build, while any mods already on the armor are ignored.", "OptimizerExplanationSearch": "Use the search bar to narrow down which armor to consider, e.g. {{example}}. If no armor in a slot matches the search, all items will be considered for that slot.", "OptimizerExplanationStats": "Drag the most important stats to the top, and uncheck stats you don't want to maximize.", "OptimizerSet": "優化器組合", "PinnedItems": "Pinned Items", "PinnedItemsFinePrint": "Search filters are saved with Loadout Optimizer settings, but pinned and excluded items are not. Pins and exclusions will be ignored when DIM checks existing Loadouts for better stat builds.", "ProcessingSets": "Finding highest stat sets...", "SaveAs": "保存為", "SetBonus": "Set Bonuses", "SpeedReport": "Evaluated {{combos, number}} combinations in {{time}} seconds using {{cpus}} CPU cores.", "StatConstraints": "Stat Priorities & Ranges", "StatMax": "Max", "StatMin": "Min", "StatRangeTooltip": "With the current min/max setting, loadouts exist which have {{min}} to {{max}} points in this stat. Double-click to set min to {{max}}.", "StatTotal": "Total: {{total}}", "TierNumber": "T{{tier}}", "UnableToAddAllMods": "無法添加所有模組。", "UnableToAddAllModsBody": "模組插槽不足,無法裝下 {{mods}}。", "UnlockItem": "取消固定物品" }, "LoadoutFilter": { "Contains": "Shows loadouts which have an item or a mod matching the filter text. Search for items with spaces in their name using quotes.", "FashionOnly": "Shows loadouts that contain only fashion (shaders or ornaments).", "LoadoutLight": "Shows loadouts based on their calculated light level. Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.", "ModsOnly": "Shows loadouts that only contain armor mods.", "Name": "Shows loadouts whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.", "Notes": "Search for loadouts by their notes field.", "PartialMatch": "Shows loadouts where their name or notes has a partial match to the filter text. Search for entire phrases using quotes.", "Season": "Shows loadouts by which season of Destiny 2 they were last modified in.", "Subclass": "Shows loadouts whose subclass name or damage type partially matches the filter text." }, "Loadouts": { "Abilities": "技能", "Actions": "Actions for {{title}}", "AddEquippedItems": "添加已裝備", "AddNotes": "添加備註", "AddUnequippedItems": "添加未裝備的", "Any": "全部職業", "Apply": "應用", "ApplyInGameLoadoutInGame": "你的配裝已就緒,但由於你正在進行活動,你需要在游戲内裝備此配裝", "ApplyMods": "正在應用模組", "ApplySearch": "轉移搜尋 \"{{query}}\"", "ArmorStats": "防具屬性", "ArtifactUnlocks": "神器解鎖", "ArtifactUnlocksDesc": "由於 Bungie.net 的限制,DIM無法自動配置你的神器模組。你需要在游戲中進行解鎖,然後再應用到配裝。", "ArtifactUnlocksWithSeason": "神器解鎖 - {{seasonNumber}} 賽季", "BadLoadoutShare": "無法加載共享配装", "BadLoadoutShareBody": "您嘗試加載的配装無效: {{error}}", "Before": "撤銷 '{{name}}'", "CancelEditing": "取消編輯", "CannotCustomizeSubclass": "無法配置此子職業", "ChooseItem": "添加 {{name}}", "ClassType": "Any class loadout", "ClassTypeMismatch": "無法將 {{className}} 項添加到此配裝中", "ClassTypeMissing": "您沒有 {{className}} 來為其創建配裝", "ClassType_female": "{{className}} loadout", "ClassType_male": "{{className}} loadout", "Classified": "你的某些物品是隱藏的,因此不能用於到最大光等的計算之中。", "ClearLoadoutParameters": "刪除配裝優化器設置", "ClearSection": "移除所有", "ClearSpace": "移除其他物品", "ClearSpaceArmor": "移除其他護甲", "ClearSpaceWeapons": "移除其他武器", "ClearUnsetMods": "移除其它模組", "ClearingSpace": "正在移走其他物品", "CopyAndEdit": "Edit Copy", "Create": "創建配裝", "CurrentlyEquipped": "當前裝備", "Deequip": "從其他角色取回物品", "Delete": "刪除", "DimLoadouts": "DIM 配裝", "Edit": "編輯配裝", "EditBrief": "編輯", "EquipInGameLoadout": "正在裝備游戲内配裝", "EquipItems": "正在裝備物品", "EquippableDifferent1": "計算你的最大能量等級時使用了多件異域裝備,所以在遊戲內配裝時可能達不到這個數值。", "EquippableDifferent2": "在計算你的掉落、強大、巔峰裝備力量等級時,最大力量等級不受「一件異域裝備」規則的限制。", "Failed": "配装未能完全應用", "Fashion": "選擇外觀", "FashionOnly": "Fashion-only", "FillFromEquipped": "填入已裝備的物品", "FillFromInventory": "填入未裝備物品", "FilteredItems": "已篩選的物品", "FindAnother": "查找另一個 {{name}}", "FromEquipped": "當前裝備", "Generated": "{{statTotal}} Stat Point Loadout", "HashtagTip": "提示:在配裝名稱或備註中使用的#標籤會顯示在這裡.", "Import": { "BadURL": "不是有效的配裝分享連結。", "Error": "獲取配裝失敗:", "Error404": "配裝不存在。", "PasteHere": "粘貼一個配裝連結以打開配裝。" }, "ImportLoadout": "導入配裝", "InGameActions": "In-Game Loadout Actions", "InGameLoadouts": "游戲内配裝", "IncludeRuntimeStatBenefits": "Include Font mod stats", "IncludeRuntimeStatBenefitsDesc": "\"Font of ...\" armor mods provide a flat boost to character stats while you have Armor Charges.\n\nWith this setting, DIM considers these mods active and adds their benefits to this Loadout's stats in calculations and optimizations.", "ItemErrorSummary_other": "{{count}} 個物品出錯:", "ItemLeveling": "物品等級", "LoadoutName": "配裝名稱", "LoadoutParameters": "配裝優化器設置", "LoadoutParametersExotic": "配裝必須包含以下異域裝備:{{exoticName}}", "LoadoutParametersQuery": "物品必須匹配該過濾條件", "LoadoutParametersStats": "Stat priorities and minimum/maximum stat ranges", "Loadouts": "配裝", "MakeRoom": "為郵政官騰出空間", "MakeRoomDone_female_other": "通過從{{store}} 上移動了{{movedNum}} 項物品,成功為郵政官騰出了{{count}} 個物品空間。", "MakeRoomDone_male_other": "通過從{{store}} 上移動了{{movedNum}} 項物品,成功為郵政官騰出了{{count}} 個物品空間。", "MakeRoomDone_other": "通過從{{store}} 上移動了{{movedNum}} 項物品,成功為郵政官騰出了{{count}} 個物品空間。", "MakeRoomError": "無法為郵政官騰出物品空間:{{error}}.", "ManageLoadouts": "管理配裝", "MaxSlots": "一個配裝中只能有 {{slots}} 個 {{bucketName}}", "MaximizeLight": "最高光等", "MaximizePower": "最大光能等級", "MaximizeStat": "最高屬性", "MissingItemsWarning": "此配裝中的某些項目不再在您的庫存中。", "ModErrorSummary_other": "{{count}} 個模組出錯:", "ModPlacement": { "InvalidMods": "無效模組", "InvalidModsDesc_other": "{{count}} 個模組不能插入任何防具。", "ModPlacement": "模組位置", "StackableMod": "Stackable", "UnassignedMods": "未分配的模組", "UnassignedModsDesc_other": "由於容量或插槽不足,{{count}} 個模組無法安裝。即使為所選護甲升級容量也是如此。", "UnstackableMod": "Not Stackable", "UpgradeCosts": "升級消耗", "UpgradeCostsDesc": "部分防具需要能量容量升級才能裝下所選模組。升級總共會花費:" }, "Mods": "模組", "ModsOnly": "Mods-only", "MoveItems": "正在移動物品", "NoSpace": "你的保險庫和其他角色均已滿。", "NoneMatch": "沒有與篩檢程式匹配的配裝.", "NotStarted": "正在等待其它操作完成,或物品欄刷新完畢", "NotesPlaceholder": "爲此配裝添加備注,或使用 #標簽 分類 (目前不支持中文)", "NotificationTitle": "配裝:{{name}}", "OnWrongCharacterAdvice": "點此查找此角色最高光等的物品。", "OnWrongCharacterWarning": "當前角色的光等最高的防具在其他角色的物品欄中,要使强大裝備和巔峰裝備掉落時的光等達到最高,這些防具必須存儲在當前角色的物品欄或是保險庫中。", "OnlyItems": "只有可裝備物品,材料和消耗品才可以載入.", "OpenInOptimizer": "防具優化", "OpenOnStreamDeck": "Open on Stream Deck", "PickArmor": "選擇防具", "PickMods": "添加防具模組", "Prismatic": { "Aspect": "Prismatic Aspect", "Grenade": "Prismatic Grenade", "Melee": "Prismatic Melee", "Super": "Super Ability" }, "PullFromPostmaster": "從郵政官處取回物品", "PullFromPostmasterError": "無法從郵政官處取回物品:{{error}}.", "PullFromPostmasterGeneralError": "無法從郵政官拉取所有物品。", "PullFromPostmasterNotification_female_other": "正在從郵政官處取回{{count}} 項物品至{{store}}.", "PullFromPostmasterNotification_male_other": "正在從郵政官處取回{{count}} 項物品至{{store}}.", "PullFromPostmasterNotification_other": "正在從郵政官處取回{{count}} 項物品至{{store}}.", "PullFromPostmasterPopupTitle": "從郵政官處取回物品", "Random": "隨機", "Randomize": "隨機配装", "RandomizeButton": "隨機搭配", "RandomizeNew": "隨機創建", "RandomizeQueryHint": "提示:可通過搜索條件來限制隨機物品池", "RandomizeSearch": "從搜索結果隨機選取", "RandomizeSearchPrompt": "從「{{query}}」的搜索結果隨機配裝?", "Redo": "重做", "RestoreAllItems": "所有物品", "SalvationsEdgeMods": "Salvation's Edge Mods", "Save": "保存", "SaveAsDIM": "另存爲DIM配裝", "SaveAsNew": "另存為", "SaveAsNewTooltip": "保留原始配裝並將其另存為新配装", "SaveDisabled": { "AlreadyExists": "為該配裝選擇一個新的名稱。", "Empty": "配裝為空。", "NoName": "配裝需要一個名稱。" }, "SaveLoadout": "保存配裝", "Season": "第 {{season}} 賽季", "SetBonusesDesc": "Required set bonuses", "Share": { "Copied": "將配装連結複製到剪貼板", "CopyButton": "複製連結", "Error": "獲取分享連結時出錯", "Fashion": "外觀 (著色器和裝飾品)", "LoadoutOptimizer": "配裝優化器設置", "NativeShare": "分享連結", "Notes": "備注", "NumItems_other": "{{count}} 個項目 - 將提示被分享者從他們的庫存中選擇可比較的項目", "NumMods_other": "{{count}} 個模組", "Placeholder": "正在載入分享連結", "Subclass": "自定義子職業", "Summary": "分享包含如下内容的配裝:", "Title": "分享“{{name}}”" }, "ShareLoadout": "分享", "ShowModPlacement": "顯示模組位置", "Snapshot": "保存爲游戲内配裝", "SocketOverrides": "正在更改子職業選項", "SortByEditTime": "按最後編輯時間排序", "SortByName": "按名稱排序", "SubclassOptions": "{{subclass}} 選項", "SubclassOptionsSearch": "搜索{{subclass}} 選項", "Succeeded": "配裝成功", "SyncFromEquipped": "同步已裝備物品", "TooManyRequested": "你有{{total}} 個{{itemname}}, 但完成配裝需要{{requested}} 個. 我們已經處理所有已擁有的", "TuningMods": "Tuning Mods", "UnassignedModError": "模組不適合你當前的護甲", "Undo": "撤銷", "Update": "保存更改", "UpdateLoadout": "更新配裝", "VendorsCannotEquip": "您沒有這些項目。點擊來選擇一個替換或點擊 X 來刪除:" }, "Manifest": { "Download": "正在從 Bungie 下載最新的命運信息數據庫...", "Error": "加載命運信息數據庫時出錯:\n{{error}}\n請刷新重試。", "Load": "正在加載命運信息數據庫..." }, "Milestone": { "Daily": "每日挑戰", "OneTime": "一次性挑戰", "SeasonalRank": "賽季等級 {{rank}}", "Special": "特殊事件挑戰", "Tutorial": "挑戰教程", "Unknown": "挑戰", "Weekly": "每週挑戰" }, "Mods": { "HarmonicModDescription": "This mod's effect comes at a reduced cost and changes element depending on the equipped subclass." }, "MoveAmount": { "Amount": "數量:" }, "MovePopup": { "Acquired": "此物品已经在收藏物中解锁", "AcquiredMod": "此模組已在收藏中解鎖。", "AddNote": "添加備注", "AddToLoadout": "配裝", "AddToLoadoutTitle": "添加此物品到配裝", "All": "全部", "ArtifactBreaker": "This weapon has {{breaker}} because of an unlocked artifact perk.", "CannotCurrentlyRoll": "此特性不會出現在此物品的當前版本上。", "CantPullFromPostmaster": "你必須去郵政官処取回此物品。", "CatalystProgress": "催化劑進度", "CommunityData": "社区描述", "Consolidate": "合併", "DistributeEvenly": "均勻分配", "EnhancementTier": "{{tier}} 階", "Equip": "裝備到:", "EquipWithName": "裝備在{{character}}", "FavoriteUnFavorite": { "Favorite": "收藏{{itemType}}", "Favorited": "已收藏", "Unfavorite": "取消收藏{{itemType}}", "Unfavorited": "已取消收藏" }, "Infuse": "灌注", "InfuseTitle": "打開灌注材料查找器", "IntrinsicBreaker": "This weapon intrinsically has {{breaker}}.", "LoadingSockets": "Perk and stat details have not loaded yet for this item.", "LockUnlock": { "AutoLock": "該物品的鎖定狀態已與其標籤同步", "Lock": "鎖定 {{itemType}}", "Locked": "已鎖定", "Unlock": "解鎖{{itemType}}", "Unlocked": "已解鎖" }, "MissingSockets": "当 bungie 正在更新其服务时, perk 和 mod 的详细信息不可用。当他们做完的时候, 它就会回来, 通常在几个小时后。", "Notes": "備註:", "OpenOnStreamDeck": "Open on Stream Deck", "OverviewTab": "概述", "Owned": "此物品在您的库存中。", "OwnedMod": "該模組在您的模組庫存中。", "PullItem": "從{{bucket}} 移到{{store}}", "PullPostmaster": "從郵政官處取回物品", "ReadLore": "在Ishtar Collective閱讀背景故事", "ReadLoreLink": "閱讀傳奇故事", "Rewards": "獎勵:", "SendToVault": "發送到保險庫", "Store": "移動到:", "StoreWithName": "移動到 {{character}}", "Subtitle": { "QuestProgress": "第 {{questStepNum}} / {{questStepsTotal}} 步", "Type": "{{classType}}{{typeName}}" }, "TabList": "Item detail tabs", "ToggleSidecar": "展開或摺疊物品操作", "TrackUntrack": { "Track": "跟蹤{{itemType}}", "Tracked": "已跟蹤", "Untrack": "取消跟蹤{{itemType}}", "Untracked": "未跟蹤" }, "TriageTab": "分類", "UnreliablePerkOption": "該特性僅出現在收藏品界面,隨機掉落的物品可能不會帶有該特性。", "Vault": "保險庫", "WeaponLevel": "武器等级{{level}}" }, "Notes": { "Error": "錯誤! 備註不能超過120個字元.", "Help": "添加備注、#標簽、和 :symbols:" }, "Notification": { "Cancel": "取消", "OK": "關閉" }, "Objectives": { "Complete": "已完成", "Incomplete": "未完成" }, "Organizer": { "BulkMove": "移動到", "BulkMoveLoadoutName": "已在管理器中選中", "BulkTag": "標籤", "Columns": { "Ammo": "Ammo", "Archetype": "原型", "BaseStats": "基礎數值", "Breaker": "對抗勇士", "Crafted": "鑄造日期", "CustomTotal": "自定義總和", "Damage": "傷害", "Energy": "能量", "Event": "事件", "Featured": "New Gear", "Foundry": "Foundry", "Frame": "Frame", "Harmonizable": "Harmonizable", "Holofoil": "Holofoil", "Icon": "圖示", "ItemTier": "Tier", "KillTracker": "Kills", "Level": "等級", "Loadouts": "配裝", "Location": "位置", "Locked": "已鎖定", "MasterworkStat": "MW Stat", "MasterworkTier": "MW Tier", "ModSlot": "模組槽位", "Mods": "模組", "Name": "名稱", "New": "新", "Notes": "備注", "OriginTraits": "Origin Trait", "OtherPerks": "Weapon Components", "PercentComplete": "% 已完成", "Perks": "特性", "PerksGrid": "Perks Grid", "Power": "光等", "Quality": "品質 %:", "Recency": "新舊", "Season": "賽季", "Shaders": "Cosmetics", "Source": "來源", "StatQuality": "屬性質量", "StatQualityStat": "{{stat}}%", "Stats": "數值", "Tag": "標籤", "TertiaryStat": "3rd Stat", "Tier": "Rarity", "Traits": "武器特性", "TuningStat": "Tuner", "WishList": "願望清單", "WishListNotes": "願望清單備註", "Year": "年" }, "EnabledColumns": "已啟用的列", "Lock": "鎖定", "NoItems": "No items match the filters. If you have a search query, try clearing it.", "NoMobile": "將手機橫放來使用管理器。", "Note": "添加備注", "OpenIn": "在管理器中顯示", "Organizer": "管理器", "SelectAll": "全選", "SelectItem": "選擇或取消選擇 {{name}}", "ShiftTip": "提示:按住 Shift 鍵並點擊單元格以篩選項目", "Stats": { "Aim": "瞄準", "Airborne": "空運效率", "AmmoGeneration": "Ammo Gen", "Power": "光等", "RPM": "彈夾數每分鐘", "Recoil": "後坐力", "Reload": "重新裝彈" }, "Unlock": "解鎖​​​​" }, "PostmasterWarningBanner": { "PostmasterAlmostFull": "郵政官快滿了!({{number}}/{{postmasterSize}})", "PostmasterFull": "郵政官全滿了!({{number}}/{{postmasterSize}})" }, "Progress": { "Bounties": "賞金", "CatalystSource": "Source: {{source}}", "CrucibleRank": "排名", "Items": "任務物品", "Milestones": "里程碑和挑戰", "NoEventChallenges": "你已經完成了所有活動挑戰", "NoTrackedTriumph": "You have no tracked triumphs. Track as many as you like in DIM.", "PaleHeartPathfinder": "Pale Heart Pathfinder", "PercentMax": "{{pct}}% to maximum", "PercentPrestige": "重置百分比{{pct}}%", "PointsUsed_other": "{{count}} points used", "PowerBonusHeader": "+{{powerBonus}} 光等獎勵", "PowerBonusHeaderUndefined": "其他獎勵", "Progress": "進度", "QueryFilteredTrackedTriumphs": "你正在追蹤的成就均不匹配此搜索", "QuestExpired": "已過期", "QuestExpires": "有效期至 ", "Quests": "任務", "Rank": "{{name}} {{rank}}", "RecordValue": "{{value}}點", "Resets_other": "{{count}} 次重置", "RewardPassEndsIn": "Reward Pass ends in ", "RewardPassPrestigeRank": "Prestige Rank {{rank}}", "SeasonalHub": "Seasonal Hub", "StatTrackers": "數據追蹤器", "TrackedTriumphs": "跟蹤的成就" }, "RecordBooks": { "HideCompleted": "隱藏完成事件", "RecordBooks": "記事本" }, "Records": { "Title": "記錄", "UniversalOrnamentSetOther": "其它" }, "SearchHistory": { "Date": "最近使用", "DeleteAll": "删除所有未加星標的搜索", "Description": "這些是你過去和保存的搜索。你可以在這裡刪除它們。", "Item": "Item Searches", "Link": "查看和編輯搜索歷史", "Loadout": "Loadout Searches", "Query": "搜索​​​​", "Title": "搜索歷史", "UsageCount": "使用次數" }, "Settings": { "Appearance": "Appearance", "ArmorArchetypeModslot": "Armor Archetype / Modslot", "AutoLockTagged": "將物品鎖定狀態與標記同步.", "AutoLockTaggedExplanation": "DIM 將自動鎖定和解鎖物品以匹配它們的標簽。製作獲得的武器將不會鎖定,以便你進行重塑。當啓用此設置時,鎖定圖標不會顯示在帶標簽的物品圖標上。", "BadgePostmaster": "在 App 圖標上顯示當前角色的郵政官物品數量", "BadgePostmasterExplanation": "若要此功能生效,你必須將 DIM 安裝為 App,且操作系統必須支持顯示標記", "BothDescriptions": "全部顯示", "BungieDescriptionOnly": "僅顯示來自Bungie的説明", "CharacterOrder": "排序方式", "CharacterOrderFixed": "角色年齡(PC平臺有錯誤)", "CharacterOrderRecent": "最近使用的角色", "CharacterOrderReversed": "最近使用的角色(倒序)", "ColumnSize": "{{num}} 個物品", "ColumnSizeAuto": "自動", "CommunityData": "社區特性見解", "CommunityDescriptionOnly": "仅显示来自社区的説明", "CsvImport": "導入 CSV", "CustomErrorLabel": "屬性名稱必須包含字符,并與其它同職業的屬性的名稱不同。", "CustomErrorValues": "屬性權重必須是正數。\n至少有兩個屬性權重必須大於零。", "CustomStatChooseName": "輸入一個自定義屬性名稱", "CustomStatCreate": "新建自定義屬性", "CustomStatDelete": "刪除該自定義屬性", "CustomStatDeleteConfirm": "刪除該自定義屬性?", "CustomStatDesc1": "选择所需的防具属性,以创建一个自定義的屬性縂和。", "CustomStatDesc3": "自定義屬性將會出現在物品彈出框、管理器以及物品對比界面。", "CustomStatTitle": "自定義属性總和", "Data": "試算表", "DefaultItemSizeNote": "50 px 的物品大小看起來最清晰, 而不會模糊物品圖片或文本。", "DontForgetDupes": "别忘了,你可以搜索 is:dupe 来快速找到重复的物品,也可以用对比工具或管理器来比较相关物品。", "EnableAdvancedStats": "在防具上顯示評分(D1)", "ExpandSingleCharacter": "顯示所有角色", "ExportLoadoutSS": "Loadout spreadsheets", "ExportLoadoutSSHelp": "Download a CSV list of your DIM Loadouts that can be easily viewed in the spreadsheet app of your choice.", "ExportProfile": "匯出API返回的帳戶資料", "ExportSS": "背包表格", "ExportSSHelp": "下載您物品的 CSV 列表, 可以在您選擇的試算表應用程式中輕鬆查看。", "HidePullFromPostmaster": "隱藏 “$t(Loadouts.PullFromPostmaster)” 按鈕", "Inventory": "物品欄顯示模式", "InventoryColumns": "物品欄顯示寬度", "InventoryColumnsMobile": "行動裝置物品欄顯示寬度", "InventoryColumnsMobileLine2": "將自動調整物品大小以適配當前設置", "InventoryNumberOfSpacesToClear": "Number of empty spaces to make when using Farming Mode", "Items": "物品顯示", "Language": "語言", "LogOut": "登出", "Masterworked": "大師之作鑄造完成", "MaxParallelCores": "Maximum cores for parallel tasks", "MaxParallelCoresExplanation": "Controls how many CPU cores DIM can use for intensive tasks like Loadout Optimizer and Loadout Analyzer. Higher values may improve performance but use more system resources.", "OrnamentDisplay": "Show Ornaments on item tiles", "OrnamentDisplayExplanationDisabled": "Items will never display their ornaments", "OrnamentDisplayExplanationEnabled": "Hovering or long-pressing armor will hide its ornament", "OrnamentDisplayExplanationHide": "Hovering or long-pressing an item will hide its ornament", "OrnamentDisplayExplanationShow": "Hovering or long-pressing an item will show its ornament", "ResetToDefault": "重設", "RestoreVaultSide": "Show vaulted items in their own column", "ReverseSort": "切換正向/反向排序", "SetSort": "物品排序方式:", "SetVaultWeaponGrouping": "Group vault weapons by:", "Settings": "設定", "ShowNewItems": "在新物品上顯示紅點", "SingleCharacter": "單角色視圖", "SingleCharacterExplanation": "DIM將只顯示最近一次遊玩的角色。\n如果隱藏的角色持有當前角色可用的物品,它們會被顯示在保險庫裏。\n其他職業獨有的物品會被完全隱藏。", "SizeItem": "物品顯示大小", "SortByAmmoType": "彈藥類型", "SortByAmount": "堆疊大小", "SortByClassType": "必要的類", "SortByCrafted": "已製造 (D2)", "SortByDeepsight": "深視共振 (D2)", "SortByFeatured": "New Gear / Featured (D2)", "SortByPrimary": "光等", "SortByRarity": "稀有度", "SortByRating": "護甲質量(D1)", "SortByRecent": "最近獲得(D2)", "SortBySeason": "賽季(D2)", "SortByTag": "標籤{{taglist}}", "SortByTier": "Tier (D2)", "SortByType": "類型", "SortByWeaponElement": "傷害類型", "SortCustom": "自訂排序", "SortName": "名稱", "SpacesSize_other": "{{count}} spaces", "Theme": "主題", "Troubleshooting": "Troubleshooting", "VaultArmorGroupingStyle": "Separate armor on different lines by class", "VaultGroupingNone": "無", "VaultUnder": "Show vaulted items under equipped items", "VaultWeaponGroupingStyle": "Separate weapon groups on different lines", "WeaponFrame": "Weapon Frame", "WishlistRefreshNotificationBody": "If you do not see any updates, be sure the source (such as GitHub) reflects them!", "WishlistRefreshNotificationTitle": "Wishlists Reloaded" }, "Sockets": { "ApplyPerks": "應用特性", "GridStyle": "按網格顯示特性", "Insert": { "Ability": "裝備技能", "Aspect": "插入星相", "Fragment": "插入碎片", "Mod": "插入模組", "Ornament": "應用皮膚", "Projection": "應用機靈投影", "Shader": "應用著色器", "Super": "裝備超能", "Transmat": "應用傳送效果" }, "ListStyle": "按列表顯示特性", "Search": "搜索名稱或說明", "Select": { "Ability": "預覽技能", "Aspect": "預覽星相", "Fragment": "預覽碎片", "Mod": "預覽模組", "Ornament": "預覽皮膚", "Projection": "預覽機靈投影", "Shader": "預覽著色器", "Super": "预览超能", "Transmat": "預覽傳送效果" }, "SelectWishlistPerks": "預覽願望單特性" }, "Stats": { "CrouchingSpeed": "蹲下", "Custom": "自定義總和", "CustomDesc": "所選基礎屬性的自定義總和,未計入模組或大師傑作。可在設置里配置自定義總和所包含的屬性。", "DamageResistance": "PvE 傷害減免", "Discipline": "訓練", "DropLevel": "Account Power", "DropLevelExplanation1": "Account Power is the base power level when calculating the increased level of rewards.", "DropLevelExplanation2": "Account Power uses the highest level item in each slot, regardless of required Class or the \"One Exotic\" rule.", "EquippableGear": "Equippable Gear", "FlinchResistance": "倒縮抗性", "HP": "生命值", "Intellect": "智力", "MaxGearPower": "當前裝備最大光等", "MaxGearPowerAll": "所有裝備最高光等", "MaxGearPowerOneExoticRule": "Maximum Power of equippable gear\n(only one Exotic armor piece equipped)", "MaxTotalPower": "最大光等", "MetersPerSecond": "米/秒", "Milliseconds": "毫秒", "NoBonus": "無加成", "NotApplicable": "不可用", "OfMaxRoll": "最大下拉範圍{{range}}", "PercentHelp": "點擊獲取更多關於品質屬性的資訊.", "Percentage": "%", "PowerModifier": "季節神器提升光等", "Prestige": "威望等級: {{level}}\n{{exp}}xp 于5明亮之塵之前.", "Quality": "品質", "ShieldHP": "Shield HP", "StrafingSpeed": "掃射", "Strength": "力量", "TierProgress": "T{{tier}} {{statName}} ({{progress}}/60 於 T{{nextTier}})", "TierProgress_Max": "T{{tier}} {{statName}} ({{progress}}/300)", "TimeToFullHP": "完全回復所需時間", "Total": "總計", "TotalHP": "總生命值", "WalkingSpeed": "步行", "WeaponPart": "Weapon Part" }, "Storage": { "ApiPermissionPrompt": { "Description": "DIM 現在可以將你的標籤、配裝和設置存儲在雲端,並將其同步給不同版本的 DIM,且無需另外創建賬號。如果你從未啟用過 DIM 同步,你可以在設置里導入現有數據。這個功能離不開我們 OpenCollective 支持者的幫助!", "No": "現在不行", "Title": "啟用 DIM 同步嗎?", "Yes": "啟用同步" }, "AutoBackup": "為了以防萬一,我們已經把你的數據備份到了下載文件夾里的 dim-data.json 文件。", "BackUpFirst": "為了以防萬一,在全部刪除數據前,你必須先將其備份。", "BrowserMayClearData": "如果硬碟空間不足或長時間未訪問DIM,流覽器有可能會刪除這些資料資訊。", "DataIsLocal": "標簽和備注僅保存在本地", "DeleteAllData": "從 DIM 同步服務器刪除所有數據", "DeleteAllDataConfirm": "你確定要從 DIM 同步中刪除你的所有賬號的所有數據嗎?這無法撤銷。", "Details": { "IndexedDBStorage": "本機存放區僅在此流覽器中存儲資訊。如果刪除流覽器資料,也會刪除這些資訊。" }, "DimApiFinePrint": "DIM 會把你的標籤、配裝和設置保存到雲端,並同步給 DIM 的不同版本。", "DimSyncDown": "與服務器通信時出現問題,DIM 同步尚未連接。", "DimSyncEnabled": "DIM 同步已啟用", "DimSyncNotEnabled": "DIM 同步尚未啓用,因此你的設置、標簽、配裝和搜索只保存在本地,會在清理瀏覽器存儲時丟失。在設置中啓用 DIM 同步以自動或手動備份你的數據。", "EnableDimApi": "啟用 DIM 同步(推薦)", "Export": "下載資料備份", "ExportError": "無法從 DIM 同步下載備份", "ExportErrorBody": "DIM同步功能或者你的网络连接可能出现了问题。我们会下载你本地保存的数据。", "Import": "導入資料備份", "ImportConfirmDimApi": "你確定要用此版本的數據覆蓋標籤、配裝和設置嗎?舊數據將被完全覆蓋。", "ImportExport": "備份和導入", "ImportFailed": "導入失敗!{{error}}", "ImportNoFile": "沒有選擇檔!", "ImportNotification": { "FailedBody": "無法導入數據。 {{error}}", "FailedTitle": "导入失败", "NoData": "未在備份中找到配裝或標籤", "SuccessBodyForced": "已從你的備份中導入了 {{loadouts}} 個配裝和 {{tags}} 個已標記的物品到 DIM 同步,並覆蓋了舊內容。", "SuccessBodyLocal": "已從你的備份向本地存儲導入並覆蓋了設置、{{loadouts}} 個配裝,和 {{tags}} 個已標記的物品。我們不能保證本地存儲里的數據完好——請考慮啟用 DIM 同步。", "SuccessTitle": "导入成功" }, "ImportTooManyFiles": "請選擇一個檔案導入。", "ImportWrongFileType": "檔案不是json檔案。它可能不是dim備份。", "IndexedDBStorage": "本地流覽器存儲", "LearnMore": "了解 DIM 同步", "MenuTitle": "同步和備份", "ProfileErrorBody": "與 DIM 同步通信時出現故障。可能無法顯示你最新的設置、標簽、配裝和搜索。你的數據仍在我們的服務器上,而我們會在重新連接后保存你的修改。我們會在 DIM 開啓時自動重試。", "ProfileErrorTitle": "DIM 同步下載出錯", "RefreshDimSync": "Reload remote data from DIM Sync", "UpdateErrorBody": "將數據保存到 DIM 同步時出錯。我們會在 DIM 開啓時自動重試。", "UpdateErrorTitle": "DIM 同步保存錯誤", "UpdateInvalid": "Failed to save data to DIM Sync", "UpdateInvalidBody": "Data sent to DIM Sync was invalid and will not be saved.", "UpdateInvalidBodyLoadout": "The loadout \"{{name}}\" is invalid and will not be saved. If you imported it from another site, please let them know that they are exporting invalid loadouts.", "UpdateQueueLength_other": "我們將在能重新連接時保存 {{count}} 個新更改。", "Usage": "DIM 使用了此設備{{quota, humanBytes}}可用空間中的{{usage, humanBytes}}.這包括從 bungie. net 下載的命運2物品資料庫。" }, "StreamDeck": { "Authorize": "Connect application", "Enable": "Stream Deck 插件", "Error": { "Body": "There was an error sending data to the Stream Deck plugin. Please contact the plugin developer. {{error}}", "Title": "Stream Deck Plugin Error" }, "FinePrint": "啓用與 DIM Stream Deck 插件的連接。該插件是獨立的項目,既不是由 DIM 團隊編寫,也不是由 DIM 團隊提供支持。", "Install": "安裝插件", "MissingAuthorization": "You must authorize the Stream Deck application to connect to DIM. Go to settings and click \"Connect application\".", "Tooltip": { "Application": "Stream Deck Application", "AuthRequired": "Click this button or go to settings and click \"Connect application\".", "Error": "Your Stream Deck plugin is no longer supported. Please update to the latest version. This plugin requires at least:", "ErrorConnection": "if you're already using the latest version, check if some browser extension is blocking the connection.", "ExtensionIssue": "Extensions Issue", "Plugin": "Plugin", "Title": "DIM Stream Deck Plugin", "Version": "Version:" } }, "StripSockets": { "Action": "清空插槽", "ArmorMods": "{{count}}x 防具模組", "Button": "清空 {{numSockets}} 個插槽", "Cancel": "取消", "Choose": "選擇要清空的插槽", "DiscountedMods": "{{count}} 個已享受折扣的模組", "Done": "已清空插槽", "NoSockets": "沒有要清空的插槽。", "Ok": "確定", "Ornaments": "{{count}}x 裝飾品", "Others": "{{count}}x 機靈投影", "Running": "清空插槽", "Shaders": "{{count}}x 著色器", "Subclass": "{{count}}x 子職業選項", "WeaponMods": "{{count}}x 武器模組" }, "Tags": { "Archive": "歸檔", "ClearTag": "清除標籤", "Favorite": "青睞", "Infuse": "灌注", "Junk": "垃圾", "Keep": "保留", "LockAll": "鎖定物品", "TagItem": "標記物品", "UnlockAll": "解鎖物品" }, "Triage": { "AccountsForArtifice": "判斷精巧防具在添加 +3 屬性模組后是否會變得更好。", "BetterArmor": "全面更優的防具", "BetterArtificeArmor": "更好的精巧防具", "BetterStatArmor": "更优的防具", "BetterStatArtificeArmor": "更好的精巧防具", "BetterWorseArmor": "更優/更差的防具", "BetterWorseIncludes": "分析防具所用條件:", "HighStats": "高屬性", "InLoadouts": "在配裝中", "OwnedCount": "數量", "PerkBetterArmorDesc": "相同或更多特性,或特殊模組槽位的護甲。", "PerkWorseArmorDesc": "固有特性相同或不存在。", "SimilarItems": "相似物品", "StatBetterArmorDesc": "所有屬性均持平,并至少有一個屬性更高。", "StatNotPerkArmorDesc": "僅用自身數值來檢測。部分包含特殊的模組槽位或内置特性。", "StatWorseArmorDesc": "所有屬性均持平或更低。", "ThisItem": "當前物品", "WorseArmor": "全面更劣的防具", "WorseArtificeArmor": "更劣的非精巧防具", "WorseStatArmor": "更差的防具", "WorseStatArtificeArmor": "更劣的非精巧防具", "YourBestItem": "你的最佳物品" }, "Triumphs": { "GildingTriumph": "鍍金成就", "HideCompleted": "隱藏已完成的成就", "RevealRedacted": "顯示隱藏的成就", "SortRecords": "優先顯示未完成成就" }, "Vendors": { "Collections": "收藏品", "Engram": "等級", "FilterToUnacquired": "僅顯示未收集物品", "HideSilverItems": "隱藏銀幣物品", "NoItems": "此商人現時沒有提供任何物品。", "RefreshTime": "庫存刷新:", "Vendors": "商販" }, "Views": { "About": { "APIHistory": "查看DIM(及其他命運2app)的歷史記錄", "BungieCopyright": "所有圖像和內容都歸Bungie所有。", "CommunityInsight": "社區特性見解取自 {{clarityLink}}。如果你有任何疑問或問題,請加入 {{clarityDiscordLink}}", "Discord": "Discord", "DiscordHelp": "報告問題,回饋建議,或在Discord頻道中獲得支援。", "FAQ": "常見問題", "FAQAccess": "DIM是如何得到我的Destiny(命運)資料的?", "FAQAccessAnswer": "我們使用Bungie的APP身份驗證獲取許可權以便查看和移動您的物品,這與命運同伴應用(Companion APP)的工作原理相同。因此DIM絕不可能竊取您的用戶名或密碼。", "FAQKeyboard": "我可以在DIM上使用鍵盤快速鍵嗎?", "FAQKeyboardAnswer": "當然! 按下\"?\"鍵查看所有可用的快速鍵.", "FAQLogout": "如何登出DIM?", "FAQLogoutAnswer": "從左上角圖示打開功能表,並選擇“登出”", "FAQLostItem": "在使用你們的網站的過程中我丟失了我的物品!", "FAQLostItemAnswer": "Bungie Api不允許APP刪除物品(包括他們自己的官方APP)。這看起來似乎更像是一次資料傳輸錯誤,把您的物品遺留在了另一個角色的保險庫裡。你可以試試看在其它角色上能否找到。如果其它角色上也沒有,請刷新下頁面。然後打開此連結{{link}} 查看,或者在遊戲中查看物品是否存在。不過我們肯定東西沒有丟失。", "FAQMobile": "DIM是否會支援行動裝置?是否會提供一個APP?", "FAQMobileAnswer": "DIM網站已可以在手機和平板上載入,您可以將其添加至主頁以獲得應用一樣的體驗。", "GitHub": "Github", "GitHubHelp": "如果您有興趣為該項目做出一份貢獻,請訪問我們在{{link}} 上的項目位址!", "Header": "DIM (命運物品管理器)", "HowItsMade": "DIM是壹個由社區開發者在Bungie.net和Destiny Companion 應用程式使用的同類服務基礎上開發的免費、開源的應用程式", "Schedule": { "beta": "每當我們更改代碼時這個測試版的DIM就會自動應用更新 - 它將應用最新的功能和修復補丁,當然咯 也會有最新的Bug哦~", "release": "DIM將於太平洋時間的每週日午夜進行例行版本更新。" }, "Translation": "加入翻譯小組!", "TranslationText": "我們使用{{link}} 來使得翻譯工作更加簡便。 如果您希望為DIM的任意語言翻譯語言出一份力,那麼請加入我們的翻譯小組吧( • ̀ω•́ )✧!", "Version": "版本{{version}}({{flavor}}),編譯於:{{date}}", "Wiki": "DIM 用戶指南", "WikiHelp": "瞭解 DIM 功能" }, "Login": { "Auth": "用Bungie.Net授權", "EnableDimSyncWarning": "你曾禁用過 DIM 同步,僅使用本地數據存儲。啟用 DIM 同步將用 DIM 同步數據替換本地數據。在啟用 DIM 同步前,應該備份你的數據。你可以在設置中恢復備份。", "Explanation": "允許DIM查看並修改您的天命角色,保險庫和進度。", "LearnMore": "了解賬戶與登錄", "NewAccount": "使用其他 Bungie.net 賬戶登錄", "Permission": "我們需要您的許可..." }, "Support": { "BackersDetail": "您可以選擇一次性捐助或者每月捐助,以便我們能繼續積極地開發。", "FreeToDownload": "DIM是一款免費下載使用的產品。同時DIM也是一個開源項目,任何人都可以幫助完善它。您永遠不會在DIM上看到任何一條廣告。這是我們的承諾!", "OpenCollective": "我們使用{{link}} 服務為我們的開發者提供酬勞,以答謝他們在這個專案上的貢獻。", "Store": "我們將我們的標誌與其他設計結合,並在{{link}}出售", "Support": "贊助DIM" } }, "WishListRoll": { "BestRatedTip_other": "這些特性與您願望清單上的某個武器完全匹配。", "Clear": "清除愿望清单", "CopiedLine": "已複製願望清單特性組合到剪貼板", "CopyLine": "複製選定的特性為願望清單特性組合", "DupeRolls": " (+{{num, number}} ignored dupes)", "ExternalSource": "Add another wish list", "ExternalSourcePlaceholder": "Paste wish list URL here", "Header": "願望清單", "Import": "導入願望清單物品", "ImportError": "Error loading wish list from \"{{url}}\": {{error}}", "ImportFailed": "None of your wish lists contained any valid rolls.", "ImportNoFile": "未選取任何檔案。", "InvalidExternalSource": "請為你的外部願望清單來源輸入一個有效的 URL。URL 的開頭必須為以下之一:", "JustAnotherTeam": "Just Another Team", "LastUpdated": "最後更新於 {{lastUpdatedDate}} {{lastUpdatedTime}}", "Num": "在您的願望單中有{{num, number}} 個物品", "NumRolls": "{{num, number}} rolls", "Refresh": "Refresh Wishlist", "SourceAlreadyAdded": "Wish List already added", "UpdateExternalSource": "Add Wish List", "Voltron": "voltron(默認)", "WishListNotes": "願望清單備註:", "WorstRatedTip_other": "這些特性與您垃圾清單上的某個武器完全匹配。" }, "no-space": "空間不足", "wrong-level": "等級錯誤" } ================================================ FILE: src/nuke.php ================================================ 1 && $_POST['token'] === $token) { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data header('Clear-Site-Data: "*"'); // Set the success message $message = 'Requested that your browser delete all DIM data.'; } else { $message = 'CSRF token validation failed! token:' . $token . " posted:" . $_POST['token']; } } // Always generate a new token on page view $token = md5(random_bytes(32)); $_SESSION['csrf_token'] = $token; session_write_close(); ?> DIM NUKE

Nuke DIM Data

This will delete all saved DIM data from this browser. If you did not have DIM Sync set up, you could lose settings, loadouts, and tags. If you do have DIM Sync set up, and it has been running successfully, you won't lose anything.

This does not do anything in Safari or any iOS browser. It may not do as much as you hope in Firefox.

================================================ FILE: src/return.html ================================================ Validating Authorization
Authorizing...
================================================ FILE: src/robots.txt ================================================ User-Agent: * Disallow: /nuke.php Disallow: /backup.html ================================================ FILE: src/service-worker.ts ================================================ import { CacheableResponsePlugin } from 'workbox-cacheable-response'; import { clientsClaim } from 'workbox-core'; import { ExpirationPlugin } from 'workbox-expiration'; import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute, } from 'workbox-precaching'; import { NavigationRoute, registerRoute } from 'workbox-routing'; import { CacheFirst } from 'workbox-strategies'; self.__precacheManifest = self.__WB_MANIFEST.concat(self.__precacheManifest || []); precacheAndRoute(self.__precacheManifest, {}); cleanupOutdatedCaches(); // Once this activates, start handling requests through the service worker immediately. // No need to wait for a refresh. clientsClaim(); registerRoute( /https:\/\/fonts\.(googleapis|gstatic)\.com\/.*/, new CacheFirst({ cacheName: 'googleapis', plugins: [ new ExpirationPlugin({ maxEntries: 20, purgeOnQuotaError: false }), new CacheableResponsePlugin({ statuses: [0, 200] }), ], }), 'GET', ); // Since we're a single page app, route all navigation to /index.html const handler = createHandlerBoundToURL(`${$PUBLIC_PATH}index.html`); const navigationRoute = new NavigationRoute(handler, { // These have their own pages (return.html) // This regex matches on query string too, so no anchors! denylist: [ /return\.html/, /backup\.html/, /\.well-known/, /\.(php|json|wasm|js|css|png|jpg|map)(\.(gz|br))?$/, /\/data\/d1\/manifests\//, ], }); registerRoute(navigationRoute); self.addEventListener('message', (event) => { if (!event.data) { return; } switch (event.data) { case 'skipWaiting': self.skipWaiting(); break; default: // NOOP break; } }); ================================================ FILE: src/testing/data/d1profiles-2022-10-24.json ================================================ { "Response": { "data": { "membershipId": "4611686018433092312", "membershipType": 2, "characters": [ { "characterBase": { "membershipId": "4611686018433092312", "membershipType": 2, "characterId": "2305843009214965410", "dateLastPlayed": "2017-09-07T05:32:39Z", "minutesPlayedThisSession": "0", "minutesPlayedTotal": "54918", "powerLevel": 386, "raceHash": 2803282938, "genderHash": 2204441813, "classHash": 671679327, "currentActivityHash": 0, "lastCompletedStoryHash": 0, "stats": { "STAT_DEFENSE": { "statHash": 3897883278, "value": 0, "maximumValue": 0 }, "STAT_INTELLECT": { "statHash": 144602215, "value": 184, "maximumValue": 0 }, "STAT_DISCIPLINE": { "statHash": 1735777505, "value": 277, "maximumValue": 0 }, "STAT_STRENGTH": { "statHash": 4244567218, "value": 151, "maximumValue": 0 }, "STAT_LIGHT": { "statHash": 2391494160, "value": 386, "maximumValue": 0 }, "STAT_ARMOR": { "statHash": 392767087, "value": 8, "maximumValue": 0 }, "STAT_AGILITY": { "statHash": 2996146975, "value": 5, "maximumValue": 0 }, "STAT_RECOVERY": { "statHash": 1943323491, "value": 3, "maximumValue": 0 }, "STAT_OPTICS": { "statHash": 3555269338, "value": 49, "maximumValue": 0 } }, "customization": { "personality": 2166136261, "face": 2695914758, "skinColor": 1815484269, "lipColor": 2571618623, "eyeColor": 1511637745, "hairColor": 1398195103, "featureColor": 2166136261, "decalColor": 2116428758, "wearHelmet": false, "hairIndex": 11, "featureIndex": 0, "decalIndex": 4 }, "grimoireScore": 5110, "peerView": { "equipment": [ { "itemHash": 2962927168, "dyes": [] }, { "itemHash": 850974178, "dyes": [ { "channelHash": 662199250, "dyeHash": 4280357665 }, { "channelHash": 1367384683, "dyeHash": 1893406720 }, { "channelHash": 218592586, "dyeHash": 3096219295 } ] }, { "itemHash": 2217280774, "dyes": [ { "channelHash": 662199250, "dyeHash": 4280357665 }, { "channelHash": 1367384683, "dyeHash": 1893406720 }, { "channelHash": 218592586, "dyeHash": 3096219295 } ] }, { "itemHash": 4219383232, "dyes": [ { "channelHash": 662199250, "dyeHash": 4280357665 }, { "channelHash": 1367384683, "dyeHash": 1893406720 }, { "channelHash": 218592586, "dyeHash": 3096219295 } ] }, { "itemHash": 1151347422, "dyes": [ { "channelHash": 662199250, "dyeHash": 4280357665 }, { "channelHash": 1367384683, "dyeHash": 1893406720 }, { "channelHash": 218592586, "dyeHash": 3096219295 } ] }, { "itemHash": 1954673441, "dyes": [ { "channelHash": 662199250, "dyeHash": 4280357665 }, { "channelHash": 1367384683, "dyeHash": 1893406720 }, { "channelHash": 218592586, "dyeHash": 3096219295 } ] }, { "itemHash": 2790109141, "dyes": [ { "channelHash": 1667433279, "dyeHash": 232590457 }, { "channelHash": 1667433278, "dyeHash": 3825422722 } ] }, { "itemHash": 2982280293, "dyes": [ { "channelHash": 1667433279, "dyeHash": 2342836205 }, { "channelHash": 1667433278, "dyeHash": 1999894918 } ] }, { "itemHash": 3536592559, "dyes": [ { "channelHash": 1667433279, "dyeHash": 522649679 }, { "channelHash": 1667433278, "dyeHash": 2094128920 } ] }, { "itemHash": 3808257330, "dyes": [{ "channelHash": 284967655, "dyeHash": 696718222 }] }, { "itemHash": 2042062616, "dyes": [{ "channelHash": 2025709351, "dyeHash": 2333365826 }] }, { "itemHash": 379227944, "dyes": [{ "channelHash": 4023194814, "dyeHash": 379227944 }] }, { "itemHash": 2686650663, "dyes": [] }, { "itemHash": 3764987318, "dyes": [ { "channelHash": 662199250, "dyeHash": 4280357665 }, { "channelHash": 1367384683, "dyeHash": 1893406720 }, { "channelHash": 218592586, "dyeHash": 3096219295 } ] }, { "itemHash": 3854908333, "dyes": [ { "channelHash": 662199250, "dyeHash": 4280357665 }, { "channelHash": 1367384683, "dyeHash": 1893406720 }, { "channelHash": 218592586, "dyeHash": 3096219295 } ] }, { "itemHash": 452104880, "dyes": [] }, { "itemHash": 2672107538, "dyes": [] } ] }, "genderType": 1, "classType": 1, "buildStatGroupHash": 3457215711 }, "levelProgression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 346000, "level": 40, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 0, "progressionHash": 1716568313 }, "emblemPath": "/common/destiny_content/icons/10d338bab2d621c0150e7cc5357e603f.jpg", "backgroundPath": "/common/destiny_content/icons/5cd0732340b84771146d8ae86173fd3c.jpg", "emblemHash": 2686650663, "characterLevel": 40, "baseCharacterLevel": 40, "isPrestigeLevel": false, "percentToNextLevel": 0.0 }, { "characterBase": { "membershipId": "4611686018433092312", "membershipType": 2, "characterId": "2305843009271495265", "dateLastPlayed": "2017-04-17T02:56:14Z", "minutesPlayedThisSession": "2", "minutesPlayedTotal": "6896", "powerLevel": 378, "raceHash": 898834093, "genderHash": 3111576190, "classHash": 3655393761, "currentActivityHash": 0, "lastCompletedStoryHash": 0, "stats": { "STAT_DEFENSE": { "statHash": 3897883278, "value": 0, "maximumValue": 0 }, "STAT_INTELLECT": { "statHash": 144602215, "value": 273, "maximumValue": 0 }, "STAT_DISCIPLINE": { "statHash": 1735777505, "value": 185, "maximumValue": 0 }, "STAT_STRENGTH": { "statHash": 4244567218, "value": 104, "maximumValue": 0 }, "STAT_LIGHT": { "statHash": 2391494160, "value": 378, "maximumValue": 0 }, "STAT_ARMOR": { "statHash": 392767087, "value": 6, "maximumValue": 0 }, "STAT_AGILITY": { "statHash": 2996146975, "value": 4, "maximumValue": 0 }, "STAT_RECOVERY": { "statHash": 1943323491, "value": 8, "maximumValue": 0 }, "STAT_OPTICS": { "statHash": 3555269338, "value": 48, "maximumValue": 0 } }, "customization": { "personality": 2166136261, "face": 4017475052, "skinColor": 1086743612, "lipColor": 156633756, "eyeColor": 4187018145, "hairColor": 1992135337, "featureColor": 2166136261, "decalColor": 1720146732, "wearHelmet": false, "hairIndex": 12, "featureIndex": 0, "decalIndex": 1 }, "grimoireScore": 5110, "peerView": { "equipment": [ { "itemHash": 2007186000, "dyes": [] }, { "itemHash": 2007792164, "dyes": [ { "channelHash": 662199250, "dyeHash": 2544555227 }, { "channelHash": 1367384683, "dyeHash": 2514332694 }, { "channelHash": 218592586, "dyeHash": 2642956333 } ] }, { "itemHash": 155374077, "dyes": [ { "channelHash": 662199250, "dyeHash": 1315074014 }, { "channelHash": 1367384683, "dyeHash": 916622543 }, { "channelHash": 218592586, "dyeHash": 1081212030 } ] }, { "itemHash": 3633902467, "dyes": [ { "channelHash": 662199250, "dyeHash": 2544555227 }, { "channelHash": 1367384683, "dyeHash": 2514332694 }, { "channelHash": 218592586, "dyeHash": 2642956333 } ] }, { "itemHash": 3896258079, "dyes": [ { "channelHash": 662199250, "dyeHash": 2544555227 }, { "channelHash": 1367384683, "dyeHash": 2514332694 }, { "channelHash": 218592586, "dyeHash": 2642956333 } ] }, { "itemHash": 2532725498, "dyes": [ { "channelHash": 662199250, "dyeHash": 2544555227 }, { "channelHash": 1367384683, "dyeHash": 2514332694 }, { "channelHash": 218592586, "dyeHash": 2642956333 } ] }, { "itemHash": 100397241, "dyes": [ { "channelHash": 1667433279, "dyeHash": 1468283937 }, { "channelHash": 1667433278, "dyeHash": 3359898026 } ] }, { "itemHash": 1689897198, "dyes": [ { "channelHash": 1667433279, "dyeHash": 1147201972 }, { "channelHash": 1667433278, "dyeHash": 1021946651 } ] }, { "itemHash": 2878293129, "dyes": [ { "channelHash": 1667433279, "dyeHash": 4214683825 }, { "channelHash": 1667433278, "dyeHash": 919238202 } ] }, { "itemHash": 526316384, "dyes": [ { "channelHash": 284967655, "dyeHash": 596032116 }, { "channelHash": 840921382, "dyeHash": 500079079 } ] }, { "itemHash": 201885898, "dyes": [{ "channelHash": 2025709351, "dyeHash": 4007621156 }] }, { "itemHash": 1369867138, "dyes": [{ "channelHash": 4023194814, "dyeHash": 1369867138 }] }, { "itemHash": 3656150983, "dyes": [] }, { "itemHash": 1263693536, "dyes": [ { "channelHash": 662199250, "dyeHash": 2544555227 }, { "channelHash": 1367384683, "dyeHash": 2514332694 }, { "channelHash": 218592586, "dyeHash": 2642956333 } ] }, { "itemHash": 2878029263, "dyes": [ { "channelHash": 662199250, "dyeHash": 2544555227 }, { "channelHash": 1367384683, "dyeHash": 2514332694 }, { "channelHash": 218592586, "dyeHash": 2642956333 } ] }, { "itemHash": 3143956507, "dyes": [] }, { "itemHash": 210497930, "dyes": [] } ] }, "genderType": 0, "classType": 0, "buildStatGroupHash": 3801959103 }, "levelProgression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 346000, "level": 40, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 0, "progressionHash": 1716568313 }, "emblemPath": "/common/destiny_content/icons/2d37c10db3b7329777db68e254d3ba76.jpg", "backgroundPath": "/common/destiny_content/icons/b6b96f36ebfbb9f846eee0e984cc6b13.jpg", "emblemHash": 3656150983, "characterLevel": 40, "baseCharacterLevel": 40, "isPrestigeLevel": false, "percentToNextLevel": 0.0 }, { "characterBase": { "membershipId": "4611686018433092312", "membershipType": 2, "characterId": "2305843009234199144", "dateLastPlayed": "2017-04-17T02:53:43Z", "minutesPlayedThisSession": "77", "minutesPlayedTotal": "9439", "powerLevel": 397, "raceHash": 3887404748, "genderHash": 2204441813, "classHash": 2271682572, "currentActivityHash": 0, "lastCompletedStoryHash": 0, "stats": { "STAT_DEFENSE": { "statHash": 3897883278, "value": 0, "maximumValue": 0 }, "STAT_INTELLECT": { "statHash": 144602215, "value": 308, "maximumValue": 0 }, "STAT_DISCIPLINE": { "statHash": 1735777505, "value": 168, "maximumValue": 0 }, "STAT_STRENGTH": { "statHash": 4244567218, "value": 116, "maximumValue": 0 }, "STAT_LIGHT": { "statHash": 2391494160, "value": 397, "maximumValue": 0 }, "STAT_ARMOR": { "statHash": 392767087, "value": 4, "maximumValue": 0 }, "STAT_AGILITY": { "statHash": 2996146975, "value": 6, "maximumValue": 0 }, "STAT_RECOVERY": { "statHash": 1943323491, "value": 7, "maximumValue": 0 }, "STAT_OPTICS": { "statHash": 3555269338, "value": 84, "maximumValue": 0 } }, "customization": { "personality": 2166136261, "face": 2132087818, "skinColor": 3045033361, "lipColor": 4152042079, "eyeColor": 1194006499, "hairColor": 2500610414, "featureColor": 2166136261, "decalColor": 552943752, "wearHelmet": false, "hairIndex": 2, "featureIndex": 0, "decalIndex": 5 }, "grimoireScore": 5110, "peerView": { "equipment": [ { "itemHash": 3658182170, "dyes": [] }, { "itemHash": 2196848513, "dyes": [ { "channelHash": 662199250, "dyeHash": 2102345799 }, { "channelHash": 1367384683, "dyeHash": 90401834 }, { "channelHash": 218592586, "dyeHash": 2221809569 } ] }, { "itemHash": 1275480033, "dyes": [ { "channelHash": 662199250, "dyeHash": 2102345799 }, { "channelHash": 1367384683, "dyeHash": 90401834 }, { "channelHash": 218592586, "dyeHash": 2221809569 } ] }, { "itemHash": 861802448, "dyes": [ { "channelHash": 662199250, "dyeHash": 2102345799 }, { "channelHash": 1367384683, "dyeHash": 90401834 }, { "channelHash": 218592586, "dyeHash": 2221809569 } ] }, { "itemHash": 4242215215, "dyes": [ { "channelHash": 662199250, "dyeHash": 2102345799 }, { "channelHash": 1367384683, "dyeHash": 90401834 }, { "channelHash": 218592586, "dyeHash": 2221809569 } ] }, { "itemHash": 509065043, "dyes": [ { "channelHash": 662199250, "dyeHash": 2102345799 }, { "channelHash": 1367384683, "dyeHash": 90401834 }, { "channelHash": 218592586, "dyeHash": 2221809569 } ] }, { "itemHash": 3688594190, "dyes": [ { "channelHash": 1667433279, "dyeHash": 1815540086 }, { "channelHash": 1667433278, "dyeHash": 1662112797 } ] }, { "itemHash": 3768542598, "dyes": [ { "channelHash": 1667433279, "dyeHash": 3987940556 }, { "channelHash": 1667433278, "dyeHash": 3984316435 } ] }, { "itemHash": 1551744702, "dyes": [ { "channelHash": 1667433279, "dyeHash": 932960326 }, { "channelHash": 1667433278, "dyeHash": 3506701229 } ] }, { "itemHash": 3926270235, "dyes": [{ "channelHash": 284967655, "dyeHash": 3936715175 }] }, { "itemHash": 2092342455, "dyes": [{ "channelHash": 2025709351, "dyeHash": 84244931 }] }, { "itemHash": 3389648702, "dyes": [{ "channelHash": 4023194814, "dyeHash": 3073379228 }] }, { "itemHash": 2596665930, "dyes": [] }, { "itemHash": 3764987316, "dyes": [ { "channelHash": 662199250, "dyeHash": 2102345799 }, { "channelHash": 1367384683, "dyeHash": 90401834 }, { "channelHash": 218592586, "dyeHash": 2221809569 } ] }, { "itemHash": 2878029263, "dyes": [ { "channelHash": 662199250, "dyeHash": 2102345799 }, { "channelHash": 1367384683, "dyeHash": 90401834 }, { "channelHash": 218592586, "dyeHash": 2221809569 } ] }, { "itemHash": 4219537949, "dyes": [] }, { "itemHash": 117392809, "dyes": [] } ] }, "genderType": 1, "classType": 2, "buildStatGroupHash": 1997970403 }, "levelProgression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 346000, "level": 40, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 0, "progressionHash": 1716568313 }, "emblemPath": "/common/destiny_content/icons/f771ee9690e2bc8138e73d5af4c06a0d.jpg", "backgroundPath": "/common/destiny_content/icons/93635163c0e76b91d4ea2a973763179e.jpg", "emblemHash": 2596665930, "characterLevel": 40, "baseCharacterLevel": 40, "isPrestigeLevel": false, "percentToNextLevel": 0.0 } ], "inventory": { "buckets": { "Invisible": [ { "items": [], "bucketHash": 1367666825 }, { "items": [ { "itemHash": 2360768126, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529095725514848", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 22067719, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 0, "transferStatus": 2, "locked": false, "lockable": false, "objectives": [], "state": 0 } ], "bucketHash": 2987185182 }, { "items": [ { "itemHash": 271410367, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529095725520936", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 4121276759, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 0, "transferStatus": 2, "locked": false, "lockable": false, "objectives": [], "state": 0 }, { "itemHash": 2225855327, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118498480475", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 2020704475, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 0, "transferStatus": 2, "locked": false, "lockable": false, "objectives": [], "state": 0 }, { "itemHash": 1469071803, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529128319366309", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1331511234, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 0, "transferStatus": 2, "locked": false, "lockable": false, "objectives": [], "state": 0 } ], "bucketHash": 549485690 } ], "Item": [ { "items": [ { "itemHash": 991704636, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529047932743808", "itemLevel": 20, "stackSize": 1, "qualityLevel": 60, "stats": [], "primaryStat": { "statHash": 3897883278, "value": 130, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 2844913036, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 3718632281, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 16664395, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133193315988", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2376172783, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 3649859369, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c0abe2d9fab105c942e261846f5d3d7d.png", "perkHash": 1359068529, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 498794675, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529125729486306", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0b5c903eca66b6fc2e3c26440c5b64b4.png", "perkHash": 2661061678, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/5ed28455f292e040bc4f19af101e7398.png", "perkHash": 3743041729, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2503707047, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114959088862", "itemLevel": 56, "stackSize": 1, "qualityLevel": 25, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 38, "maximumValue": 0 }, { "statHash": 4244567218, "value": 40, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1368285237, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2503707047, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/2ba5fe86c4b7917145301764964c0bbe.png", "perkHash": 3906787506, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/07e91a06ece39040d1475f1470ce8b4a.png", "perkHash": 3689182788, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7511f6b6db64bd9dc4cdb2ce78baba34.png", "perkHash": 1880837044, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2563486541, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529077159566329", "itemLevel": 50, "stackSize": 1, "qualityLevel": 2, "stats": [ { "statHash": 144602215, "value": 28, "maximumValue": 0 }, { "statHash": 1735777505, "value": 22, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 302, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 87298, "level": 10, "step": 0, "progressToNextLevel": 1320, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 3731054529, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/1e51aacea8cbf51ccc1f73e88a71f529.png", "perkHash": 3474991791, "isActive": true }, { "iconPath": "/common/destiny_content/icons/eb444533bc8b6874b60ce245303c7631.png", "perkHash": 1986661836, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 4060987422, "isActive": false }, { "iconPath": "/common/destiny_content/icons/736e4005c411aca1474ee893874b9570.png", "perkHash": 1257014586, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 149041867, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129649308177", "itemLevel": 58, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 41, "maximumValue": 0 }, { "statHash": 4244567218, "value": 40, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 380, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2376172783, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 3649859369, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dd203a835fcd01cd7c19360097fd0425.png", "perkHash": 2482846307, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 632341, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529109915361263", "itemLevel": 46, "stackSize": 1, "qualityLevel": 109, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 48, "maximumValue": 0 }, { "statHash": 4244567218, "value": 56, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 369, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2972548328, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a0974670299336fbb5bd8b87ed193eea.png", "perkHash": 1228656138, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3b62c791499bf5016d7fa52f484b15b0.png", "perkHash": 3457701870, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a8616c46c151322d2590e860b7c7d8fe.png", "perkHash": 3735909663, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2931417008, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114853686791", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 61, "maximumValue": 0 }, { "statHash": 4244567218, "value": 61, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1810145619, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 860389683, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/9daf76a4dae91d8696ac3186ca601e16.png", "perkHash": 1577779520, "isActive": false }, { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2dbaa704e7bc129b62ae73ef7a90ec41.png", "perkHash": 4051254786, "isActive": false }, { "iconPath": "/common/destiny_content/icons/60a28de3f3c791200c3c3bacbf42bc17.png", "perkHash": 671224739, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 4232315296, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529088087085099", "itemLevel": 50, "stackSize": 1, "qualityLevel": 4, "stats": [ { "statHash": 144602215, "value": 48, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 44, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 304, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2914841120, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 1150674919, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3b62c791499bf5016d7fa52f484b15b0.png", "perkHash": 3457701870, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 201220485, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131782982044", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 56, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 61, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2252822608, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/97c7cafbf449460cf79d6a0937548ee3.png", "perkHash": 1752089080, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2dbaa704e7bc129b62ae73ef7a90ec41.png", "perkHash": 4051254786, "isActive": false }, { "iconPath": "/common/destiny_content/icons/60a28de3f3c791200c3c3bacbf42bc17.png", "perkHash": 671224739, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 44227836, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529102559284139", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1368285237, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 263023623, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7e95bf66a236b584fa3ec47f92235698.png", "perkHash": 1175838572, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 106004904, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529073863558247", "itemLevel": 52, "stackSize": 1, "qualityLevel": 1, "stats": [ { "statHash": 144602215, "value": 38, "maximumValue": 0 }, { "statHash": 1735777505, "value": 50, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 321, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1368285237, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 85883, "level": 9, "step": 0, "progressToNextLevel": 10020, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 3652839510, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": true }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 3649859369, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dd203a835fcd01cd7c19360097fd0425.png", "perkHash": 2482846307, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3847828218, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133193613417", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/52178947ef39c0b3388ec3b0c1e05291.png", "perkHash": 707792617, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8b5146cef70383225f0761c1fecb89a4.png", "perkHash": 448609668, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 288017098, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529109019697678", "itemLevel": 46, "stackSize": 1, "qualityLevel": 107, "stats": [ { "statHash": 144602215, "value": 58, "maximumValue": 0 }, { "statHash": 1735777505, "value": 60, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 367, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 49948, "level": 6, "step": 0, "progressToNextLevel": 4430, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2605152607, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/97c7cafbf449460cf79d6a0937548ee3.png", "perkHash": 1752089080, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3dc9d2f9858ec738f61cfa86dfa9809.png", "perkHash": 2095340230, "isActive": false }, { "iconPath": "/common/destiny_content/icons/4f33d8f77466ab14d6cba23893abd3a1.png", "perkHash": 1028572792, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 870077908, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529091064040297", "itemLevel": 50, "stackSize": 1, "qualityLevel": 27, "stats": [ { "statHash": 144602215, "value": 40, "maximumValue": 0 }, { "statHash": 1735777505, "value": 43, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 327, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2726639978, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2162910294, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a1f18a14e78185c124c97149a7f8c3b7.png", "perkHash": 1846145321, "isActive": false }, { "iconPath": "/common/destiny_content/icons/f75557181654271021fdf2410f482365.png", "perkHash": 1101818187, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1823306242, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529113105727757", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 59, "maximumValue": 0 }, { "statHash": 1735777505, "value": 80, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 75863, "level": 9, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 1823306242, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/820869f17a66a5c8303b084a6ccad9f9.png", "perkHash": 3240339843, "isActive": true }, { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3287db1f2843024995055b7619268cde.png", "perkHash": 3050866996, "isActive": true }, { "iconPath": "/common/destiny_content/icons/4f33d8f77466ab14d6cba23893abd3a1.png", "perkHash": 1028572792, "isActive": true }, { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 3480879699, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1943318157, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529120723905429", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2752852289, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29067dcd206064531572e46d78a4d21f.png", "perkHash": 124234431, "isActive": false }, { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3dc9d2f9858ec738f61cfa86dfa9809.png", "perkHash": 2095340230, "isActive": false }, { "iconPath": "/common/destiny_content/icons/4f33d8f77466ab14d6cba23893abd3a1.png", "perkHash": 1028572792, "isActive": false }, { "iconPath": "/common/destiny_content/icons/5dd4f90fe2d524aaa4505f449576c7bd.png", "perkHash": 1868041021, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2237496545, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529038696753309", "itemLevel": 20, "stackSize": 1, "qualityLevel": 60, "stats": [], "primaryStat": { "statHash": 3897883278, "value": 130, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 3810914820, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1186397662, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133191434385", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 1462086374, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4f806fc3dbaea6e341cb8ceaeb0e812d.png", "perkHash": 700071256, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eb444533bc8b6874b60ce245303c7631.png", "perkHash": 1986661836, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false }, { "iconPath": "/common/destiny_content/icons/683d97a4f1b92e6d43f9f8916a84dc6c.png", "perkHash": 1694891218, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3367372899, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529102561196532", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 804231650, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2963107642, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7e95bf66a236b584fa3ec47f92235698.png", "perkHash": 1175838572, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2364921277, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119480370671", "itemLevel": 57, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 41, "maximumValue": 0 }, { "statHash": 4244567218, "value": 41, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 370, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 1885, "level": 1, "step": 0, "progressToNextLevel": 1885, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 885052093, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 13 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a1f18a14e78185c124c97149a7f8c3b7.png", "perkHash": 1846145321, "isActive": false }, { "iconPath": "/common/destiny_content/icons/9c0ba6a5a01c98097f4d82b7c10a935f.png", "perkHash": 653222695, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8b5146cef70383225f0761c1fecb89a4.png", "perkHash": 448609668, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3847828216, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133192498277", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3145550630, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0b5c903eca66b6fc2e3c26440c5b64b4.png", "perkHash": 2661061678, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8b5146cef70383225f0761c1fecb89a4.png", "perkHash": 448609668, "isActive": false }, { "iconPath": "/common/destiny_content/icons/d2b9c00bad83f78d72c6fa0df6b54e47.png", "perkHash": 561936092, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1516397455, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119478433567", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3940311006, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4f806fc3dbaea6e341cb8ceaeb0e812d.png", "perkHash": 700071256, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eb444533bc8b6874b60ce245303c7631.png", "perkHash": 1986661836, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 661529959, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119468426148", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 61, "maximumValue": 0 }, { "statHash": 1735777505, "value": 44, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 78988, "level": 9, "step": 0, "progressToNextLevel": 3125, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 661529959, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/0b5c903eca66b6fc2e3c26440c5b64b4.png", "perkHash": 356306776, "isActive": true }, { "iconPath": "/common/destiny_content/icons/0d3cde9d6ec0c2f1a93ab65bacaf3de9.png", "perkHash": 97066487, "isActive": true }, { "iconPath": "/common/destiny_content/icons/5ed28455f292e040bc4f19af101e7398.png", "perkHash": 3743041729, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1001251654, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529075765379787", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 15173, "level": 3, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 3777382782, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/218c1a31067eb15143c0e7bd8f14333f.png", "perkHash": 2818471317, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3152205784, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129591022219", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 25, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 21, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3755757229, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/1e51aacea8cbf51ccc1f73e88a71f529.png", "perkHash": 3474991791, "isActive": false }, { "iconPath": "/common/destiny_content/icons/9f26750f83ac4e8e1c97780117de9d3a.png", "perkHash": 1875467969, "isActive": false }, { "iconPath": "/common/destiny_content/icons/736e4005c411aca1474ee893874b9570.png", "perkHash": 1257014586, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 4232315296, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529086506738719", "itemLevel": 49, "stackSize": 1, "qualityLevel": 6, "stats": [ { "statHash": 144602215, "value": 47, "maximumValue": 0 }, { "statHash": 1735777505, "value": 45, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 296, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2914841120, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 1150674919, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a0974670299336fbb5bd8b87ed193eea.png", "perkHash": 1228656138, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3b62c791499bf5016d7fa52f484b15b0.png", "perkHash": 3457701870, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2038561473, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133056078659", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 24, "maximumValue": 0 }, { "statHash": 1735777505, "value": 23, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3940311006, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4f806fc3dbaea6e341cb8ceaeb0e812d.png", "perkHash": 700071256, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2a6153550adc28e3ff202588f20759eb.png", "perkHash": 2988093069, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1113181209, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119468423906", "itemLevel": 50, "stackSize": 1, "qualityLevel": 85, "stats": [ { "statHash": 144602215, "value": 38, "maximumValue": 0 }, { "statHash": 1735777505, "value": 40, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2376172783, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 3649859369, "isActive": false }, { "iconPath": "/common/destiny_content/icons/43ee387ced62aaa6e710030411b315e4.png", "perkHash": 4090945472, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 626933542, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118368410516", "itemLevel": 46, "stackSize": 1, "qualityLevel": 125, "stats": [ { "statHash": 144602215, "value": 40, "maximumValue": 0 }, { "statHash": 1735777505, "value": 36, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2376172783, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ff74dbed3aab96855f2186ded4a98075.png", "perkHash": 632126377, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/43ee387ced62aaa6e710030411b315e4.png", "perkHash": 4090945472, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2063415782, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074279966700", "itemLevel": 50, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 144602215, "value": 65, "maximumValue": 0 }, { "statHash": 1735777505, "value": 49, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 305, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3485532491, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 77383, "level": 9, "step": 0, "progressToNextLevel": 1520, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 570151472, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": true }, { "iconPath": "/common/destiny_content/icons/006db286765a7ca3cc8090116e57272b.png", "perkHash": 1765729154, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2405148796, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129009101766", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 23, "maximumValue": 0 }, { "statHash": 4244567218, "value": 23, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 979049529, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 224975876, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4f806fc3dbaea6e341cb8ceaeb0e812d.png", "perkHash": 700071256, "isActive": false }, { "iconPath": "/common/destiny_content/icons/81cb0ebcf6b5da06547fd5ae040de98b.png", "perkHash": 3538671621, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2302693613, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529085116380852", "itemLevel": 53, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 144602215, "value": 38, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 55, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 335, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 78747, "level": 9, "step": 0, "progressToNextLevel": 2884, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 1049743322, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/7511f6b6db64bd9dc4cdb2ce78baba34.png", "perkHash": 1880837044, "isActive": true }, { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/d9445dd39dc2b86953a0ae8583700aff.png", "perkHash": 317976013, "isActive": true }, { "iconPath": "/common/destiny_content/icons/43ee387ced62aaa6e710030411b315e4.png", "perkHash": 4090945472, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1665256925, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114463860445", "itemLevel": 55, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 55, "maximumValue": 0 }, { "statHash": 4244567218, "value": 55, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 350, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 2000, "level": 1, "step": 0, "progressToNextLevel": 2000, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2972548328, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": false }, { "iconPath": "/common/destiny_content/icons/006db286765a7ca3cc8090116e57272b.png", "perkHash": 1765729154, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a8616c46c151322d2590e860b7c7d8fe.png", "perkHash": 2999530468, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 144386080, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529121103241192", "itemLevel": 46, "stackSize": 1, "qualityLevel": 87, "stats": [ { "statHash": 144602215, "value": 54, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 56, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 347, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2950776081, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a0974670299336fbb5bd8b87ed193eea.png", "perkHash": 1228656138, "isActive": false }, { "iconPath": "/common/destiny_content/icons/bda8f85a6fd5b6bcbcf6cbbc8306f4de.png", "perkHash": 3129120313, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3b62c791499bf5016d7fa52f484b15b0.png", "perkHash": 3457701870, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1883484055, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529048697407276", "itemLevel": 20, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 19, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 130, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 1368285237, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 123598, "level": 13, "step": 0, "progressToNextLevel": 7275, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2236448540, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 12 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b2e707cc35fb889d19a8fafc50dd4209.png", "perkHash": 765056859, "isActive": false }, { "iconPath": "/common/destiny_content/icons/ff74dbed3aab96855f2186ded4a98075.png", "perkHash": 632126376, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1765846920, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129649302224", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 46, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 39, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0b5c903eca66b6fc2e3c26440c5b64b4.png", "perkHash": 2661061678, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/5ed28455f292e040bc4f19af101e7398.png", "perkHash": 3743041729, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1574664, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129940438975", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 45, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 42, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ad3cf5561c3f09180904c799f0d5abd1.png", "perkHash": 3792198727, "isActive": false }, { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8b5146cef70383225f0761c1fecb89a4.png", "perkHash": 448609668, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2240991371, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529108695776838", "itemLevel": 46, "stackSize": 1, "qualityLevel": 105, "stats": [ { "statHash": 144602215, "value": 50, "maximumValue": 0 }, { "statHash": 1735777505, "value": 56, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 365, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2950776081, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/bda8f85a6fd5b6bcbcf6cbbc8306f4de.png", "perkHash": 3129120313, "isActive": false }, { "iconPath": "/common/destiny_content/icons/66b5c55263b24e7975dfbc0f571f1b60.png", "perkHash": 1456076301, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1494493138, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114853686896", "itemLevel": 55, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 55, "maximumValue": 0 }, { "statHash": 4244567218, "value": 55, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 350, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3485532491, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2206896281, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": false }, { "iconPath": "/common/destiny_content/icons/006db286765a7ca3cc8090116e57272b.png", "perkHash": 1765729154, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a8616c46c151322d2590e860b7c7d8fe.png", "perkHash": 2999530468, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2440643321, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529128595567277", "itemLevel": 50, "stackSize": 1, "qualityLevel": 93, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 22, "maximumValue": 0 }, { "statHash": 4244567218, "value": 25, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 393, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2844913036, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 934533321, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4f806fc3dbaea6e341cb8ceaeb0e812d.png", "perkHash": 700071256, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eb444533bc8b6874b60ce245303c7631.png", "perkHash": 1986661836, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 639304757, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132839728120", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3968045209, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a0974670299336fbb5bd8b87ed193eea.png", "perkHash": 1228656138, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3b62c791499bf5016d7fa52f484b15b0.png", "perkHash": 3457701870, "isActive": false }, { "iconPath": "/common/destiny_content/icons/683d97a4f1b92e6d43f9f8916a84dc6c.png", "perkHash": 1694891218, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2222052867, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529108695775664", "itemLevel": 46, "stackSize": 1, "qualityLevel": 105, "stats": [ { "statHash": 144602215, "value": 59, "maximumValue": 0 }, { "statHash": 1735777505, "value": 57, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 365, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 11764, "level": 2, "step": 0, "progressToNextLevel": 6706, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2252822608, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29067dcd206064531572e46d78a4d21f.png", "perkHash": 124234431, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2dbaa704e7bc129b62ae73ef7a90ec41.png", "perkHash": 4051254786, "isActive": false }, { "iconPath": "/common/destiny_content/icons/60a28de3f3c791200c3c3bacbf42bc17.png", "perkHash": 671224739, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2283132909, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118267193970", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 25, "maximumValue": 0 }, { "statHash": 1735777505, "value": 25, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2844913036, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 1253308114, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4f806fc3dbaea6e341cb8ceaeb0e812d.png", "perkHash": 700071256, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eb444533bc8b6874b60ce245303c7631.png", "perkHash": 1986661836, "isActive": false }, { "iconPath": "/common/destiny_content/icons/736e4005c411aca1474ee893874b9570.png", "perkHash": 1257014586, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2828837278, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529076375860009", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 17396, "level": 3, "step": 0, "progressToNextLevel": 2223, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 1498330252, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/60817125929c46048a359b6ad90a6b0f.png", "perkHash": 525257484, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3934967423, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119480363218", "itemLevel": 50, "stackSize": 1, "qualityLevel": 85, "stats": [ { "statHash": 144602215, "value": 56, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 59, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2605152607, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/97c7cafbf449460cf79d6a0937548ee3.png", "perkHash": 1752089080, "isActive": false }, { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2dbaa704e7bc129b62ae73ef7a90ec41.png", "perkHash": 1723656171, "isActive": false }, { "iconPath": "/common/destiny_content/icons/60a28de3f3c791200c3c3bacbf42bc17.png", "perkHash": 671224739, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 294399990, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529120721490760", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 2250, "currentProgress": 2250, "level": 1, "step": 0, "progressToNextLevel": 2250, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2750544037, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a1f18a14e78185c124c97149a7f8c3b7.png", "perkHash": 1846145321, "isActive": false }, { "iconPath": "/common/destiny_content/icons/9c0ba6a5a01c98097f4d82b7c10a935f.png", "perkHash": 653222695, "isActive": false }, { "iconPath": "/common/destiny_content/icons/5ed28455f292e040bc4f19af101e7398.png", "perkHash": 3743041729, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2242715339, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529091677637234", "itemLevel": 58, "stackSize": 1, "qualityLevel": 1, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 33, "maximumValue": 0 }, { "statHash": 4244567218, "value": 25, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 381, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 58133, "level": 7, "step": 0, "progressToNextLevel": 2500, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 732191223, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fcfeb8020d320abb3154f3c8c0f29b34.png", "perkHash": 3857263415, "isActive": true }, { "iconPath": "/common/destiny_content/icons/eb444533bc8b6874b60ce245303c7631.png", "perkHash": 1986661836, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3342415802, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529109981839470", "itemLevel": 50, "stackSize": 1, "qualityLevel": 68, "stats": [ { "statHash": 144602215, "value": 37, "maximumValue": 0 }, { "statHash": 1735777505, "value": 39, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 368, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2376172783, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ff74dbed3aab96855f2186ded4a98075.png", "perkHash": 632126377, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c0abe2d9fab105c942e261846f5d3d7d.png", "perkHash": 1359068529, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 137348250, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529117674311738", "itemLevel": 46, "stackSize": 1, "qualityLevel": 121, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 43, "maximumValue": 0 }, { "statHash": 4244567218, "value": 45, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 381, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0b5c903eca66b6fc2e3c26440c5b64b4.png", "perkHash": 2661061678, "isActive": false }, { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8b5146cef70383225f0761c1fecb89a4.png", "perkHash": 448609668, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2620256214, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529125728254097", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 25, "maximumValue": 0 }, { "statHash": 1735777505, "value": 23, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3940311006, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fcfeb8020d320abb3154f3c8c0f29b34.png", "perkHash": 3857263415, "isActive": false }, { "iconPath": "/common/destiny_content/icons/03229a7106a6b11a37b7ba7f176a767d.png", "perkHash": 1613412752, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2374619875, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529116941747575", "itemLevel": 46, "stackSize": 1, "qualityLevel": 125, "stats": [ { "statHash": 144602215, "value": 36, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 40, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2376172783, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 3649859369, "isActive": false }, { "iconPath": "/common/destiny_content/icons/89b29362b4be2cb97e00fcc202568a0c.png", "perkHash": 1394084296, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1988228618, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131776357389", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 24, "maximumValue": 0 }, { "statHash": 4244567218, "value": 23, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3940311006, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fad5e0d2ca5946712cb0dbed645f35b6.png", "perkHash": 247305382, "isActive": false }, { "iconPath": "/common/destiny_content/icons/ae3705f13ed7ad31b50abf884fede730.png", "perkHash": 2979228483, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2532725498, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118260907570", "itemLevel": 55, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 25, "maximumValue": 0 }, { "statHash": 4244567218, "value": 23, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 350, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 979049529, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 4201168507, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fad5e0d2ca5946712cb0dbed645f35b6.png", "perkHash": 247305382, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eb444533bc8b6874b60ce245303c7631.png", "perkHash": 1986661836, "isActive": false }, { "iconPath": "/common/destiny_content/icons/736e4005c411aca1474ee893874b9570.png", "perkHash": 1257014586, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2588937859, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129592304217", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2159510519, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 3649859369, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c0abe2d9fab105c942e261846f5d3d7d.png", "perkHash": 1359068529, "isActive": false }, { "iconPath": "/common/destiny_content/icons/683d97a4f1b92e6d43f9f8916a84dc6c.png", "perkHash": 1694891218, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1846613521, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133003319024", "itemLevel": 56, "stackSize": 1, "qualityLevel": 40, "stats": [ { "statHash": 144602215, "value": 25, "maximumValue": 0 }, { "statHash": 1735777505, "value": 22, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3734893712, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/1e51aacea8cbf51ccc1f73e88a71f529.png", "perkHash": 3474991791, "isActive": false }, { "iconPath": "/common/destiny_content/icons/81cb0ebcf6b5da06547fd5ae040de98b.png", "perkHash": 3538671621, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1899850755, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114821748127", "itemLevel": 50, "stackSize": 1, "qualityLevel": 85, "stats": [ { "statHash": 144602215, "value": 37, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 40, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2376172783, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 3649859369, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c0abe2d9fab105c942e261846f5d3d7d.png", "perkHash": 1359068529, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1212068371, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074278000964", "itemLevel": 49, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 144602215, "value": 51, "maximumValue": 0 }, { "statHash": 1735777505, "value": 44, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 295, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1810145619, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 35090, "level": 4, "step": 0, "progressToNextLevel": 9802, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 860389683, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/97c7cafbf449460cf79d6a0937548ee3.png", "perkHash": 1752089080, "isActive": false }, { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/722db9b29e845cecf9874f2df17b7da7.png", "perkHash": 3795565363, "isActive": false }, { "iconPath": "/common/destiny_content/icons/6ebc3c34761cc09c14cb08d03b70358a.png", "perkHash": 3944665868, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 463021445, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133004899528", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 25, "maximumValue": 0 }, { "statHash": 4244567218, "value": 24, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 30, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3755757229, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fcfeb8020d320abb3154f3c8c0f29b34.png", "perkHash": 3857263415, "isActive": false }, { "iconPath": "/common/destiny_content/icons/81cb0ebcf6b5da06547fd5ae040de98b.png", "perkHash": 3538671621, "isActive": false }, { "iconPath": "/common/destiny_content/icons/736e4005c411aca1474ee893874b9570.png", "perkHash": 1257014586, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2055339184, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529103807435915", "itemLevel": 57, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 23, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 25, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 370, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3940311006, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4f806fc3dbaea6e341cb8ceaeb0e812d.png", "perkHash": 700071256, "isActive": false }, { "iconPath": "/common/destiny_content/icons/03229a7106a6b11a37b7ba7f176a767d.png", "perkHash": 1613412752, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2747259661, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529091054212177", "itemLevel": 59, "stackSize": 1, "qualityLevel": 6, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 62, "maximumValue": 0 }, { "statHash": 4244567218, "value": 45, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 396, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 78733, "level": 9, "step": 0, "progressToNextLevel": 2870, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2119370775, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": true }, { "iconPath": "/common/destiny_content/icons/a1f18a14e78185c124c97149a7f8c3b7.png", "perkHash": 1846145321, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3c72fdcea0f7293339d6e4ba25e3001.png", "perkHash": 2131801999, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1519376147, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529073581338566", "itemLevel": 48, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 34, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 35, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 280, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2726639978, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 7438, "progressionHash": 2043912351 }, "talentGridHash": 1531428516, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/23f0c161ba754ba9521ac5efdd5ff863.png", "perkHash": 81610349, "isActive": true }, { "iconPath": "/common/destiny_content/icons/ad3cf5561c3f09180904c799f0d5abd1.png", "perkHash": 3792198727, "isActive": false }, { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8b5146cef70383225f0761c1fecb89a4.png", "perkHash": 448609668, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1266297712, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529075769011263", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 17396, "level": 3, "step": 0, "progressToNextLevel": 2223, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 3489644148, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/c904e4b6443f416cbd592bb0ded8410b.png", "perkHash": 3943006857, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3934967423, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529087009302769", "itemLevel": 48, "stackSize": 1, "qualityLevel": 4, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 47, "maximumValue": 0 }, { "statHash": 4244567218, "value": 48, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 284, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2605152607, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29067dcd206064531572e46d78a4d21f.png", "perkHash": 124234431, "isActive": false }, { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3dc9d2f9858ec738f61cfa86dfa9809.png", "perkHash": 2095340230, "isActive": false }, { "iconPath": "/common/destiny_content/icons/4f33d8f77466ab14d6cba23893abd3a1.png", "perkHash": 1028572792, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 4253790216, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529125728256818", "itemLevel": 22, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 140, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 970850469, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 941890991, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129954380028", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 45, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 41, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 705784709, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 7438, "progressionHash": 2043912351 }, "talentGridHash": 3387912248, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/911111a54572f8ab45dae68fcf320b8d.png", "perkHash": 2239009753, "isActive": true }, { "iconPath": "/common/destiny_content/icons/ad3cf5561c3f09180904c799f0d5abd1.png", "perkHash": 3792198727, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3c72fdcea0f7293339d6e4ba25e3001.png", "perkHash": 2131801999, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1113181209, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133055731627", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 37, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 39, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2376172783, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ff74dbed3aab96855f2186ded4a98075.png", "perkHash": 632126377, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/89b29362b4be2cb97e00fcc202568a0c.png", "perkHash": 1394084296, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3572302285, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129585395068", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 57, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 56, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2605152607, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/97c7cafbf449460cf79d6a0937548ee3.png", "perkHash": 1752089080, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2dbaa704e7bc129b62ae73ef7a90ec41.png", "perkHash": 1723656171, "isActive": false }, { "iconPath": "/common/destiny_content/icons/60a28de3f3c791200c3c3bacbf42bc17.png", "perkHash": 671224739, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 144386080, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529109887039390", "itemLevel": 57, "stackSize": 1, "qualityLevel": 9, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 79, "maximumValue": 0 }, { "statHash": 4244567218, "value": 53, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 379, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 55633, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2950776081, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": false }, { "iconPath": "/common/destiny_content/icons/006db286765a7ca3cc8090116e57272b.png", "perkHash": 1765729154, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1186397662, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529130795400307", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 1462086374, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4f806fc3dbaea6e341cb8ceaeb0e812d.png", "perkHash": 700071256, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eb444533bc8b6874b60ce245303c7631.png", "perkHash": 1986661836, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false }, { "iconPath": "/common/destiny_content/icons/ea8b2823cf505702c1989f5a6e07a4b9.png", "perkHash": 1080822483, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2819887338, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529078951835190", "itemLevel": 49, "stackSize": 1, "qualityLevel": 8, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 45, "maximumValue": 0 }, { "statHash": 4244567218, "value": 46, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 298, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 55633, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2950776081, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/bda8f85a6fd5b6bcbcf6cbbc8306f4de.png", "perkHash": 3129120313, "isActive": false }, { "iconPath": "/common/destiny_content/icons/006db286765a7ca3cc8090116e57272b.png", "perkHash": 1765729154, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1765846920, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118529524890", "itemLevel": 50, "stackSize": 1, "qualityLevel": 85, "stats": [ { "statHash": 144602215, "value": 46, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 42, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0b5c903eca66b6fc2e3c26440c5b64b4.png", "perkHash": 2661061678, "isActive": false }, { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": false }, { "iconPath": "/common/destiny_content/icons/f75557181654271021fdf2410f482365.png", "perkHash": 1101818187, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1736102875, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529125726806733", "itemLevel": 22, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 144602215, "value": 12, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 12, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 150, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 440937088, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 4, "state": 13, "hidden": true, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b2e707cc35fb889d19a8fafc50dd4209.png", "perkHash": 765056859, "isActive": false }, { "iconPath": "/common/destiny_content/icons/38f708ea28f1b195f87870f03c321def.png", "perkHash": 51093942, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8869cbeeb74135d676b87306ec834256.png", "perkHash": 1395368594, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3872841536, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114841924535", "itemLevel": 50, "stackSize": 1, "qualityLevel": 90, "stats": [ { "statHash": 144602215, "value": 42, "maximumValue": 0 }, { "statHash": 1735777505, "value": 44, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ad3cf5561c3f09180904c799f0d5abd1.png", "perkHash": 3792198727, "isActive": false }, { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8b5146cef70383225f0761c1fecb89a4.png", "perkHash": 448609668, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 651489503, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133004061934", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 39, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 46, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0b5c903eca66b6fc2e3c26440c5b64b4.png", "perkHash": 2661061678, "isActive": false }, { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3c72fdcea0f7293339d6e4ba25e3001.png", "perkHash": 2131801999, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 308606233, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529089096951511", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2950776081, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/bda8f85a6fd5b6bcbcf6cbbc8306f4de.png", "perkHash": 3129120313, "isActive": false }, { "iconPath": "/common/destiny_content/icons/006db286765a7ca3cc8090116e57272b.png", "perkHash": 1765729154, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 288017098, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133192495871", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 54, "maximumValue": 0 }, { "statHash": 1735777505, "value": 61, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2605152607, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/9daf76a4dae91d8696ac3186ca601e16.png", "perkHash": 1577779520, "isActive": false }, { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3dc9d2f9858ec738f61cfa86dfa9809.png", "perkHash": 2095340230, "isActive": false }, { "iconPath": "/common/destiny_content/icons/4f33d8f77466ab14d6cba23893abd3a1.png", "perkHash": 1028572792, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3367372899, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529099030556168", "itemLevel": 54, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 36, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 40, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 340, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 804231650, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 4399, "level": 1, "step": 0, "progressToNextLevel": 4399, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2963107642, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a9bad46f2525fdd750a3f87ea0bea47f.png", "perkHash": 2152652274, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7e95bf66a236b584fa3ec47f92235698.png", "perkHash": 1175838572, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3380804975, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118527511406", "itemLevel": 50, "stackSize": 1, "qualityLevel": 85, "stats": [ { "statHash": 144602215, "value": 59, "maximumValue": 0 }, { "statHash": 1735777505, "value": 59, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2605152607, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/820869f17a66a5c8303b084a6ccad9f9.png", "perkHash": 3240339843, "isActive": false }, { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/722db9b29e845cecf9874f2df17b7da7.png", "perkHash": 539512168, "isActive": false }, { "iconPath": "/common/destiny_content/icons/6ebc3c34761cc09c14cb08d03b70358a.png", "perkHash": 3944665868, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1866413378, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529080034584019", "itemLevel": 51, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 144602215, "value": 36, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 50, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 315, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 804231650, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 55633, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 1227905893, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ff74dbed3aab96855f2186ded4a98075.png", "perkHash": 632126377, "isActive": true }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/89b29362b4be2cb97e00fcc202568a0c.png", "perkHash": 1394084296, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 288017098, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529116937725891", "itemLevel": 46, "stackSize": 1, "qualityLevel": 125, "stats": [ { "statHash": 144602215, "value": 61, "maximumValue": 0 }, { "statHash": 1735777505, "value": 52, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2605152607, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29067dcd206064531572e46d78a4d21f.png", "perkHash": 124234431, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a0974670299336fbb5bd8b87ed193eea.png", "perkHash": 1228656138, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2dbaa704e7bc129b62ae73ef7a90ec41.png", "perkHash": 1723656171, "isActive": false }, { "iconPath": "/common/destiny_content/icons/60a28de3f3c791200c3c3bacbf42bc17.png", "perkHash": 671224739, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3282867245, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529113129492580", "itemLevel": 50, "stackSize": 1, "qualityLevel": 86, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 85, "maximumValue": 0 }, { "statHash": 4244567218, "value": 61, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 386, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 30, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 69748, "level": 8, "step": 0, "progressToNextLevel": 4000, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2252822608, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/820869f17a66a5c8303b084a6ccad9f9.png", "perkHash": 3240339843, "isActive": true }, { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3dc9d2f9858ec738f61cfa86dfa9809.png", "perkHash": 2556279843, "isActive": false }, { "iconPath": "/common/destiny_content/icons/4f33d8f77466ab14d6cba23893abd3a1.png", "perkHash": 1028572792, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 290931251, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131103099853", "itemLevel": 50, "stackSize": 1, "qualityLevel": 97, "stats": [ { "statHash": 144602215, "value": 24, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 24, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 397, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2844913036, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 934533321, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fcfeb8020d320abb3154f3c8c0f29b34.png", "perkHash": 3857263415, "isActive": false }, { "iconPath": "/common/destiny_content/icons/03229a7106a6b11a37b7ba7f176a767d.png", "perkHash": 1613412752, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 446517955, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132701931541", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2159510519, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ff74dbed3aab96855f2186ded4a98075.png", "perkHash": 632126377, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/86e7704c989095441f627a86783a83f4.png", "perkHash": 4111868508, "isActive": false }, { "iconPath": "/common/destiny_content/icons/405c66653f98a2e0fe36ec12778c015f.png", "perkHash": 2504755345, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 144386080, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529110267209313", "itemLevel": 46, "stackSize": 1, "qualityLevel": 115, "stats": [ { "statHash": 144602215, "value": 75, "maximumValue": 0 }, { "statHash": 1735777505, "value": 55, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 375, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 55633, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2950776081, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a0974670299336fbb5bd8b87ed193eea.png", "perkHash": 1228656138, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": false }, { "iconPath": "/common/destiny_content/icons/66b5c55263b24e7975dfbc0f571f1b60.png", "perkHash": 1456076301, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3934967423, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129955528018", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 61, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 55, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2605152607, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/97c7cafbf449460cf79d6a0937548ee3.png", "perkHash": 1752089080, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a0974670299336fbb5bd8b87ed193eea.png", "perkHash": 1228656138, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2dbaa704e7bc129b62ae73ef7a90ec41.png", "perkHash": 1723656171, "isActive": false }, { "iconPath": "/common/destiny_content/icons/60a28de3f3c791200c3c3bacbf42bc17.png", "perkHash": 671224739, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 446517955, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119629962026", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2159510519, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ff74dbed3aab96855f2186ded4a98075.png", "perkHash": 632126377, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/07e91a06ece39040d1475f1470ce8b4a.png", "perkHash": 3689182788, "isActive": false }, { "iconPath": "/common/destiny_content/icons/683d97a4f1b92e6d43f9f8916a84dc6c.png", "perkHash": 1694891218, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2055339189, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133192151019", "itemLevel": 50, "stackSize": 1, "qualityLevel": 92, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 25, "maximumValue": 0 }, { "statHash": 4244567218, "value": 24, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 391, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3940311006, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fcfeb8020d320abb3154f3c8c0f29b34.png", "perkHash": 3857263415, "isActive": false }, { "iconPath": "/common/destiny_content/icons/81cb0ebcf6b5da06547fd5ae040de98b.png", "perkHash": 3538671621, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7815b4d787bddb6b16c11ce2facf7f87.png", "perkHash": 3669655943, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3055891368, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129007837884", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 60, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 59, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2252822608, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/97c7cafbf449460cf79d6a0937548ee3.png", "perkHash": 1752089080, "isActive": false }, { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2dbaa704e7bc129b62ae73ef7a90ec41.png", "perkHash": 4051254786, "isActive": false }, { "iconPath": "/common/destiny_content/icons/60a28de3f3c791200c3c3bacbf42bc17.png", "perkHash": 671224739, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3957443386, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129578433441", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 39, "maximumValue": 0 }, { "statHash": 1735777505, "value": 46, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0b5c903eca66b6fc2e3c26440c5b64b4.png", "perkHash": 2661061678, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/5ed28455f292e040bc4f19af101e7398.png", "perkHash": 3743041729, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2975536019, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133193315299", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 804231650, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 867718073, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ff74dbed3aab96855f2186ded4a98075.png", "perkHash": 632126377, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7e95bf66a236b584fa3ec47f92235698.png", "perkHash": 1175838572, "isActive": false }, { "iconPath": "/common/destiny_content/icons/405c66653f98a2e0fe36ec12778c015f.png", "perkHash": 2504755345, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2072689472, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119353585368", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 1297429066, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/c904e4b6443f416cbd592bb0ded8410b.png", "perkHash": 3943006857, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1706217754, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119353587912", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 208713380, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/218c1a31067eb15143c0e7bd8f14333f.png", "perkHash": 2818471317, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2640087282, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114984790371", "itemLevel": 50, "stackSize": 1, "qualityLevel": 85, "stats": [ { "statHash": 144602215, "value": 52, "maximumValue": 0 }, { "statHash": 1735777505, "value": 52, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3485532491, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 570151472, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fc49809367bbf65f0cf3ebe46ec0b6cd.png", "perkHash": 709737102, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3b62c791499bf5016d7fa52f484b15b0.png", "perkHash": 3457701870, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 201220485, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114463854036", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 86, "maximumValue": 0 }, { "statHash": 4244567218, "value": 61, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 80623, "level": 9, "step": 0, "progressToNextLevel": 4760, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2252822608, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29067dcd206064531572e46d78a4d21f.png", "perkHash": 124234431, "isActive": true }, { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/722db9b29e845cecf9874f2df17b7da7.png", "perkHash": 3795565363, "isActive": true }, { "iconPath": "/common/destiny_content/icons/6ebc3c34761cc09c14cb08d03b70358a.png", "perkHash": 3944665868, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3572302285, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133056775603", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 60, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 53, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2605152607, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/97c7cafbf449460cf79d6a0937548ee3.png", "perkHash": 1752089080, "isActive": false }, { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2dbaa704e7bc129b62ae73ef7a90ec41.png", "perkHash": 1723656171, "isActive": false }, { "iconPath": "/common/destiny_content/icons/60a28de3f3c791200c3c3bacbf42bc17.png", "perkHash": 671224739, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3282867245, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529116941748707", "itemLevel": 46, "stackSize": 1, "qualityLevel": 125, "stats": [ { "statHash": 144602215, "value": 60, "maximumValue": 0 }, { "statHash": 1735777505, "value": 53, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2252822608, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/97c7cafbf449460cf79d6a0937548ee3.png", "perkHash": 1752089080, "isActive": false }, { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/722db9b29e845cecf9874f2df17b7da7.png", "perkHash": 3795565363, "isActive": false }, { "iconPath": "/common/destiny_content/icons/6ebc3c34761cc09c14cb08d03b70358a.png", "perkHash": 3944665868, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2402750214, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529077832885599", "itemLevel": 51, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 144602215, "value": 48, "maximumValue": 0 }, { "statHash": 1735777505, "value": 70, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 315, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 318828292, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 87248, "level": 10, "step": 0, "progressToNextLevel": 1270, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 2972548328, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/eeabf440d8e25aa1d39e08c38ad65bfc.png", "perkHash": 1269473995, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e468c9457391c7c004e9dcf4a136da4c.png", "perkHash": 1880426832, "isActive": true }, { "iconPath": "/common/destiny_content/icons/3b62c791499bf5016d7fa52f484b15b0.png", "perkHash": 3457701870, "isActive": true }, { "iconPath": "/common/destiny_content/icons/a8616c46c151322d2590e860b7c7d8fe.png", "perkHash": 3735909663, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2970247930, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119629967165", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3145550630, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/52178947ef39c0b3388ec3b0c1e05291.png", "perkHash": 707792617, "isActive": false }, { "iconPath": "/common/destiny_content/icons/1b2153a822907ca11ec3cb3987b544a7.png", "perkHash": 2664229771, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8b5146cef70383225f0761c1fecb89a4.png", "perkHash": 448609668, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8abe02a926935725024b14de9e49bcae.png", "perkHash": 1907552246, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3282867245, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529121099309976", "itemLevel": 46, "stackSize": 1, "qualityLevel": 125, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 58, "maximumValue": 0 }, { "statHash": 4244567218, "value": 60, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2252822608, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29067dcd206064531572e46d78a4d21f.png", "perkHash": 124234431, "isActive": false }, { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3dc9d2f9858ec738f61cfa86dfa9809.png", "perkHash": 2556279843, "isActive": false }, { "iconPath": "/common/destiny_content/icons/4f33d8f77466ab14d6cba23893abd3a1.png", "perkHash": 1028572792, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2374619875, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129955526677", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 39, "maximumValue": 0 }, { "statHash": 4244567218, "value": 39, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 201769094, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 3750, "level": 1, "step": 0, "progressToNextLevel": 3750, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2376172783, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/ff74dbed3aab96855f2186ded4a98075.png", "perkHash": 632126377, "isActive": false }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 1950307497, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dd203a835fcd01cd7c19360097fd0425.png", "perkHash": 2482846307, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2970247930, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132701925894", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3145550630, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/52178947ef39c0b3388ec3b0c1e05291.png", "perkHash": 707792617, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c3c72fdcea0f7293339d6e4ba25e3001.png", "perkHash": 2131801999, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8abe02a926935725024b14de9e49bcae.png", "perkHash": 1907552246, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3055891368, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132960045334", "itemLevel": 50, "stackSize": 1, "qualityLevel": 94, "stats": [ { "statHash": 144602215, "value": 60, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 60, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 394, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2252822608, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29067dcd206064531572e46d78a4d21f.png", "perkHash": 124234431, "isActive": false }, { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/722db9b29e845cecf9874f2df17b7da7.png", "perkHash": 3795565363, "isActive": false }, { "iconPath": "/common/destiny_content/icons/6ebc3c34761cc09c14cb08d03b70358a.png", "perkHash": 3944665868, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1988228621, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529113123548932", "itemLevel": 50, "stackSize": 1, "qualityLevel": 85, "stats": [ { "statHash": 144602215, "value": 35, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 23, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 48936, "level": 6, "step": 0, "progressToNextLevel": 3418, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 3755757229, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fcfeb8020d320abb3154f3c8c0f29b34.png", "perkHash": 3857263415, "isActive": true }, { "iconPath": "/common/destiny_content/icons/ae3705f13ed7ad31b50abf884fede730.png", "perkHash": 2979228483, "isActive": false }, { "iconPath": "/common/destiny_content/icons/736e4005c411aca1474ee893874b9570.png", "perkHash": 1257014586, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1765846920, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119468426882", "itemLevel": 50, "stackSize": 1, "qualityLevel": 85, "stats": [ { "statHash": 144602215, "value": 44, "maximumValue": 0 }, { "statHash": 1735777505, "value": 46, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0b5c903eca66b6fc2e3c26440c5b64b4.png", "perkHash": 2661061678, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8b5146cef70383225f0761c1fecb89a4.png", "perkHash": 448609668, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1126881294, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133193314791", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 25, "maximumValue": 0 }, { "statHash": 4244567218, "value": 24, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 218917, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3755757229, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4f806fc3dbaea6e341cb8ceaeb0e812d.png", "perkHash": 700071256, "isActive": false }, { "iconPath": "/common/destiny_content/icons/ae3705f13ed7ad31b50abf884fede730.png", "perkHash": 2979228483, "isActive": false }, { "iconPath": "/common/destiny_content/icons/736e4005c411aca1474ee893874b9570.png", "perkHash": 1257014586, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1574664, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529130707583095", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 46, "maximumValue": 0 }, { "statHash": 1735777505, "value": 40, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3720682193, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3106303166, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/52178947ef39c0b3388ec3b0c1e05291.png", "perkHash": 707792617, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3f319ccb68ec66d5afb4f657842c4a59.png", "perkHash": 3302053862, "isActive": false }, { "iconPath": "/common/destiny_content/icons/5ed28455f292e040bc4f19af101e7398.png", "perkHash": 3743041729, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3282867245, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129956923335", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 144602215, "value": 55, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 57, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1302045394, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2252822608, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": false, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29067dcd206064531572e46d78a4d21f.png", "perkHash": 124234431, "isActive": false }, { "iconPath": "/common/destiny_content/icons/af945aa18fdf771f2ec94c9f800862b8.png", "perkHash": 3326413736, "isActive": false }, { "iconPath": "/common/destiny_content/icons/722db9b29e845cecf9874f2df17b7da7.png", "perkHash": 3795565363, "isActive": false }, { "iconPath": "/common/destiny_content/icons/6ebc3c34761cc09c14cb08d03b70358a.png", "perkHash": 3944665868, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 } ], "bucketHash": 3003523923 }, { "items": [ { "itemHash": 4254734345, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131101753229", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 22, "maximumValue": 100 }, { "statHash": 4043523819, "value": 81, "maximumValue": 100 }, { "statHash": 1240592695, "value": 32, "maximumValue": 100 }, { "statHash": 155624089, "value": 42, "maximumValue": 100 }, { "statHash": 4188031367, "value": 45, "maximumValue": 100 }, { "statHash": 3871231066, "value": 7, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2452843494, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 1327609045 }, "talentGridHash": 1002266788, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 3, "21": 3 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": false }, { "iconPath": "/common/destiny_content/icons/af9fb9e79767496f21c690f35547d4ea.png", "perkHash": 2421244048, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1758882169, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529109015765639", "itemLevel": 55, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 62, "maximumValue": 100 }, { "statHash": 4043523819, "value": 20, "maximumValue": 100 }, { "statHash": 1240592695, "value": 27, "maximumValue": 100 }, { "statHash": 155624089, "value": 36, "maximumValue": 100 }, { "statHash": 4188031367, "value": 52, "maximumValue": 100 }, { "statHash": 3871231066, "value": 26, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 350, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 33558, "level": 9, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4284, "progressionHash": 2657402687 }, "talentGridHash": 1758882169, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 16 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 17 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 18 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 19 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 20 } ], "useCustomDyes": true, "artRegions": { "3": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/f4390cf7106e11adf6911844444daaed.png", "perkHash": 106314641, "isActive": true }, { "iconPath": "/common/destiny_content/icons/39cc45284689e06dfa03465dd40dca41.png", "perkHash": 602922568, "isActive": true }, { "iconPath": "/common/destiny_content/icons/03546d548bb56028f44721b78542e0e1.png", "perkHash": 4251636737, "isActive": false }, { "iconPath": "/common/destiny_content/icons/75290615c94c18bd73218e242de74dd5.png", "perkHash": 2935545776, "isActive": false }, { "iconPath": "/common/destiny_content/icons/f4390cf7106e11adf6911844444daaed.png", "perkHash": 670580232, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2055601061, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529064760629424", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 28, "maximumValue": 100 }, { "statHash": 1240592695, "value": 34, "maximumValue": 100 }, { "statHash": 155624089, "value": 79, "maximumValue": 100 }, { "statHash": 4188031367, "value": 65, "maximumValue": 100 }, { "statHash": 3871231066, "value": 33, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 1, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 29274, "level": 8, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4284, "progressionHash": 2657402687 }, "talentGridHash": 757159366, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 15 } ], "useCustomDyes": true, "artRegions": { "3": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/090f8a416432ac70839834581dca99bd.png", "perkHash": 3300391898, "isActive": true }, { "iconPath": "/common/destiny_content/icons/83cf91d2d68493be72fd70209be4c1c0.png", "perkHash": 1632665602, "isActive": false }, { "iconPath": "/common/destiny_content/icons/0da3f7cc9a45f400625bc2cab2d38439.png", "perkHash": 2270268283, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2542033072, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114966955247", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 27, "maximumValue": 100 }, { "statHash": 4043523819, "value": 62, "maximumValue": 100 }, { "statHash": 1240592695, "value": 85, "maximumValue": 100 }, { "statHash": 155624089, "value": 24, "maximumValue": 100 }, { "statHash": 4188031367, "value": 52, "maximumValue": 100 }, { "statHash": 3871231066, "value": 12, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 1635066761 }, "talentGridHash": 1386452352, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "1": 0, "5": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e0fbfaa73de5378fdf11a92827af751b.png", "perkHash": 4023922623, "isActive": false }, { "iconPath": "/common/destiny_content/icons/678bb39407a7151deb656cd0fc679941.png", "perkHash": 3851285775, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1050258874, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129648053311", "itemLevel": 58, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 100, "maximumValue": 100 }, { "statHash": 4043523819, "value": 2, "maximumValue": 100 }, { "statHash": 1240592695, "value": 17, "maximumValue": 100 }, { "statHash": 155624089, "value": 43, "maximumValue": 100 }, { "statHash": 4188031367, "value": 77, "maximumValue": 100 }, { "statHash": 3871231066, "value": 57, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 380, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 7, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 2958601622 }, "talentGridHash": 188529294, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 1, "21": 1 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4a7731eac3630914e5a4dde7929f7213.png", "perkHash": 1688301903, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c5fabd06404f049071ee98ad20705dbf.png", "perkHash": 2814215427, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1585718131, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129512751901", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 2961396640, "value": 24, "maximumValue": 100 }, { "statHash": 4043523819, "value": 86, "maximumValue": 100 }, { "statHash": 1240592695, "value": 36, "maximumValue": 100 }, { "statHash": 155624089, "value": 54, "maximumValue": 100 }, { "statHash": 4188031367, "value": 59, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 3605101483 }, "talentGridHash": 1093766598, "nodes": [ { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 7, "5": 0, "21": 7 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/2e9c80070ae6fb6b14c5a23d7c130f79.png", "perkHash": 1900693659, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c5fabd06404f049071ee98ad20705dbf.png", "perkHash": 2814215427, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2055601062, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529071835324993", "itemLevel": 49, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 88, "maximumValue": 100 }, { "statHash": 4043523819, "value": 8, "maximumValue": 100 }, { "statHash": 1240592695, "value": 58, "maximumValue": 100 }, { "statHash": 155624089, "value": 75, "maximumValue": 100 }, { "statHash": 4188031367, "value": 79, "maximumValue": 100 }, { "statHash": 3871231066, "value": 36, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 290, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 1, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 24990, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4284, "progressionHash": 2657402687 }, "talentGridHash": 757159365, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 15 } ], "useCustomDyes": true, "artRegions": { "3": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/08fd7eb859387c7a26ce561c8b0b88de.png", "perkHash": 4205188814, "isActive": true }, { "iconPath": "/common/destiny_content/icons/c5fabd06404f049071ee98ad20705dbf.png", "perkHash": 2814215427, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1264422556, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529092530260714", "itemLevel": 50, "stackSize": 1, "qualityLevel": 32, "stats": [ { "statHash": 4284893193, "value": 40, "maximumValue": 100 }, { "statHash": 4043523819, "value": 13, "maximumValue": 100 }, { "statHash": 1240592695, "value": 66, "maximumValue": 100 }, { "statHash": 155624089, "value": 48, "maximumValue": 100 }, { "statHash": 4188031367, "value": 85, "maximumValue": 100 }, { "statHash": 3871231066, "value": 5, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 332, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 536, "weeklyProgress": 536, "currentProgress": 635, "level": 1, "step": 0, "progressToNextLevel": 635, "nextLevelAt": 1999, "progressionHash": 1156912330 }, "talentGridHash": 2200380957, "nodes": [ { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 8, "5": 0, "21": 8 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/dbdf5aa087d3c511169748b483e8b5aa.png", "perkHash": 2626112550, "isActive": false }, { "iconPath": "/common/destiny_content/icons/d21f4f4ee36600d4f32394038d2fa57f.png", "perkHash": 972070082, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 261727865, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529029943344326", "itemLevel": 18, "stackSize": 1, "qualityLevel": 40, "stats": [ { "statHash": 4284893193, "value": 66, "maximumValue": 100 }, { "statHash": 4043523819, "value": 14, "maximumValue": 100 }, { "statHash": 1240592695, "value": 45, "maximumValue": 100 }, { "statHash": 155624089, "value": 70, "maximumValue": 100 }, { "statHash": 4188031367, "value": 72, "maximumValue": 100 }, { "statHash": 3871231066, "value": 21, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 116, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 16, "unlockFlagHashRequiredToEquip": 919048791, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 40800, "level": 11, "step": 0, "progressToNextLevel": 680, "nextLevelAt": 4080, "progressionHash": 1372069377 }, "talentGridHash": 1393344713, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "5": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/30d243fe46142e83f303c728fd261771.png", "perkHash": 4262725708, "isActive": true }, { "iconPath": "/common/destiny_content/icons/39cc45284689e06dfa03465dd40dca41.png", "perkHash": 3245550306, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3490124917, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133193617903", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 37, "maximumValue": 100 }, { "statHash": 4043523819, "value": 48, "maximumValue": 100 }, { "statHash": 1240592695, "value": 70, "maximumValue": 100 }, { "statHash": 155624089, "value": 59, "maximumValue": 100 }, { "statHash": 4188031367, "value": 69, "maximumValue": 100 }, { "statHash": 3871231066, "value": 16, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 4, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 1635066761 }, "talentGridHash": 689346335, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "5": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/46fa6543967995eff68f32196e1a5a54.png", "perkHash": 770631416, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e1f8726fd86727f2bde3d92ea72007b0.png", "perkHash": 1863078623, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 337037804, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529036647862502", "itemLevel": 20, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 15, "maximumValue": 100 }, { "statHash": 4043523819, "value": 94, "maximumValue": 100 }, { "statHash": 1240592695, "value": 48, "maximumValue": 100 }, { "statHash": 155624089, "value": 17, "maximumValue": 100 }, { "statHash": 4188031367, "value": 37, "maximumValue": 100 }, { "statHash": 3871231066, "value": 7, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 138, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 2452843494, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 189000, "level": 45, "step": 0, "progressToNextLevel": 1218, "nextLevelAt": 4284, "progressionHash": 1327609045 }, "talentGridHash": 1038182623, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 } ], "useCustomDyes": true, "artRegions": { "3": 3, "21": 3 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/f1c3005f73f780570c43691109367214.png", "perkHash": 824061322, "isActive": true }, { "iconPath": "/common/destiny_content/icons/fe7bb77174e0df000cdc9677e0b1a9bc.png", "perkHash": 4198986151, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2992782156, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129007834764", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 4284893193, "value": 18, "maximumValue": 100 }, { "statHash": 3614673599, "value": 98, "maximumValue": 100 }, { "statHash": 2523465841, "value": 39, "maximumValue": 100 }, { "statHash": 155624089, "value": 56, "maximumValue": 100 }, { "statHash": 4188031367, "value": 51, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1054627247 }, "talentGridHash": 3085113436, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b6aa44fdd61ba9212754ac9d8fdb5baf.png", "perkHash": 2784842908, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7fec2a9c052b7180bec71dffc761aadb.png", "perkHash": 2179933840, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3835813881, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529080942747022", "itemLevel": 51, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 19, "maximumValue": 100 }, { "statHash": 4043523819, "value": 31, "maximumValue": 100 }, { "statHash": 1240592695, "value": 96, "maximumValue": 100 }, { "statHash": 155624089, "value": 32, "maximumValue": 100 }, { "statHash": 4188031367, "value": 62, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 310, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 17650, "level": 8, "step": 0, "progressToNextLevel": 1257, "nextLevelAt": 2399, "progressionHash": 2826935383 }, "talentGridHash": 274143998, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/709ca560b8423681442751d50d715d0f.png", "perkHash": 4271995221, "isActive": true }, { "iconPath": "/common/destiny_content/icons/83cf91d2d68493be72fd70209be4c1c0.png", "perkHash": 3002204708, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 552354419, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529070116024928", "itemLevel": 49, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 22, "maximumValue": 100 }, { "statHash": 4043523819, "value": 81, "maximumValue": 100 }, { "statHash": 1240592695, "value": 40, "maximumValue": 100 }, { "statHash": 155624089, "value": 52, "maximumValue": 100 }, { "statHash": 4188031367, "value": 43, "maximumValue": 100 }, { "statHash": 3871231066, "value": 12, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 290, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2452843494, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 24990, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4284, "progressionHash": 1336676114 }, "talentGridHash": 3421457034, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 2, "21": 2 }, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/46fa6543967995eff68f32196e1a5a54.png", "perkHash": 770631416, "isActive": true }, { "iconPath": "/common/destiny_content/icons/4369f9d40063918cdc171ca264a49ee7.png", "perkHash": 4036458532, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 486279087, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529072005099520", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 5, "maximumValue": 100 }, { "statHash": 4043523819, "value": 67, "maximumValue": 100 }, { "statHash": 1240592695, "value": 31, "maximumValue": 100 }, { "statHash": 155624089, "value": 26, "maximumValue": 100 }, { "statHash": 4188031367, "value": 9, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 31571, "level": 14, "step": 0, "progressToNextLevel": 784, "nextLevelAt": 2399, "progressionHash": 2572834290 }, "talentGridHash": 1418552541, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 2290550941, "isActive": true }, { "iconPath": "/common/destiny_content/icons/5cfab37355e9e524d235b5222ab8cea0.png", "perkHash": 2201881340, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1505957929, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132838009506", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 11, "maximumValue": 100 }, { "statHash": 3614673599, "value": 70, "maximumValue": 100 }, { "statHash": 2523465841, "value": 86, "maximumValue": 100 }, { "statHash": 155624089, "value": 41, "maximumValue": 100 }, { "statHash": 4188031367, "value": 72, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1054627247 }, "talentGridHash": 1967036684, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/507b0ea0b465e8520d3e16df26d08035.png", "perkHash": 3283108368, "isActive": false }, { "iconPath": "/common/destiny_content/icons/07d2a002cdd4518759a5ab61112eacfe.png", "perkHash": 2725595956, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1475134443, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529067458797248", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 37, "maximumValue": 100 }, { "statHash": 4043523819, "value": 48, "maximumValue": 100 }, { "statHash": 1240592695, "value": 60, "maximumValue": 100 }, { "statHash": 155624089, "value": 97, "maximumValue": 100 }, { "statHash": 4188031367, "value": 75, "maximumValue": 100 }, { "statHash": 3871231066, "value": 17, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 7, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 34558, "level": 9, "step": 0, "progressToNextLevel": 1000, "nextLevelAt": 4284, "progressionHash": 1635066761 }, "talentGridHash": 2145151405, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 4, "5": 0, "21": 4 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fa74e3c5a127176e77777257f7a61363.png", "perkHash": 3911170550, "isActive": true }, { "iconPath": "/common/destiny_content/icons/d21f4f4ee36600d4f32394038d2fa57f.png", "perkHash": 972070082, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e0fbfaa73de5378fdf11a92827af751b.png", "perkHash": 4023922623, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3904536202, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529092662222320", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 66, "maximumValue": 100 }, { "statHash": 4043523819, "value": 14, "maximumValue": 100 }, { "statHash": 1240592695, "value": 40, "maximumValue": 100 }, { "statHash": 155624089, "value": 99, "maximumValue": 100 }, { "statHash": 4188031367, "value": 68, "maximumValue": 100 }, { "statHash": 3871231066, "value": 24, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 919048791, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 29090, "level": 7, "step": 0, "progressToNextLevel": 4100, "nextLevelAt": 4284, "progressionHash": 4231690432 }, "talentGridHash": 3684294341, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 9, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 13, "5": 0, "21": 13 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b1d6a5c5769303d22872631148c016bd.png", "perkHash": 731752060, "isActive": true }, { "iconPath": "/common/destiny_content/icons/4a7731eac3630914e5a4dde7929f7213.png", "perkHash": 1688301903, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3497087277, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529091068439971", "itemLevel": 48, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 27, "maximumValue": 100 }, { "statHash": 4043523819, "value": 61, "maximumValue": 100 }, { "statHash": 1240592695, "value": 84, "maximumValue": 100 }, { "statHash": 155624089, "value": 40, "maximumValue": 100 }, { "statHash": 4188031367, "value": 52, "maximumValue": 100 }, { "statHash": 3871231066, "value": 16, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 280, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 2624, "weeklyProgress": 2624, "currentProgress": 2624, "level": 1, "step": 0, "progressToNextLevel": 2624, "nextLevelAt": 3570, "progressionHash": 1635066761 }, "talentGridHash": 2073748644, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 8, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 7, "5": 0, "21": 7 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a180884886739b34b824ff9afed386fb.png", "perkHash": 2450150110, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 602786655, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529078460746562", "itemLevel": 48, "stackSize": 1, "qualityLevel": 6, "stats": [ { "statHash": 4284893193, "value": 66, "maximumValue": 100 }, { "statHash": 4043523819, "value": 55, "maximumValue": 100 }, { "statHash": 1240592695, "value": 18, "maximumValue": 100 }, { "statHash": 155624089, "value": 50, "maximumValue": 100 }, { "statHash": 4188031367, "value": 46, "maximumValue": 100 }, { "statHash": 3871231066, "value": 39, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 286, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 19299, "level": 16, "step": 0, "progressToNextLevel": 238, "nextLevelAt": 1285, "progressionHash": 1296042811 }, "talentGridHash": 563165056, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/8c6ec4c4973010c791cfa28f0fe08d69.png", "perkHash": 3523239750, "isActive": true }, { "iconPath": "/common/destiny_content/icons/a5b3881006d498c6936a0555c4ddf056.png", "perkHash": 318657130, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1177550374, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529084835781333", "itemLevel": 48, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 73, "maximumValue": 100 }, { "statHash": 4043523819, "value": 7, "maximumValue": 100 }, { "statHash": 1240592695, "value": 39, "maximumValue": 100 }, { "statHash": 155624089, "value": 72, "maximumValue": 100 }, { "statHash": 4188031367, "value": 65, "maximumValue": 100 }, { "statHash": 3871231066, "value": 24, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 280, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 919048791, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 3, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 24990, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4284, "progressionHash": 3025482627 }, "talentGridHash": 932175511, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": { "3": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/9d1f2ccd0549b0d8502e82b253cf293a.png", "perkHash": 164606820, "isActive": true }, { "iconPath": "/common/destiny_content/icons/c5fabd06404f049071ee98ad20705dbf.png", "perkHash": 2814215427, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2878293129, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529130710143648", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 41, "maximumValue": 100 }, { "statHash": 1240592695, "value": 26, "maximumValue": 100 }, { "statHash": 155624089, "value": 27, "maximumValue": 100 }, { "statHash": 4188031367, "value": 43, "maximumValue": 100 }, { "statHash": 3871231066, "value": 40, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 4, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 1156912330 }, "talentGridHash": 465289219, "nodes": [ { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": false }, { "iconPath": "/common/destiny_content/icons/507b0ea0b465e8520d3e16df26d08035.png", "perkHash": 3283108368, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2790109143, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529076375857712", "itemLevel": 48, "stackSize": 1, "qualityLevel": 6, "stats": [ { "statHash": 4284893193, "value": 73, "maximumValue": 100 }, { "statHash": 4043523819, "value": 7, "maximumValue": 100 }, { "statHash": 1240592695, "value": 22, "maximumValue": 100 }, { "statHash": 155624089, "value": 75, "maximumValue": 100 }, { "statHash": 4188031367, "value": 90, "maximumValue": 100 }, { "statHash": 3871231066, "value": 27, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 286, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 919048791, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 4231690432 }, "talentGridHash": 529240916, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 1, "5": 0, "21": 1 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4a7731eac3630914e5a4dde7929f7213.png", "perkHash": 1688301903, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 4113238754, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129582752286", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 2961396640, "value": 40, "maximumValue": 100 }, { "statHash": 4043523819, "value": 35, "maximumValue": 100 }, { "statHash": 1240592695, "value": 49, "maximumValue": 100 }, { "statHash": 155624089, "value": 79, "maximumValue": 100 }, { "statHash": 4188031367, "value": 72, "maximumValue": 100 }, { "statHash": 3871231066, "value": 32, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 2, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 13994, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 2399, "progressionHash": 751343210 }, "talentGridHash": 3231567393, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/a180884886739b34b824ff9afed386fb.png", "perkHash": 2450150110, "isActive": true }, { "iconPath": "/common/destiny_content/icons/e1f8726fd86727f2bde3d92ea72007b0.png", "perkHash": 1863078623, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1026578963, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114986884808", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 42, "maximumValue": 100 }, { "statHash": 4043523819, "value": 38, "maximumValue": 100 }, { "statHash": 1240592695, "value": 60, "maximumValue": 100 }, { "statHash": 155624089, "value": 68, "maximumValue": 100 }, { "statHash": 4188031367, "value": 76, "maximumValue": 100 }, { "statHash": 3871231066, "value": 15, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 41084, "level": 10, "step": 0, "progressToNextLevel": 3242, "nextLevelAt": 4284, "progressionHash": 1635066761 }, "talentGridHash": 277602097, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 1, "21": 1 }, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/e1f8726fd86727f2bde3d92ea72007b0.png", "perkHash": 1863078623, "isActive": true }, { "iconPath": "/common/destiny_content/icons/fa74e3c5a127176e77777257f7a61363.png", "perkHash": 3911170550, "isActive": false }, { "iconPath": "/common/destiny_content/icons/39cc45284689e06dfa03465dd40dca41.png", "perkHash": 3245550306, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3615265777, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529046886859936", "itemLevel": 24, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 35, "maximumValue": 100 }, { "statHash": 4043523819, "value": 42, "maximumValue": 100 }, { "statHash": 1240592695, "value": 17, "maximumValue": 100 }, { "statHash": 155624089, "value": 25, "maximumValue": 100 }, { "statHash": 4188031367, "value": 53, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 170, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 1, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 146908, "level": 62, "step": 0, "progressToNextLevel": 969, "nextLevelAt": 2399, "progressionHash": 2572834290 }, "talentGridHash": 1494260892, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 15 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/507b0ea0b465e8520d3e16df26d08035.png", "perkHash": 3283108368, "isActive": true }, { "iconPath": "/common/destiny_content/icons/0439ab899771712db69d11d23765eb51.png", "perkHash": 3752206822, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 958238921, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118366319312", "itemLevel": 59, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 2961396640, "value": 34, "maximumValue": 100 }, { "statHash": 4043523819, "value": 76, "maximumValue": 100 }, { "statHash": 1240592695, "value": 45, "maximumValue": 100 }, { "statHash": 155624089, "value": 38, "maximumValue": 100 }, { "statHash": 4188031367, "value": 82, "maximumValue": 100 }, { "statHash": 3871231066, "value": 4, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 395, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 2, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 875, "level": 1, "step": 0, "progressToNextLevel": 875, "nextLevelAt": 1999, "progressionHash": 2572834290 }, "talentGridHash": 1411705539, "nodes": [ { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/af9fb9e79767496f21c690f35547d4ea.png", "perkHash": 2421244048, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1033894158, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529130708865978", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 23, "maximumValue": 100 }, { "statHash": 4043523819, "value": 52, "maximumValue": 100 }, { "statHash": 1240592695, "value": 18, "maximumValue": 100 }, { "statHash": 155624089, "value": 34, "maximumValue": 100 }, { "statHash": 4188031367, "value": 39, "maximumValue": 100 }, { "statHash": 3871231066, "value": 5, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 2572834290 }, "talentGridHash": 1169913555, "nodes": [ { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0439ab899771712db69d11d23765eb51.png", "perkHash": 3752206822, "isActive": false }, { "iconPath": "/common/destiny_content/icons/07d2a002cdd4518759a5ab61112eacfe.png", "perkHash": 2725595956, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 791351720, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129068058420", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 39, "maximumValue": 100 }, { "statHash": 1240592695, "value": 30, "maximumValue": 100 }, { "statHash": 155624089, "value": 36, "maximumValue": 100 }, { "statHash": 4188031367, "value": 34, "maximumValue": 100 }, { "statHash": 3871231066, "value": 43, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1296042811 }, "talentGridHash": 563165056, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dd5cfef4d7122c56172c8f3cba5d5498.png", "perkHash": 192233393, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3142762099, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529112176823318", "itemLevel": 50, "stackSize": 1, "qualityLevel": 66, "stats": [ { "statHash": 4284893193, "value": 27, "maximumValue": 100 }, { "statHash": 4043523819, "value": 61, "maximumValue": 100 }, { "statHash": 1240592695, "value": 77, "maximumValue": 100 }, { "statHash": 155624089, "value": 52, "maximumValue": 100 }, { "statHash": 4188031367, "value": 57, "maximumValue": 100 }, { "statHash": 3871231066, "value": 14, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 366, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 812, "level": 1, "step": 0, "progressToNextLevel": 812, "nextLevelAt": 3570, "progressionHash": 1635066761 }, "talentGridHash": 2073748644, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 7, "5": 0, "21": 7 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/507b0ea0b465e8520d3e16df26d08035.png", "perkHash": 3283108368, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8ee959d90343fabd4679c6848790d76d.png", "perkHash": 1026458383, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 152628833, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529038698902325", "itemLevel": 20, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 88, "maximumValue": 100 }, { "statHash": 4043523819, "value": 27, "maximumValue": 100 }, { "statHash": 1240592695, "value": 18, "maximumValue": 100 }, { "statHash": 155624089, "value": 60, "maximumValue": 100 }, { "statHash": 4188031367, "value": 39, "maximumValue": 100 }, { "statHash": 3871231066, "value": 58, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 150, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 1, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 143550, "level": 112, "step": 0, "progressToNextLevel": 1129, "nextLevelAt": 1285, "progressionHash": 1296042811 }, "talentGridHash": 3782993714, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/009abe065e2385494dbea5f98d19818f.png", "perkHash": 3317503477, "isActive": true }, { "iconPath": "/common/destiny_content/icons/cc523b97c7521e88a35463604a128bfa.png", "perkHash": 808857588, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3836861003, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129595049756", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 37, "maximumValue": 100 }, { "statHash": 4043523819, "value": 16, "maximumValue": 100 }, { "statHash": 1240592695, "value": 82, "maximumValue": 100 }, { "statHash": 155624089, "value": 57, "maximumValue": 100 }, { "statHash": 4188031367, "value": 71, "maximumValue": 100 }, { "statHash": 3871231066, "value": 4, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 1156912330 }, "talentGridHash": 3073573921, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 1, "5": 0, "21": 1 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e0fbfaa73de5378fdf11a92827af751b.png", "perkHash": 4023922623, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dbdf5aa087d3c511169748b483e8b5aa.png", "perkHash": 2626112550, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dd5cfef4d7122c56172c8f3cba5d5498.png", "perkHash": 192233393, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3768542598, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118258693618", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 26, "maximumValue": 100 }, { "statHash": 4043523819, "value": 24, "maximumValue": 100 }, { "statHash": 1240592695, "value": 80, "maximumValue": 100 }, { "statHash": 155624089, "value": 57, "maximumValue": 100 }, { "statHash": 4188031367, "value": 61, "maximumValue": 100 }, { "statHash": 3871231066, "value": 4, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 4, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 24063, "level": 11, "step": 0, "progressToNextLevel": 473, "nextLevelAt": 2399, "progressionHash": 1156912330 }, "talentGridHash": 7269682, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "21": 0 }, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": true }, { "iconPath": "/common/destiny_content/icons/2e9c80070ae6fb6b14c5a23d7c130f79.png", "perkHash": 1900693659, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2609120348, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529037838039818", "itemLevel": 22, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 2961396640, "value": 31, "maximumValue": 100 }, { "statHash": 4043523819, "value": 76, "maximumValue": 100 }, { "statHash": 1240592695, "value": 30, "maximumValue": 100 }, { "statHash": 155624089, "value": 46, "maximumValue": 100 }, { "statHash": 4188031367, "value": 86, "maximumValue": 100 }, { "statHash": 3871231066, "value": 7, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 160, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 15, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 142500, "level": 60, "step": 0, "progressToNextLevel": 1359, "nextLevelAt": 2399, "progressionHash": 3605101483 }, "talentGridHash": 726179907, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b4273fbfb031bb47b7877a103e07d1a2.png", "perkHash": 3787917923, "isActive": true }, { "iconPath": "/common/destiny_content/icons/507b0ea0b465e8520d3e16df26d08035.png", "perkHash": 3283108368, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3820351995, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119623575122", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 52, "maximumValue": 100 }, { "statHash": 4043523819, "value": 37, "maximumValue": 100 }, { "statHash": 1240592695, "value": 59, "maximumValue": 100 }, { "statHash": 155624089, "value": 48, "maximumValue": 100 }, { "statHash": 4188031367, "value": 65, "maximumValue": 100 }, { "statHash": 3871231066, "value": 21, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 1635066761 }, "talentGridHash": 2073748644, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 6, "5": 0, "21": 6 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dd2d14b064d5fa15d0bb6ae38f3567de.png", "perkHash": 3722223157, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2999797736, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529122763022023", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 4, "maximumValue": 100 }, { "statHash": 1240592695, "value": 32, "maximumValue": 100 }, { "statHash": 155624089, "value": 66, "maximumValue": 100 }, { "statHash": 4188031367, "value": 59, "maximumValue": 100 }, { "statHash": 3871231066, "value": 21, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 919048791, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 4231690432 }, "talentGridHash": 1224728520, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 1, "21": 1 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4a7731eac3630914e5a4dde7929f7213.png", "perkHash": 1688301903, "isActive": false }, { "iconPath": "/common/destiny_content/icons/fe7bb77174e0df000cdc9677e0b1a9bc.png", "perkHash": 4198986151, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 602786655, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074619006009", "itemLevel": 56, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 4284893193, "value": 66, "maximumValue": 100 }, { "statHash": 4043523819, "value": 57, "maximumValue": 100 }, { "statHash": 1240592695, "value": 10, "maximumValue": 100 }, { "statHash": 155624089, "value": 60, "maximumValue": 100 }, { "statHash": 4188031367, "value": 39, "maximumValue": 100 }, { "statHash": 3871231066, "value": 39, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 365, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 7496, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1285, "progressionHash": 1296042811 }, "talentGridHash": 563165056, "nodes": [ { "isActivated": true, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/cc523b97c7521e88a35463604a128bfa.png", "perkHash": 3133914358, "isActive": true }, { "iconPath": "/common/destiny_content/icons/a5b3881006d498c6936a0555c4ddf056.png", "perkHash": 318657130, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3863768569, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131781591765", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 91, "maximumValue": 100 }, { "statHash": 4043523819, "value": 15, "maximumValue": 100 }, { "statHash": 1240592695, "value": 33, "maximumValue": 100 }, { "statHash": 155624089, "value": 77, "maximumValue": 100 }, { "statHash": 4188031367, "value": 87, "maximumValue": 100 }, { "statHash": 3871231066, "value": 12, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4274081686, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 1241939069 }, "talentGridHash": 2516462505, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 3, "21": 3 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": false }, { "iconPath": "/common/destiny_content/icons/fe7bb77174e0df000cdc9677e0b1a9bc.png", "perkHash": 4198986151, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2781253736, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529068944372101", "itemLevel": 48, "stackSize": 1, "qualityLevel": 10, "stats": [ { "statHash": 4284893193, "value": 4, "maximumValue": 100 }, { "statHash": 3614673599, "value": 94, "maximumValue": 100 }, { "statHash": 2523465841, "value": 89, "maximumValue": 100 }, { "statHash": 155624089, "value": 35, "maximumValue": 100 }, { "statHash": 4188031367, "value": 51, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 290, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 9350, "level": 8, "step": 0, "progressToNextLevel": 569, "nextLevelAt": 1285, "progressionHash": 1054627247 }, "talentGridHash": 3092831195, "nodes": [ { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/52ee2d246caa7d71da2465fcaa00aa09.png", "perkHash": 2425591494, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1397524040, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529080569695801", "itemLevel": 51, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 18, "maximumValue": 100 }, { "statHash": 3614673599, "value": 94, "maximumValue": 100 }, { "statHash": 2523465841, "value": 56, "maximumValue": 100 }, { "statHash": 155624089, "value": 50, "maximumValue": 100 }, { "statHash": 4188031367, "value": 38, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 310, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 9809, "level": 8, "step": 0, "progressToNextLevel": 1028, "nextLevelAt": 1285, "progressionHash": 1054627247 }, "talentGridHash": 3520571751, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/7fec2a9c052b7180bec71dffc761aadb.png", "perkHash": 2179933840, "isActive": true }, { "iconPath": "/common/destiny_content/icons/75216c1283bcb3a0f1fc6675f44d287f.png", "perkHash": 1966098145, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3491886958, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529075148408445", "itemLevel": 48, "stackSize": 1, "qualityLevel": 6, "stats": [ { "statHash": 4284893193, "value": 52, "maximumValue": 100 }, { "statHash": 4043523819, "value": 35, "maximumValue": 100 }, { "statHash": 1240592695, "value": 75, "maximumValue": 100 }, { "statHash": 155624089, "value": 63, "maximumValue": 100 }, { "statHash": 4188031367, "value": 48, "maximumValue": 100 }, { "statHash": 3871231066, "value": 18, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 286, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 32245, "level": 8, "step": 0, "progressToNextLevel": 2971, "nextLevelAt": 4284, "progressionHash": 1635066761 }, "talentGridHash": 1415170625, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 14, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 4, "5": 0, "21": 4 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/39cc45284689e06dfa03465dd40dca41.png", "perkHash": 3245550306, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2346956472, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529044242435023", "itemLevel": 20, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 42, "maximumValue": 100 }, { "statHash": 4043523819, "value": 38, "maximumValue": 100 }, { "statHash": 1240592695, "value": 59, "maximumValue": 100 }, { "statHash": 155624089, "value": 52, "maximumValue": 100 }, { "statHash": 4188031367, "value": 83, "maximumValue": 100 }, { "statHash": 3871231066, "value": 12, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 130, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 6582, "level": 2, "step": 0, "progressToNextLevel": 3182, "nextLevelAt": 4080, "progressionHash": 601852144 }, "talentGridHash": 1470517783, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 } ], "useCustomDyes": true, "artRegions": { "3": 10, "5": 0, "21": 10 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/1e8251bf938ca20e2323ba2e3e06804d.png", "perkHash": 1173182893, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1026578963, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131778709187", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 42, "maximumValue": 100 }, { "statHash": 4043523819, "value": 38, "maximumValue": 100 }, { "statHash": 1240592695, "value": 60, "maximumValue": 100 }, { "statHash": 155624089, "value": 51, "maximumValue": 100 }, { "statHash": 4188031367, "value": 76, "maximumValue": 100 }, { "statHash": 3871231066, "value": 15, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 5577, "level": 2, "step": 0, "progressToNextLevel": 2007, "nextLevelAt": 4284, "progressionHash": 1635066761 }, "talentGridHash": 277602097, "nodes": [ { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 1, "21": 1 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/8ee959d90343fabd4679c6848790d76d.png", "perkHash": 1026458383, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a180884886739b34b824ff9afed386fb.png", "perkHash": 2450150110, "isActive": false }, { "iconPath": "/common/destiny_content/icons/39cc45284689e06dfa03465dd40dca41.png", "perkHash": 3245550306, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3695068318, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529042783813899", "itemLevel": 20, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 37, "maximumValue": 100 }, { "statHash": 4043523819, "value": 16, "maximumValue": 100 }, { "statHash": 1240592695, "value": 68, "maximumValue": 100 }, { "statHash": 155624089, "value": 96, "maximumValue": 100 }, { "statHash": 4188031367, "value": 75, "maximumValue": 100 }, { "statHash": 3871231066, "value": 4, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 150, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 1, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 142500, "level": 60, "step": 0, "progressToNextLevel": 1359, "nextLevelAt": 2399, "progressionHash": 1156912330 }, "talentGridHash": 3674910527, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": { "3": 2, "5": 0, "21": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b1d6a5c5769303d22872631148c016bd.png", "perkHash": 731752060, "isActive": true }, { "iconPath": "/common/destiny_content/icons/fa74e3c5a127176e77777257f7a61363.png", "perkHash": 3911170550, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2121113047, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074251398078", "itemLevel": 49, "stackSize": 1, "qualityLevel": 1, "stats": [ { "statHash": 4284893193, "value": 66, "maximumValue": 100 }, { "statHash": 4043523819, "value": 53, "maximumValue": 100 }, { "statHash": 1240592695, "value": 10, "maximumValue": 100 }, { "statHash": 155624089, "value": 72, "maximumValue": 100 }, { "statHash": 4188031367, "value": 26, "maximumValue": 100 }, { "statHash": 3871231066, "value": 35, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 291, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 11749, "level": 10, "step": 0, "progressToNextLevel": 398, "nextLevelAt": 1285, "progressionHash": 1296042811 }, "talentGridHash": 563165056, "nodes": [ { "isActivated": false, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a180884886739b34b824ff9afed386fb.png", "perkHash": 2450150110, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dd5cfef4d7122c56172c8f3cba5d5498.png", "perkHash": 192233393, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2999797736, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118267195430", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 4, "maximumValue": 100 }, { "statHash": 1240592695, "value": 34, "maximumValue": 100 }, { "statHash": 155624089, "value": 94, "maximumValue": 100 }, { "statHash": 4188031367, "value": 59, "maximumValue": 100 }, { "statHash": 3871231066, "value": 15, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 919048791, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 45281, "level": 11, "step": 0, "progressToNextLevel": 3155, "nextLevelAt": 4284, "progressionHash": 4231690432 }, "talentGridHash": 1224728520, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 1, "21": 1 }, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": true }, { "iconPath": "/common/destiny_content/icons/8ee959d90343fabd4679c6848790d76d.png", "perkHash": 1026458383, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1435154085, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529033164836988", "itemLevel": 20, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 2961396640, "value": 28, "maximumValue": 100 }, { "statHash": 4043523819, "value": 81, "maximumValue": 100 }, { "statHash": 1240592695, "value": 44, "maximumValue": 100 }, { "statHash": 155624089, "value": 52, "maximumValue": 100 }, { "statHash": 4188031367, "value": 73, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 138, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 142500, "level": 60, "step": 0, "progressToNextLevel": 1359, "nextLevelAt": 2399, "progressionHash": 3605101483 }, "talentGridHash": 388623345, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 } ], "useCustomDyes": true, "artRegions": { "3": 7, "5": 0, "21": 7 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/1c12394789f1a4dcd5119042506e0b95.png", "perkHash": 1029160485, "isActive": true }, { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1784034858, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529116732092076", "itemLevel": 56, "stackSize": 1, "qualityLevel": 25, "stats": [ { "statHash": 4284893193, "value": 11, "maximumValue": 100 }, { "statHash": 3614673599, "value": 66, "maximumValue": 100 }, { "statHash": 2523465841, "value": 78, "maximumValue": 100 }, { "statHash": 155624089, "value": 53, "maximumValue": 100 }, { "statHash": 4188031367, "value": 72, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 11, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1054627247 }, "talentGridHash": 2724833726, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "1": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/dd5cfef4d7122c56172c8f3cba5d5498.png", "perkHash": 192233393, "isActive": false }, { "iconPath": "/common/destiny_content/icons/4034d11482a8963b7e4e7d792cdd62ce.png", "perkHash": 888309016, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1439896155, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131781591894", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 18, "maximumValue": 100 }, { "statHash": 3614673599, "value": 96, "maximumValue": 100 }, { "statHash": 2523465841, "value": 37, "maximumValue": 100 }, { "statHash": 155624089, "value": 74, "maximumValue": 100 }, { "statHash": 4188031367, "value": 50, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 7, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1054627247 }, "talentGridHash": 1823071726, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/52ee2d246caa7d71da2465fcaa00aa09.png", "perkHash": 2425591494, "isActive": false }, { "iconPath": "/common/destiny_content/icons/b6aa44fdd61ba9212754ac9d8fdb5baf.png", "perkHash": 2784842908, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 601780095, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529091616172038", "itemLevel": 50, "stackSize": 1, "qualityLevel": 32, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 28, "maximumValue": 100 }, { "statHash": 1240592695, "value": 28, "maximumValue": 100 }, { "statHash": 155624089, "value": 49, "maximumValue": 100 }, { "statHash": 4188031367, "value": 66, "maximumValue": 100 }, { "statHash": 3871231066, "value": 26, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 332, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 607, "weeklyProgress": 607, "currentProgress": 5618, "level": 2, "step": 0, "progressToNextLevel": 2048, "nextLevelAt": 4284, "progressionHash": 2958601622 }, "talentGridHash": 1173596555, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 6, "5": 0, "21": 6 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": false }, { "iconPath": "/common/destiny_content/icons/507b0ea0b465e8520d3e16df26d08035.png", "perkHash": 3283108368, "isActive": false }, { "iconPath": "/common/destiny_content/icons/683d97a4f1b92e6d43f9f8916a84dc6c.png", "perkHash": 1694891218, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1177550375, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529081023143176", "itemLevel": 51, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 66, "maximumValue": 100 }, { "statHash": 4043523819, "value": 15, "maximumValue": 100 }, { "statHash": 1240592695, "value": 38, "maximumValue": 100 }, { "statHash": 155624089, "value": 44, "maximumValue": 100 }, { "statHash": 4188031367, "value": 76, "maximumValue": 100 }, { "statHash": 3871231066, "value": 27, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 310, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 919048791, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 1, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 25392, "level": 7, "step": 0, "progressToNextLevel": 402, "nextLevelAt": 4284, "progressionHash": 3025482627 }, "talentGridHash": 932175510, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 15 } ], "useCustomDyes": true, "artRegions": { "3": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b87a1f255c216eeaf13b278a06288c29.png", "perkHash": 2417835318, "isActive": true }, { "iconPath": "/common/destiny_content/icons/dbdf5aa087d3c511169748b483e8b5aa.png", "perkHash": 2626112550, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2125403517, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529110235939774", "itemLevel": 58, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 4284893193, "value": 8, "maximumValue": 100 }, { "statHash": 4043523819, "value": 68, "maximumValue": 100 }, { "statHash": 1240592695, "value": 21, "maximumValue": 100 }, { "statHash": 155624089, "value": 17, "maximumValue": 100 }, { "statHash": 4188031367, "value": 33, "maximumValue": 100 }, { "statHash": 3871231066, "value": 5, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 11, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 2192, "level": 2, "step": 0, "progressToNextLevel": 193, "nextLevelAt": 2399, "progressionHash": 2572834290 }, "talentGridHash": 563501471, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "1": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/07d2a002cdd4518759a5ab61112eacfe.png", "perkHash": 2725595956, "isActive": false }, { "iconPath": "/common/destiny_content/icons/32de89d64c2db519325cd716ed2ad94c.png", "perkHash": 3256310621, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1603229152, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529046942569132", "itemLevel": 20, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 4, "maximumValue": 100 }, { "statHash": 1240592695, "value": 26, "maximumValue": 100 }, { "statHash": 155624089, "value": 73, "maximumValue": 100 }, { "statHash": 4188031367, "value": 54, "maximumValue": 100 }, { "statHash": 3871231066, "value": 30, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 130, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 919048791, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 20674, "level": 5, "step": 0, "progressToNextLevel": 4252, "nextLevelAt": 4284, "progressionHash": 4231690432 }, "talentGridHash": 1027210955, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": { "3": 5, "5": 0, "21": 5 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/c2257672c6adea5bea5926d2f67c4d0a.png", "perkHash": 661681055, "isActive": true }, { "iconPath": "/common/destiny_content/icons/b1b8ae04e5245854994939a6f6abd23a.png", "perkHash": 1600125496, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1387145760, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131102466358", "itemLevel": 50, "stackSize": 1, "qualityLevel": 96, "stats": [ { "statHash": 4284893193, "value": 12, "maximumValue": 100 }, { "statHash": 4043523819, "value": 38, "maximumValue": 100 }, { "statHash": 1240592695, "value": 88, "maximumValue": 100 }, { "statHash": 155624089, "value": 31, "maximumValue": 100 }, { "statHash": 4188031367, "value": 58, "maximumValue": 100 }, { "statHash": 3871231066, "value": 3, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 396, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 6, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 440, "weeklyProgress": 440, "currentProgress": 440, "level": 1, "step": 0, "progressToNextLevel": 440, "nextLevelAt": 1999, "progressionHash": 1156912330 }, "talentGridHash": 1387145760, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "5": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fa74e3c5a127176e77777257f7a61363.png", "perkHash": 3911170550, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8ee959d90343fabd4679c6848790d76d.png", "perkHash": 1026458383, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3012398148, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529075115922011", "itemLevel": 58, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 2961396640, "value": 37, "maximumValue": 100 }, { "statHash": 4043523819, "value": 71, "maximumValue": 100 }, { "statHash": 1240592695, "value": 30, "maximumValue": 100 }, { "statHash": 155624089, "value": 93, "maximumValue": 100 }, { "statHash": 4188031367, "value": 79, "maximumValue": 100 }, { "statHash": 3871231066, "value": 5, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 385, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 14725, "level": 7, "step": 0, "progressToNextLevel": 731, "nextLevelAt": 2399, "progressionHash": 751343210 }, "talentGridHash": 667168543, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/e95c714f44e5d0e3a923f495d105e89a.png", "perkHash": 3582449302, "isActive": true }, { "iconPath": "/common/destiny_content/icons/c155d0d9f64ddf0844430ba8e31e078b.png", "perkHash": 1651022462, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3863768569, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529128585728802", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 4284893193, "value": 91, "maximumValue": 100 }, { "statHash": 4043523819, "value": 15, "maximumValue": 100 }, { "statHash": 1240592695, "value": 33, "maximumValue": 100 }, { "statHash": 155624089, "value": 76, "maximumValue": 100 }, { "statHash": 4188031367, "value": 87, "maximumValue": 100 }, { "statHash": 3871231066, "value": 12, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4274081686, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 1241939069 }, "talentGridHash": 2516462505, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a180884886739b34b824ff9afed386fb.png", "perkHash": 2450150110, "isActive": false }, { "iconPath": "/common/destiny_content/icons/fe7bb77174e0df000cdc9677e0b1a9bc.png", "perkHash": 4198986151, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2835863910, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114229446544", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 14, "maximumValue": 100 }, { "statHash": 4043523819, "value": 60, "maximumValue": 100 }, { "statHash": 1240592695, "value": 31, "maximumValue": 100 }, { "statHash": 155624089, "value": 48, "maximumValue": 100 }, { "statHash": 4188031367, "value": 26, "maximumValue": 100 }, { "statHash": 3871231066, "value": 4, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 6, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 17930, "level": 8, "step": 0, "progressToNextLevel": 1537, "nextLevelAt": 2399, "progressionHash": 2572834290 }, "talentGridHash": 345792935, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 2290550941, "isActive": true }, { "iconPath": "/common/destiny_content/icons/5cfab37355e9e524d235b5222ab8cea0.png", "perkHash": 2201881340, "isActive": true }, { "iconPath": "/common/destiny_content/icons/2e9c80070ae6fb6b14c5a23d7c130f79.png", "perkHash": 1900693659, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1994742696, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529121097348173", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 35, "maximumValue": 100 }, { "statHash": 4043523819, "value": 40, "maximumValue": 100 }, { "statHash": 1240592695, "value": 14, "maximumValue": 100 }, { "statHash": 155624089, "value": 36, "maximumValue": 100 }, { "statHash": 4188031367, "value": 46, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 2572834290 }, "talentGridHash": 1169913555, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/694ac158025659b958f93a4de0f40d6b.png", "perkHash": 3102938162, "isActive": false }, { "iconPath": "/common/destiny_content/icons/07d2a002cdd4518759a5ab61112eacfe.png", "perkHash": 2725595956, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 330048677, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132701179350", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 11, "maximumValue": 100 }, { "statHash": 3614673599, "value": 72, "maximumValue": 100 }, { "statHash": 2523465841, "value": 76, "maximumValue": 100 }, { "statHash": 155624089, "value": 55, "maximumValue": 100 }, { "statHash": 4188031367, "value": 72, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 7, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 230, "level": 1, "step": 0, "progressToNextLevel": 230, "nextLevelAt": 1071, "progressionHash": 1054627247 }, "talentGridHash": 3334322743, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/52ee2d246caa7d71da2465fcaa00aa09.png", "perkHash": 2425591494, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dd5cfef4d7122c56172c8f3cba5d5498.png", "perkHash": 192233393, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2361858758, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529043361091888", "itemLevel": 22, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 11, "maximumValue": 100 }, { "statHash": 3614673599, "value": 68, "maximumValue": 100 }, { "statHash": 2523465841, "value": 90, "maximumValue": 100 }, { "statHash": 155624089, "value": 58, "maximumValue": 100 }, { "statHash": 4188031367, "value": 71, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 160, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 144914, "level": 113, "step": 0, "progressToNextLevel": 1208, "nextLevelAt": 1285, "progressionHash": 1054627247 }, "talentGridHash": 4002156753, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/7fec2a9c052b7180bec71dffc761aadb.png", "perkHash": 2179933840, "isActive": true }, { "iconPath": "/common/destiny_content/icons/31ab63192f9a1d1c2251a4d352919797.png", "perkHash": 1928591453, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 457366421, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129451332849", "itemLevel": 56, "stackSize": 1, "qualityLevel": 40, "stats": [ { "statHash": 4284893193, "value": 14, "maximumValue": 100 }, { "statHash": 4043523819, "value": 62, "maximumValue": 100 }, { "statHash": 1240592695, "value": 14, "maximumValue": 100 }, { "statHash": 155624089, "value": 35, "maximumValue": 100 }, { "statHash": 4188031367, "value": 20, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 2572834290 }, "talentGridHash": 722027546, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e5b2509e4d6104046b8e982e8ca4c3ba.png", "perkHash": 2047535886, "isActive": false }, { "iconPath": "/common/destiny_content/icons/75216c1283bcb3a0f1fc6675f44d287f.png", "perkHash": 1966098145, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3322979578, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131769992509", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 4284893193, "value": 100, "maximumValue": 100 }, { "statHash": 4043523819, "value": 27, "maximumValue": 100 }, { "statHash": 1240592695, "value": 12, "maximumValue": 100 }, { "statHash": 155624089, "value": 21, "maximumValue": 100 }, { "statHash": 4188031367, "value": 37, "maximumValue": 100 }, { "statHash": 3871231066, "value": 30, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1296042811 }, "talentGridHash": 1226235350, "nodes": [ { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 8, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 8, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4a7731eac3630914e5a4dde7929f7213.png", "perkHash": 1688301903, "isActive": false }, { "iconPath": "/common/destiny_content/icons/b1d6a5c5769303d22872631148c016bd.png", "perkHash": 731752060, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2512322824, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129001384192", "itemLevel": 56, "stackSize": 1, "qualityLevel": 40, "stats": [ { "statHash": 4284893193, "value": 100, "maximumValue": 100 }, { "statHash": 4043523819, "value": 2, "maximumValue": 100 }, { "statHash": 1240592695, "value": 18, "maximumValue": 100 }, { "statHash": 155624089, "value": 60, "maximumValue": 100 }, { "statHash": 4188031367, "value": 71, "maximumValue": 100 }, { "statHash": 3871231066, "value": 60, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 9, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 25735, "level": 7, "step": 0, "progressToNextLevel": 745, "nextLevelAt": 4284, "progressionHash": 2657402687 }, "talentGridHash": 790292287, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 6, "5": 0, "21": 6 }, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/9269b29ce2bf96cf2a5cfba9d77c1719.png", "perkHash": 3119194123, "isActive": true }, { "iconPath": "/common/destiny_content/icons/cc523b97c7521e88a35463604a128bfa.png", "perkHash": 808857588, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 4100639363, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529111640989194", "itemLevel": 56, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 2837207746, "value": 45, "maximumValue": 100 }, { "statHash": 4043523819, "value": 55, "maximumValue": 100 }, { "statHash": 1240592695, "value": 25, "maximumValue": 100 }, { "statHash": 2762071195, "value": 75, "maximumValue": 100 }, { "statHash": 209426660, "value": 50, "maximumValue": 100 }, { "statHash": 925767036, "value": 76, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 365, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1299354384, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 14868, "level": 11, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1512, "progressionHash": 2205945287 }, "talentGridHash": 3873616219, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/f1da75597652d706b0973d22943b92af.png", "perkHash": 2560578688, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 99462853, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119464673891", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 23, "maximumValue": 100 }, { "statHash": 4043523819, "value": 40, "maximumValue": 100 }, { "statHash": 1240592695, "value": 23, "maximumValue": 100 }, { "statHash": 155624089, "value": 36, "maximumValue": 100 }, { "statHash": 4188031367, "value": 16, "maximumValue": 100 }, { "statHash": 3871231066, "value": 3, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 1, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 9479, "level": 5, "step": 0, "progressToNextLevel": 283, "nextLevelAt": 2399, "progressionHash": 953899265 }, "talentGridHash": 736075600, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/860c99bfc022c89145f6b51e58492e31.png", "perkHash": 545530572, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a180884886739b34b824ff9afed386fb.png", "perkHash": 2450150110, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3322979578, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131775898453", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 4284893193, "value": 100, "maximumValue": 100 }, { "statHash": 4043523819, "value": 27, "maximumValue": 100 }, { "statHash": 1240592695, "value": 12, "maximumValue": 100 }, { "statHash": 155624089, "value": 21, "maximumValue": 100 }, { "statHash": 4188031367, "value": 37, "maximumValue": 100 }, { "statHash": 3871231066, "value": 30, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1296042811 }, "talentGridHash": 1226235350, "nodes": [ { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a5b3881006d498c6936a0555c4ddf056.png", "perkHash": 318657130, "isActive": false }, { "iconPath": "/common/destiny_content/icons/2e9c80070ae6fb6b14c5a23d7c130f79.png", "perkHash": 1900693659, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1456315384, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129574376878", "itemLevel": 56, "stackSize": 1, "qualityLevel": 40, "stats": [ { "statHash": 4284893193, "value": 22, "maximumValue": 100 }, { "statHash": 4043523819, "value": 81, "maximumValue": 100 }, { "statHash": 1240592695, "value": 40, "maximumValue": 100 }, { "statHash": 155624089, "value": 38, "maximumValue": 100 }, { "statHash": 4188031367, "value": 39, "maximumValue": 100 }, { "statHash": 3871231066, "value": 12, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2452843494, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 10, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 15259, "weeklyProgress": 15259, "currentProgress": 25175, "level": 7, "step": 0, "progressToNextLevel": 185, "nextLevelAt": 4284, "progressionHash": 1327609045 }, "talentGridHash": 327169823, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 1, "21": 1 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fe7bb77174e0df000cdc9677e0b1a9bc.png", "perkHash": 4198986151, "isActive": false }, { "iconPath": "/common/destiny_content/icons/fa74e3c5a127176e77777257f7a61363.png", "perkHash": 3911170550, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2443083323, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132834321537", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 2961396640, "value": 22, "maximumValue": 100 }, { "statHash": 4043523819, "value": 87, "maximumValue": 100 }, { "statHash": 1240592695, "value": 43, "maximumValue": 100 }, { "statHash": 155624089, "value": 54, "maximumValue": 100 }, { "statHash": 4188031367, "value": 74, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 3605101483 }, "talentGridHash": 1666012684, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 9, "5": 0, "21": 9 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/f189e4a2b69d9a964a5af0f089feeef0.png", "perkHash": 3661306029, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c5fabd06404f049071ee98ad20705dbf.png", "perkHash": 2814215427, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1689897198, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529105127925557", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 35, "maximumValue": 100 }, { "statHash": 4043523819, "value": 42, "maximumValue": 100 }, { "statHash": 1240592695, "value": 15, "maximumValue": 100 }, { "statHash": 155624089, "value": 33, "maximumValue": 100 }, { "statHash": 4188031367, "value": 53, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 2, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 2572834290 }, "talentGridHash": 3005191786, "nodes": [ { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a44df5aa8657a94fba4caea6aac5c93a.png", "perkHash": 1566222180, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 2290550941, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1346849289, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529075441936459", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 42, "maximumValue": 100 }, { "statHash": 4043523819, "value": 38, "maximumValue": 100 }, { "statHash": 1240592695, "value": 52, "maximumValue": 100 }, { "statHash": 155624089, "value": 59, "maximumValue": 100 }, { "statHash": 4188031367, "value": 100, "maximumValue": 100 }, { "statHash": 3871231066, "value": 21, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 1, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 51415, "level": 13, "step": 0, "progressToNextLevel": 721, "nextLevelAt": 4284, "progressionHash": 143810986 }, "talentGridHash": 1364148644, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 15 } ], "useCustomDyes": true, "artRegions": { "3": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/cb0717c3b0aab7869d68daddfe20108a.png", "perkHash": 2852658353, "isActive": true }, { "iconPath": "/common/destiny_content/icons/46fa6543967995eff68f32196e1a5a54.png", "perkHash": 770631416, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3275294461, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529072470795233", "itemLevel": 49, "stackSize": 1, "qualityLevel": 2, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 28, "maximumValue": 100 }, { "statHash": 1240592695, "value": 59, "maximumValue": 100 }, { "statHash": 155624089, "value": 58, "maximumValue": 100 }, { "statHash": 4188031367, "value": 45, "maximumValue": 100 }, { "statHash": 3871231066, "value": 44, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 292, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 26163, "level": 7, "step": 0, "progressToNextLevel": 1173, "nextLevelAt": 4284, "progressionHash": 2958601622 }, "talentGridHash": 4058565332, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 10, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 2, "1": 0, "0": 0, "5": 0, "4": 0, "21": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2447423793, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529078946352555", "itemLevel": 49, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 32, "maximumValue": 100 }, { "statHash": 4043523819, "value": 68, "maximumValue": 100 }, { "statHash": 1240592695, "value": 8, "maximumValue": 100 }, { "statHash": 155624089, "value": 51, "maximumValue": 100 }, { "statHash": 4188031367, "value": 56, "maximumValue": 100 }, { "statHash": 3871231066, "value": 8, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 290, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2452843494, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 2, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 25308, "level": 7, "step": 0, "progressToNextLevel": 318, "nextLevelAt": 4284, "progressionHash": 1336676114 }, "talentGridHash": 3800121296, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 13 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/bc76e0bacb7fca5eab90c17acadae847.png", "perkHash": 824209275, "isActive": true }, { "iconPath": "/common/destiny_content/icons/c5fabd06404f049071ee98ad20705dbf.png", "perkHash": 2814215427, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2168530918, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129649309799", "itemLevel": 10, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 100, "maximumValue": 100 }, { "statHash": 4043523819, "value": 2, "maximumValue": 100 }, { "statHash": 1240592695, "value": 13, "maximumValue": 100 }, { "statHash": 155624089, "value": 37, "maximumValue": 100 }, { "statHash": 4188031367, "value": 51, "maximumValue": 100 }, { "statHash": 3871231066, "value": 37, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 80, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 10, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 500, "progressionHash": 3063842802 }, "talentGridHash": 3807163129, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2816115592, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131781590068", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 18, "maximumValue": 100 }, { "statHash": 3614673599, "value": 100, "maximumValue": 100 }, { "statHash": 2523465841, "value": 35, "maximumValue": 100 }, { "statHash": 155624089, "value": 50, "maximumValue": 100 }, { "statHash": 4188031367, "value": 49, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1054627247 }, "talentGridHash": 3796398169, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/8295df6414e2f932c83f31b11490da6b.png", "perkHash": 3052118152, "isActive": false }, { "iconPath": "/common/destiny_content/icons/b6aa44fdd61ba9212754ac9d8fdb5baf.png", "perkHash": 2784842908, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1200540135, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131782155627", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 19, "maximumValue": 100 }, { "statHash": 4043523819, "value": 31, "maximumValue": 100 }, { "statHash": 1240592695, "value": 71, "maximumValue": 100 }, { "statHash": 155624089, "value": 60, "maximumValue": 100 }, { "statHash": 4188031367, "value": 65, "maximumValue": 100 }, { "statHash": 3871231066, "value": 4, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 6298, "weeklyProgress": 6298, "currentProgress": 6298, "level": 3, "step": 0, "progressToNextLevel": 1900, "nextLevelAt": 2399, "progressionHash": 1156912330 }, "talentGridHash": 303723075, "nodes": [ { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 6, "5": 0, "21": 6 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fe7bb77174e0df000cdc9677e0b1a9bc.png", "perkHash": 4198986151, "isActive": false }, { "iconPath": "/common/destiny_content/icons/0cff1d6da4575918c1d4597d2431a397.png", "perkHash": 3848963378, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 330048677, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529123392456445", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 11, "maximumValue": 100 }, { "statHash": 3614673599, "value": 78, "maximumValue": 100 }, { "statHash": 2523465841, "value": 85, "maximumValue": 100 }, { "statHash": 155624089, "value": 55, "maximumValue": 100 }, { "statHash": 4188031367, "value": 79, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 7, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 9935, "level": 8, "step": 0, "progressToNextLevel": 1154, "nextLevelAt": 1285, "progressionHash": 1054627247 }, "talentGridHash": 3334322743, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "21": 0 }, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/52ee2d246caa7d71da2465fcaa00aa09.png", "perkHash": 2425591494, "isActive": true }, { "iconPath": "/common/destiny_content/icons/dd5cfef4d7122c56172c8f3cba5d5498.png", "perkHash": 192233393, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1771861810, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133004061151", "itemLevel": 56, "stackSize": 1, "qualityLevel": 40, "stats": [ { "statHash": 4284893193, "value": 11, "maximumValue": 100 }, { "statHash": 3614673599, "value": 68, "maximumValue": 100 }, { "statHash": 2523465841, "value": 84, "maximumValue": 100 }, { "statHash": 155624089, "value": 58, "maximumValue": 100 }, { "statHash": 4188031367, "value": 71, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1054627247 }, "talentGridHash": 2688452277, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/7fec2a9c052b7180bec71dffc761aadb.png", "perkHash": 2179933840, "isActive": false }, { "iconPath": "/common/destiny_content/icons/31ab63192f9a1d1c2251a4d352919797.png", "perkHash": 1928591453, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3452625744, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529086587930588", "itemLevel": 48, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 88, "maximumValue": 100 }, { "statHash": 4043523819, "value": 8, "maximumValue": 100 }, { "statHash": 1240592695, "value": 23, "maximumValue": 100 }, { "statHash": 155624089, "value": 62, "maximumValue": 100 }, { "statHash": 4188031367, "value": 66, "maximumValue": 100 }, { "statHash": 3871231066, "value": 36, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 280, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 4680, "currentProgress": 4680, "level": 2, "step": 0, "progressToNextLevel": 1110, "nextLevelAt": 4284, "progressionHash": 2958601622 }, "talentGridHash": 3981554245, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 7, "5": 0, "21": 7 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": false }, { "iconPath": "/common/destiny_content/icons/4a7731eac3630914e5a4dde7929f7213.png", "perkHash": 1688301903, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2201079122, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529095514655294", "itemLevel": 51, "stackSize": 1, "qualityLevel": 24, "stats": [ { "statHash": 4284893193, "value": 14, "maximumValue": 100 }, { "statHash": 4043523819, "value": 62, "maximumValue": 100 }, { "statHash": 1240592695, "value": 14, "maximumValue": 100 }, { "statHash": 155624089, "value": 35, "maximumValue": 100 }, { "statHash": 4188031367, "value": 20, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 334, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 2295, "level": 2, "step": 0, "progressToNextLevel": 296, "nextLevelAt": 2399, "progressionHash": 2572834290 }, "talentGridHash": 722027546, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/2e9c80070ae6fb6b14c5a23d7c130f79.png", "perkHash": 1900693659, "isActive": false }, { "iconPath": "/common/destiny_content/icons/75216c1283bcb3a0f1fc6675f44d287f.png", "perkHash": 1966098145, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2639689178, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529087674213832", "itemLevel": 49, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 2961396640, "value": 32, "maximumValue": 100 }, { "statHash": 4043523819, "value": 76, "maximumValue": 100 }, { "statHash": 1240592695, "value": 31, "maximumValue": 100 }, { "statHash": 155624089, "value": 48, "maximumValue": 100 }, { "statHash": 4188031367, "value": 73, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 295, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 13994, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 2399, "progressionHash": 3605101483 }, "talentGridHash": 3682800992, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 6, "5": 0, "21": 6 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a5b3881006d498c6936a0555c4ddf056.png", "perkHash": 318657130, "isActive": false }, { "iconPath": "/common/destiny_content/icons/f189e4a2b69d9a964a5af0f089feeef0.png", "perkHash": 3661306029, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2901505496, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119346155874", "itemLevel": 50, "stackSize": 1, "qualityLevel": 90, "stats": [ { "statHash": 2961396640, "value": 34, "maximumValue": 100 }, { "statHash": 4043523819, "value": 74, "maximumValue": 100 }, { "statHash": 1240592695, "value": 44, "maximumValue": 100 }, { "statHash": 155624089, "value": 45, "maximumValue": 100 }, { "statHash": 4188031367, "value": 82, "maximumValue": 100 }, { "statHash": 3871231066, "value": 4, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 6, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 3605101483 }, "talentGridHash": 446728784, "nodes": [ { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 6, "5": 0, "21": 6 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/4a7731eac3630914e5a4dde7929f7213.png", "perkHash": 1688301903, "isActive": false }, { "iconPath": "/common/destiny_content/icons/c5fabd06404f049071ee98ad20705dbf.png", "perkHash": 2814215427, "isActive": false }, { "iconPath": "/common/destiny_content/icons/8c6ec4c4973010c791cfa28f0fe08d69.png", "perkHash": 3523239750, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2443083323, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529079945105630", "itemLevel": 48, "stackSize": 1, "qualityLevel": 6, "stats": [ { "statHash": 2961396640, "value": 22, "maximumValue": 100 }, { "statHash": 4043523819, "value": 87, "maximumValue": 100 }, { "statHash": 1240592695, "value": 60, "maximumValue": 100 }, { "statHash": 155624089, "value": 59, "maximumValue": 100 }, { "statHash": 4188031367, "value": 57, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 286, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 13994, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 2399, "progressionHash": 3605101483 }, "talentGridHash": 1666012684, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 7, "5": 0, "21": 7 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/af9fb9e79767496f21c690f35547d4ea.png", "perkHash": 2421244048, "isActive": true }, { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 791351720, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132838980804", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 41, "maximumValue": 100 }, { "statHash": 1240592695, "value": 22, "maximumValue": 100 }, { "statHash": 155624089, "value": 26, "maximumValue": 100 }, { "statHash": 4188031367, "value": 34, "maximumValue": 100 }, { "statHash": 3871231066, "value": 43, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1296042811 }, "talentGridHash": 563165056, "nodes": [ { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/2e9c80070ae6fb6b14c5a23d7c130f79.png", "perkHash": 1900693659, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7e4f0ca2fd172f21603f4ba43ba1262c.png", "perkHash": 3031234337, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3919765141, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529080567992214", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 26, "maximumValue": 100 }, { "statHash": 4043523819, "value": 24, "maximumValue": 100 }, { "statHash": 1240592695, "value": 58, "maximumValue": 100 }, { "statHash": 155624089, "value": 65, "maximumValue": 100 }, { "statHash": 4188031367, "value": 41, "maximumValue": 100 }, { "statHash": 3871231066, "value": 4, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 3, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 13994, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 2399, "progressionHash": 1156912330 }, "talentGridHash": 3607266306, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/1c12394789f1a4dcd5119042506e0b95.png", "perkHash": 2887187771, "isActive": true }, { "iconPath": "/common/destiny_content/icons/75216c1283bcb3a0f1fc6675f44d287f.png", "perkHash": 1966098145, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1287343925, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074610098750", "itemLevel": 49, "stackSize": 1, "qualityLevel": 3, "stats": [ { "statHash": 4284893193, "value": 11, "maximumValue": 100 }, { "statHash": 4043523819, "value": 66, "maximumValue": 100 }, { "statHash": 1240592695, "value": 32, "maximumValue": 100 }, { "statHash": 155624089, "value": 31, "maximumValue": 100 }, { "statHash": 4188031367, "value": 16, "maximumValue": 100 }, { "statHash": 3871231066, "value": 5, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 293, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1661276197, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 15269, "level": 7, "step": 0, "progressToNextLevel": 1275, "nextLevelAt": 2399, "progressionHash": 2572834290 }, "talentGridHash": 1169913555, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 8, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0439ab899771712db69d11d23765eb51.png", "perkHash": 3752206822, "isActive": true }, { "iconPath": "/common/destiny_content/icons/e5b2509e4d6104046b8e982e8ca4c3ba.png", "perkHash": 2047535886, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1205866943, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133007255067", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 66, "maximumValue": 100 }, { "statHash": 4043523819, "value": 14, "maximumValue": 100 }, { "statHash": 1240592695, "value": 36, "maximumValue": 100 }, { "statHash": 155624089, "value": 76, "maximumValue": 100 }, { "statHash": 4188031367, "value": 66, "maximumValue": 100 }, { "statHash": 3871231066, "value": 24, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 919048791, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 4231690432 }, "talentGridHash": 879677959, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 9, "5": 0, "21": 9 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b1d6a5c5769303d22872631148c016bd.png", "perkHash": 731752060, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": false }, { "iconPath": "/common/destiny_content/icons/683d97a4f1b92e6d43f9f8916a84dc6c.png", "perkHash": 1694891218, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 4100639364, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129954372151", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 2837207746, "value": 45, "maximumValue": 100 }, { "statHash": 4043523819, "value": 60, "maximumValue": 100 }, { "statHash": 1240592695, "value": 25, "maximumValue": 100 }, { "statHash": 2762071195, "value": 75, "maximumValue": 100 }, { "statHash": 209426660, "value": 50, "maximumValue": 100 }, { "statHash": 925767036, "value": 88, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1299354384, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 13158, "level": 9, "step": 0, "progressToNextLevel": 1314, "nextLevelAt": 1512, "progressionHash": 1806762204 }, "talentGridHash": 3889573986, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/f1da75597652d706b0973d22943b92af.png", "perkHash": 2560578688, "isActive": true }, { "iconPath": "/common/destiny_content/icons/385eaa4c1c87d542c4009532d77af5f7.png", "perkHash": 517718709, "isActive": false }, { "iconPath": "/common/destiny_content/icons/b14f26329f41bf7760776d7175a57862.png", "perkHash": 1392713960, "isActive": false }, { "iconPath": "/common/destiny_content/icons/4f33d8f77466ab14d6cba23893abd3a1.png", "perkHash": 1167703023, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2496861921, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133193613375", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 2961396640, "value": 22, "maximumValue": 100 }, { "statHash": 4043523819, "value": 87, "maximumValue": 100 }, { "statHash": 1240592695, "value": 35, "maximumValue": 100 }, { "statHash": 155624089, "value": 59, "maximumValue": 100 }, { "statHash": 4188031367, "value": 66, "maximumValue": 100 }, { "statHash": 3871231066, "value": 6, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 6, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 3605101483 }, "talentGridHash": 446728784, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 7, "5": 0, "21": 7 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": false }, { "iconPath": "/common/destiny_content/icons/07d2a002cdd4518759a5ab61112eacfe.png", "perkHash": 2725595956, "isActive": false }, { "iconPath": "/common/destiny_content/icons/d21f4f4ee36600d4f32394038d2fa57f.png", "perkHash": 972070082, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3068424913, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529092602380471", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 94, "maximumValue": 100 }, { "statHash": 4043523819, "value": 12, "maximumValue": 100 }, { "statHash": 1240592695, "value": 26, "maximumValue": 100 }, { "statHash": 155624089, "value": 76, "maximumValue": 100 }, { "statHash": 4188031367, "value": 87, "maximumValue": 100 }, { "statHash": 3871231066, "value": 18, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4274081686, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 19792, "level": 9, "step": 0, "progressToNextLevel": 1000, "nextLevelAt": 2399, "progressionHash": 1241939069 }, "talentGridHash": 2516462505, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 2, "21": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e48fb0751f23aa7002b7068a6488e8d6.png", "perkHash": 3464328064, "isActive": true }, { "iconPath": "/common/destiny_content/icons/0cff1d6da4575918c1d4597d2431a397.png", "perkHash": 3848963378, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 788138261, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074169428890", "itemLevel": 49, "stackSize": 1, "qualityLevel": 7, "stats": [ { "statHash": 2961396640, "value": 32, "maximumValue": 100 }, { "statHash": 4043523819, "value": 76, "maximumValue": 100 }, { "statHash": 1240592695, "value": 28, "maximumValue": 100 }, { "statHash": 155624089, "value": 55, "maximumValue": 100 }, { "statHash": 4188031367, "value": 73, "maximumValue": 100 }, { "statHash": 3871231066, "value": 7, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 297, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 16052, "level": 7, "step": 0, "progressToNextLevel": 2058, "nextLevelAt": 2399, "progressionHash": 3605101483 }, "talentGridHash": 1093766598, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 7, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 6, "5": 0, "21": 6 }, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/2e9c80070ae6fb6b14c5a23d7c130f79.png", "perkHash": 1900693659, "isActive": true }, { "iconPath": "/common/destiny_content/icons/a5b3881006d498c6936a0555c4ddf056.png", "perkHash": 318657130, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3275615580, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529110284901038", "itemLevel": 50, "stackSize": 1, "qualityLevel": 73, "stats": [ { "statHash": 4284893193, "value": 19, "maximumValue": 100 }, { "statHash": 4043523819, "value": 32, "maximumValue": 100 }, { "statHash": 1240592695, "value": 63, "maximumValue": 100 }, { "statHash": 155624089, "value": 80, "maximumValue": 100 }, { "statHash": 4188031367, "value": 73, "maximumValue": 100 }, { "statHash": 3871231066, "value": 4, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 373, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 4, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 12556, "level": 6, "step": 0, "progressToNextLevel": 961, "nextLevelAt": 2399, "progressionHash": 1156912330 }, "talentGridHash": 7269682, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/fa74e3c5a127176e77777257f7a61363.png", "perkHash": 3911170550, "isActive": false }, { "iconPath": "/common/destiny_content/icons/e0fbfaa73de5378fdf11a92827af751b.png", "perkHash": 4023922623, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 560601823, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529043303149493", "itemLevel": 22, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 12, "maximumValue": 100 }, { "statHash": 4043523819, "value": 37, "maximumValue": 100 }, { "statHash": 1240592695, "value": 80, "maximumValue": 100 }, { "statHash": 155624089, "value": 69, "maximumValue": 100 }, { "statHash": 4188031367, "value": 62, "maximumValue": 100 }, { "statHash": 3871231066, "value": 3, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 160, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 2252206363, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 1, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 142500, "level": 60, "step": 0, "progressToNextLevel": 1359, "nextLevelAt": 2399, "progressionHash": 1156912330 }, "talentGridHash": 1922369976, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": { "3": 3, "5": 0, "21": 3 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/f1c3005f73f780570c43691109367214.png", "perkHash": 824061322, "isActive": true }, { "iconPath": "/common/destiny_content/icons/eafa6e26551c1d705b1288c072cca022.png", "perkHash": 2655722661, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2992782156, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529110267207349", "itemLevel": 46, "stackSize": 1, "qualityLevel": 112, "stats": [ { "statHash": 4284893193, "value": 18, "maximumValue": 100 }, { "statHash": 3614673599, "value": 96, "maximumValue": 100 }, { "statHash": 2523465841, "value": 31, "maximumValue": 100 }, { "statHash": 155624089, "value": 74, "maximumValue": 100 }, { "statHash": 4188031367, "value": 51, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 371, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 9548, "level": 8, "step": 0, "progressToNextLevel": 767, "nextLevelAt": 1285, "progressionHash": 1054627247 }, "talentGridHash": 3085113436, "nodes": [ { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b6aa44fdd61ba9212754ac9d8fdb5baf.png", "perkHash": 2784842908, "isActive": false }, { "iconPath": "/common/destiny_content/icons/31ab63192f9a1d1c2251a4d352919797.png", "perkHash": 1928591453, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 547597837, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129512749866", "itemLevel": 56, "stackSize": 1, "qualityLevel": 40, "stats": [ { "statHash": 4284893193, "value": 37, "maximumValue": 100 }, { "statHash": 4043523819, "value": 49, "maximumValue": 100 }, { "statHash": 1240592695, "value": 51, "maximumValue": 100 }, { "statHash": 155624089, "value": 37, "maximumValue": 100 }, { "statHash": 4188031367, "value": 48, "maximumValue": 100 }, { "statHash": 3871231066, "value": 20, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 11, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 143810986 }, "talentGridHash": 705536255, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/39cc45284689e06dfa03465dd40dca41.png", "perkHash": 3245550306, "isActive": false }, { "iconPath": "/common/destiny_content/icons/75216c1283bcb3a0f1fc6675f44d287f.png", "perkHash": 1966098145, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2205574383, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133193615032", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 98, "maximumValue": 100 }, { "statHash": 4043523819, "value": 8, "maximumValue": 100 }, { "statHash": 1240592695, "value": 25, "maximumValue": 100 }, { "statHash": 155624089, "value": 75, "maximumValue": 100 }, { "statHash": 4188031367, "value": 91, "maximumValue": 100 }, { "statHash": 3871231066, "value": 15, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4274081686, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 1241939069 }, "talentGridHash": 2925416197, "nodes": [ { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 } ], "useCustomDyes": true, "artRegions": { "3": 2, "21": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": false }, { "iconPath": "/common/destiny_content/icons/07d2a002cdd4518759a5ab61112eacfe.png", "perkHash": 2725595956, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2992782156, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133056077699", "itemLevel": 46, "stackSize": 1, "qualityLevel": 130, "stats": [ { "statHash": 4284893193, "value": 18, "maximumValue": 100 }, { "statHash": 3614673599, "value": 98, "maximumValue": 100 }, { "statHash": 2523465841, "value": 39, "maximumValue": 100 }, { "statHash": 155624089, "value": 56, "maximumValue": 100 }, { "statHash": 4188031367, "value": 51, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1054627247 }, "talentGridHash": 3085113436, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a5b3881006d498c6936a0555c4ddf056.png", "perkHash": 318657130, "isActive": false }, { "iconPath": "/common/destiny_content/icons/dd5cfef4d7122c56172c8f3cba5d5498.png", "perkHash": 192233393, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2878293129, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114553999024", "itemLevel": 50, "stackSize": 1, "qualityLevel": 88, "stats": [ { "statHash": 4284893193, "value": 77, "maximumValue": 100 }, { "statHash": 4043523819, "value": 41, "maximumValue": 100 }, { "statHash": 1240592695, "value": 26, "maximumValue": 100 }, { "statHash": 155624089, "value": 27, "maximumValue": 100 }, { "statHash": 4188031367, "value": 43, "maximumValue": 100 }, { "statHash": 3871231066, "value": 40, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 388, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 4, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1999, "progressionHash": 1156912330 }, "talentGridHash": 465289219, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 0, "21": 0 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/dd5cfef4d7122c56172c8f3cba5d5498.png", "perkHash": 192233393, "isActive": false }, { "iconPath": "/common/destiny_content/icons/507b0ea0b465e8520d3e16df26d08035.png", "perkHash": 3283108368, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2992782156, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074472072832", "itemLevel": 48, "stackSize": 1, "qualityLevel": 5, "stats": [ { "statHash": 4284893193, "value": 18, "maximumValue": 100 }, { "statHash": 3614673599, "value": 96, "maximumValue": 100 }, { "statHash": 2523465841, "value": 31, "maximumValue": 100 }, { "statHash": 155624089, "value": 74, "maximumValue": 100 }, { "statHash": 4188031367, "value": 51, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 285, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 7496, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1285, "progressionHash": 1054627247 }, "talentGridHash": 3085113436, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/2e9c80070ae6fb6b14c5a23d7c130f79.png", "perkHash": 1900693659, "isActive": true }, { "iconPath": "/common/destiny_content/icons/7fec2a9c052b7180bec71dffc761aadb.png", "perkHash": 2179933840, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2168530919, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529041294962568", "itemLevel": 22, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 4284893193, "value": 100, "maximumValue": 100 }, { "statHash": 4043523819, "value": 4, "maximumValue": 100 }, { "statHash": 1240592695, "value": 22, "maximumValue": 100 }, { "statHash": 155624089, "value": 37, "maximumValue": 100 }, { "statHash": 4188031367, "value": 65, "maximumValue": 100 }, { "statHash": 3871231066, "value": 42, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 140, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 43226, "level": 11, "step": 0, "progressToNextLevel": 1100, "nextLevelAt": 4284, "progressionHash": 2958601622 }, "talentGridHash": 3807163128, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/1c12394789f1a4dcd5119042506e0b95.png", "perkHash": 1029160485, "isActive": true }, { "iconPath": "/common/destiny_content/icons/e4e0f6d2abcefcc1a4bfed9afd08b4ea.png", "perkHash": 597127202, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3783480580, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133057348471", "itemLevel": 45, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 100, "maximumValue": 100 }, { "statHash": 4043523819, "value": 2, "maximumValue": 100 }, { "statHash": 1240592695, "value": 13, "maximumValue": 100 }, { "statHash": 155624089, "value": 37, "maximumValue": 100 }, { "statHash": 4188031367, "value": 51, "maximumValue": 100 }, { "statHash": 3871231066, "value": 37, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 350, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 500, "progressionHash": 741293903 }, "talentGridHash": 3783480580, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3142762098, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129649309040", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 4284893193, "value": 37, "maximumValue": 100 }, { "statHash": 4043523819, "value": 48, "maximumValue": 100 }, { "statHash": 1240592695, "value": 78, "maximumValue": 100 }, { "statHash": 155624089, "value": 58, "maximumValue": 100 }, { "statHash": 4188031367, "value": 74, "maximumValue": 100 }, { "statHash": 3871231066, "value": 13, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1912493438, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 3570, "progressionHash": 1635066761 }, "talentGridHash": 2073748644, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 8, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 8, "5": 0, "21": 8 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/507b0ea0b465e8520d3e16df26d08035.png", "perkHash": 3283108368, "isActive": false }, { "iconPath": "/common/destiny_content/icons/a180884886739b34b824ff9afed386fb.png", "perkHash": 2450150110, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3452625744, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529091068433130", "itemLevel": 50, "stackSize": 1, "qualityLevel": 28, "stats": [ { "statHash": 4284893193, "value": 88, "maximumValue": 100 }, { "statHash": 4043523819, "value": 8, "maximumValue": 100 }, { "statHash": 1240592695, "value": 23, "maximumValue": 100 }, { "statHash": 155624089, "value": 62, "maximumValue": 100 }, { "statHash": 4188031367, "value": 66, "maximumValue": 100 }, { "statHash": 3871231066, "value": 36, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 328, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 539, "level": 1, "step": 0, "progressToNextLevel": 539, "nextLevelAt": 3570, "progressionHash": 2958601622 }, "talentGridHash": 3981554245, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 10, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 5, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 7, "5": 0, "21": 7 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a180884886739b34b824ff9afed386fb.png", "perkHash": 2450150110, "isActive": false }, { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2651271602, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529109884954190", "itemLevel": 46, "stackSize": 1, "qualityLevel": 110, "stats": [ { "statHash": 4284893193, "value": 94, "maximumValue": 100 }, { "statHash": 4043523819, "value": 12, "maximumValue": 100 }, { "statHash": 1240592695, "value": 28, "maximumValue": 100 }, { "statHash": 155624089, "value": 73, "maximumValue": 100 }, { "statHash": 4188031367, "value": 92, "maximumValue": 100 }, { "statHash": 3871231066, "value": 15, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 370, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4274081686, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 8, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 19252, "level": 9, "step": 0, "progressToNextLevel": 460, "nextLevelAt": 2399, "progressionHash": 1241939069 }, "talentGridHash": 2516462505, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": { "3": 2, "21": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/dd5cfef4d7122c56172c8f3cba5d5498.png", "perkHash": 2028552052, "isActive": true }, { "iconPath": "/common/destiny_content/icons/1c12394789f1a4dcd5119042506e0b95.png", "perkHash": 1029160485, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1267147308, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529042582838232", "itemLevel": 22, "stackSize": 1, "qualityLevel": 60, "stats": [ { "statHash": 2961396640, "value": 40, "maximumValue": 100 }, { "statHash": 4043523819, "value": 68, "maximumValue": 100 }, { "statHash": 1240592695, "value": 32, "maximumValue": 100 }, { "statHash": 155624089, "value": 41, "maximumValue": 100 }, { "statHash": 4188031367, "value": 71, "maximumValue": 100 }, { "statHash": 3871231066, "value": 8, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 160, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 20, "unlockFlagHashRequiredToEquip": 3697503310, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 0, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 142500, "level": 60, "step": 0, "progressToNextLevel": 1359, "nextLevelAt": 2399, "progressionHash": 3605101483 }, "talentGridHash": 1591348429, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 11 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 12 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 13 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 14 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 15 }, { "isActivated": false, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 16 } ], "useCustomDyes": true, "artRegions": { "3": 2, "5": 0, "21": 2 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/a180884886739b34b824ff9afed386fb.png", "perkHash": 2450150110, "isActive": true }, { "iconPath": "/common/destiny_content/icons/c5fabd06404f049071ee98ad20705dbf.png", "perkHash": 2814215427, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 3275294463, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529072940824671", "itemLevel": 49, "stackSize": 1, "qualityLevel": 10, "stats": [ { "statHash": 4284893193, "value": 88, "maximumValue": 100 }, { "statHash": 4043523819, "value": 8, "maximumValue": 100 }, { "statHash": 1240592695, "value": 25, "maximumValue": 100 }, { "statHash": 155624089, "value": 96, "maximumValue": 100 }, { "statHash": 4188031367, "value": 65, "maximumValue": 100 }, { "statHash": 3871231066, "value": 24, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 300, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3817088859, "cannotEquipReason": 16, "damageType": 1, "damageTypeHash": 3373582085, "damageTypeNodeIndex": 6, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 33558, "level": 9, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4284, "progressionHash": 2958601622 }, "talentGridHash": 214163140, "nodes": [ { "isActivated": true, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 6, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 } ], "useCustomDyes": true, "artRegions": { "3": 4, "5": 0, "21": 4 }, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b1d6a5c5769303d22872631148c016bd.png", "perkHash": 731752060, "isActive": true }, { "iconPath": "/common/destiny_content/icons/e48fb0751f23aa7002b7068a6488e8d6.png", "perkHash": 3464328064, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1439896155, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129649311040", "itemLevel": 58, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 18, "maximumValue": 100 }, { "statHash": 3614673599, "value": 98, "maximumValue": 100 }, { "statHash": 2523465841, "value": 44, "maximumValue": 100 }, { "statHash": 155624089, "value": 56, "maximumValue": 100 }, { "statHash": 4188031367, "value": 50, "maximumValue": 100 }, { "statHash": 3871231066, "value": 2, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 380, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 3186775604, "cannotEquipReason": 16, "damageType": 3, "damageTypeHash": 1847026933, "damageTypeNodeIndex": 7, "damageTypeStepIndex": 1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1071, "progressionHash": 1054627247 }, "talentGridHash": 1823071726, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/31ab63192f9a1d1c2251a4d352919797.png", "perkHash": 1928591453, "isActive": false }, { "iconPath": "/common/destiny_content/icons/b6aa44fdd61ba9212754ac9d8fdb5baf.png", "perkHash": 2784842908, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2982280293, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529108699979425", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 4284893193, "value": 94, "maximumValue": 100 }, { "statHash": 4043523819, "value": 12, "maximumValue": 100 }, { "statHash": 1240592695, "value": 28, "maximumValue": 100 }, { "statHash": 155624089, "value": 78, "maximumValue": 100 }, { "statHash": 4188031367, "value": 88, "maximumValue": 100 }, { "statHash": 3871231066, "value": 12, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4274081686, "cannotEquipReason": 16, "damageType": 2, "damageTypeHash": 2303181850, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 2, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 15673, "level": 7, "step": 0, "progressToNextLevel": 1679, "nextLevelAt": 2399, "progressionHash": 1241939069 }, "talentGridHash": 2208281672, "nodes": [ { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 10 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 11 } ], "useCustomDyes": true, "artRegions": { "3": 1, "21": 1 }, "isEquipment": true, "isGridComplete": true, "perks": [ { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": true }, { "iconPath": "/common/destiny_content/icons/e1f8726fd86727f2bde3d92ea72007b0.png", "perkHash": 1863078623, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 1205039748, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118529529528", "itemLevel": 50, "stackSize": 1, "qualityLevel": 90, "stats": [ { "statHash": 4284893193, "value": 59, "maximumValue": 100 }, { "statHash": 4043523819, "value": 59, "maximumValue": 100 }, { "statHash": 1240592695, "value": 32, "maximumValue": 100 }, { "statHash": 155624089, "value": 51, "maximumValue": 100 }, { "statHash": 4188031367, "value": 31, "maximumValue": 100 }, { "statHash": 3871231066, "value": 25, "maximumValue": 100 } ], "primaryStat": { "statHash": 368428387, "value": 390, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 1361651374, "cannotEquipReason": 16, "damageType": 4, "damageTypeHash": 3454344768, "damageTypeNodeIndex": 5, "damageTypeStepIndex": 0, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 7496, "level": 7, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 1285, "progressionHash": 1296042811 }, "talentGridHash": 1189457380, "nodes": [ { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 8 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 9 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 10 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/8ee959d90343fabd4679c6848790d76d.png", "perkHash": 1026458383, "isActive": false }, { "iconPath": "/common/destiny_content/icons/267b93df00c716260808145ed6b96bcc.png", "perkHash": 2507926095, "isActive": true }, { "iconPath": "/common/destiny_content/icons/c5fabd06404f049071ee98ad20705dbf.png", "perkHash": 2814215427, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 } ], "bucketHash": 4046403665 }, { "items": [ { "itemHash": 1777175504, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118690245290", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2026036815, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118527507818", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 70, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4250, "progressionHash": 2663148817 }, "talentGridHash": 83157830, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0c39a9e4b9e4274fbd5853cf56493e45.png", "perkHash": 3467141629, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2815148218, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529093299928508", "itemLevel": 52, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 32, "maximumValue": 0 }, { "statHash": 1735777505, "value": 46, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 320, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 2132212154, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 5 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/dac61c49330de106c39113f5cf5dca9a.png", "perkHash": 1724858392, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 894761026, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "0", "itemLevel": 0, "stackSize": 6, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": false, "objectives": [], "state": 0 }, { "itemHash": 1814540593, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119623575581", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1814540602, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529121227305569", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3859658562, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529111146637981", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3720866501, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529036213572428", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 194424268, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529128588667734", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1777175506, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118701800463", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1500229041, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "0", "itemLevel": 0, "stackSize": 18, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": false, "objectives": [], "state": 0 }, { "itemHash": 3705287267, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "0", "itemLevel": 0, "stackSize": 17, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": false, "objectives": [], "state": 0 }, { "itemHash": 3671454772, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529037004931936", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1158121103, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529073695332783", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 894761027, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "0", "itemLevel": 0, "stackSize": 14, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": false, "objectives": [], "state": 0 }, { "itemHash": 3056359299, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529091681724545", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 139252901, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529092989772759", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1040215532, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529027815933600", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 20615, "level": 3, "step": 0, "progressToNextLevel": 2765, "nextLevelAt": 11900, "progressionHash": 3306030760 }, "talentGridHash": 734858275, "nodes": [ { "isActivated": false, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e0c96fe64761ce91fe8fc2e773fd27b7.png", "perkHash": 2144441188, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7218a676bed5ff28a5599abf59b3fd40.png", "perkHash": 58719660, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3d6ad381d0927f80f2d94a6750694114.png", "perkHash": 745231149, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3056359297, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074308380195", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2026036815, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131774923338", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 67, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4250, "progressionHash": 2663148817 }, "talentGridHash": 83157830, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0c39a9e4b9e4274fbd5853cf56493e45.png", "perkHash": 3467141629, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3367786035, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529092663766603", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 4211402863, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529059748647113", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 4112903825, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529080649128089", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2026036808, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529130706251789", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 31, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 34, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4250, "progressionHash": 2663148817 }, "talentGridHash": 3028292481, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/538e0b75cb0fbeb1edd9af2e8b6059b2.png", "perkHash": 2871746970, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2026036814, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132701531046", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 34, "maximumValue": 0 }, { "statHash": 4244567218, "value": 31, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4250, "progressionHash": 2663148817 }, "talentGridHash": 1494987619, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/dac61c49330de106c39113f5cf5dca9a.png", "perkHash": 1724858392, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3753095826, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529031011368713", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 27771125, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131772286175", "itemLevel": 50, "stackSize": 1, "qualityLevel": 34, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 63, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 334, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 470680123, "nodes": [ { "isActivated": true, "stepIndex": 3, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 5 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29a4597cc4aa8af255643502a92f1499.png", "perkHash": 2333942527, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2801714428, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529080503373814", "itemLevel": 51, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 66, "maximumValue": 0 }, { "statHash": 1735777505, "value": 44, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 310, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 45518, "level": 6, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 10115, "progressionHash": 262790308 }, "talentGridHash": 3782387027, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 6 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0c39a9e4b9e4274fbd5853cf56493e45.png", "perkHash": 3467141629, "isActive": false }, { "iconPath": "/common/destiny_content/icons/0db0ed18b69b9faa4172a1b5cd65e48d.png", "perkHash": 1981671584, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 269776572, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "0", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": false, "objectives": [], "state": 0 }, { "itemHash": 2254123540, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "0", "itemLevel": 0, "stackSize": 200, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": false, "objectives": [], "state": 0 }, { "itemHash": 1096884848, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114448098345", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 521135891, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2989526609, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074294369248", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 27771123, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131773834937", "itemLevel": 50, "stackSize": 1, "qualityLevel": 35, "stats": [ { "statHash": 144602215, "value": 30, "maximumValue": 0 }, { "statHash": 1735777505, "value": 44, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 335, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 630916180, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 5 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/538e0b75cb0fbeb1edd9af2e8b6059b2.png", "perkHash": 2871746970, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2989526610, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529071152432593", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1814540606, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529119478437119", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 202245945, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529093192273111", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2026036809, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529118529527206", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 33, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 37, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4250, "progressionHash": 2663148817 }, "talentGridHash": 2050839012, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/6f06a12d7ba59821853dabbd9f9f6a8c.png", "perkHash": 4096399315, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2245431211, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529123382722108", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 521135891, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 4023667584, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529080571388071", "itemLevel": 48, "stackSize": 1, "qualityLevel": 7, "stats": [ { "statHash": 144602215, "value": 29, "maximumValue": 0 }, { "statHash": 1735777505, "value": 38, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 287, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2993747650, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 3676017674, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 5 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/0c39a9e4b9e4274fbd5853cf56493e45.png", "perkHash": 3467141629, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 4197595974, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529122751146193", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 813108079, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529080925360696", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3056359296, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529121216785521", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3703598457, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529036205977260", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 3017642079, "value": 35, "maximumValue": 100 }, { "statHash": 360359141, "value": 50, "maximumValue": 100 } ], "primaryStat": { "statHash": 1501155019, "value": 150, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2028074163, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 3302606530, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/b8400c7bc18edc8b258cb2931b9f8802.png", "perkHash": 1605283625, "isActive": true }, { "iconPath": "/common/destiny_content/icons/cb6ac595259e6e4c0aa6f98a7d4cfa52.png", "perkHash": 4091143788, "isActive": true }, { "iconPath": "/common/destiny_content/icons/6bafd18440f70faedab72c669a68c4c0.png", "perkHash": 3632688592, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1491990347, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133057345624", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 521135891, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1068518401, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129575772386", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 521135891, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3764987317, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529076959901601", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1158121101, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529113320510960", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3764987315, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529077152447733", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 202245948, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529121210686418", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1096884855, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529123390413881", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 521135891, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1263693537, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529105150914971", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 4112903824, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529085119100695", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3764987319, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529076953335119", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1202967480, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529042785921434", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 3017642079, "value": 60, "maximumValue": 100 }, { "statHash": 360359141, "value": 60, "maximumValue": 100 } ], "primaryStat": { "statHash": 1501155019, "value": 150, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 2064730451, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 0 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/101e5832c1ec59c2c56e9a0c1aec95e4.png", "perkHash": 3104860955, "isActive": true }, { "iconPath": "/common/destiny_content/icons/cb6ac595259e6e4c0aa6f98a7d4cfa52.png", "perkHash": 4091143788, "isActive": true }, { "iconPath": "/common/destiny_content/icons/6bafd18440f70faedab72c669a68c4c0.png", "perkHash": 3632688592, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2751204699, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529041196877684", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "primaryStat": { "statHash": 3897883278, "value": 3, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 521135891, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1096884851, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529084841289003", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 521135891, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 533543004, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529109987539344", "itemLevel": 60, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 25, "maximumValue": 0 }, { "statHash": 4244567218, "value": 34, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 173060, "level": 16, "step": 0, "progressToNextLevel": 510, "nextLevelAt": 11900, "progressionHash": 3306030760 }, "talentGridHash": 734858275, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": true, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 4, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e0c96fe64761ce91fe8fc2e773fd27b7.png", "perkHash": 4182717269, "isActive": true }, { "iconPath": "/common/destiny_content/icons/7218a676bed5ff28a5599abf59b3fd40.png", "perkHash": 2678818989, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3d6ad381d0927f80f2d94a6750694114.png", "perkHash": 4148534176, "isActive": true } ], "location": 2, "transferStatus": 0, "locked": true, "lockable": true, "objectives": [], "state": 1 }, { "itemHash": 2502086554, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529074283287767", "itemLevel": 34, "stackSize": 1, "qualityLevel": 0, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 12, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 170, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 33, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 2500, "progressionHash": 3194085378 }, "talentGridHash": 458986123, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1592588005, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529043011028449", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2026036813, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529132834805504", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 72, "maximumValue": 0 }, { "statHash": 4244567218, "value": 0, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 4250, "progressionHash": 2663148817 }, "talentGridHash": 586879530, "nodes": [ { "isActivated": true, "stepIndex": 2, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 2, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/754a6bbf8f80335b3fdcf900d504d7b9.png", "perkHash": 183958561, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 185564345, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529114828077778", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 668012624, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129575780962", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 27771125, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529131772284507", "itemLevel": 50, "stackSize": 1, "qualityLevel": 35, "stats": [ { "statHash": 144602215, "value": 32, "maximumValue": 0 }, { "statHash": 1735777505, "value": 0, "maximumValue": 0 }, { "statHash": 4244567218, "value": 37, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 335, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 4110009629, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5058, "progressionHash": 262790308 }, "talentGridHash": 470680123, "nodes": [ { "isActivated": true, "stepIndex": 1, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 1, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 5 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/29a4597cc4aa8af255643502a92f1499.png", "perkHash": 2333942527, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 2715326881, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529133193314970", "itemLevel": 50, "stackSize": 1, "qualityLevel": 100, "stats": [ { "statHash": 144602215, "value": 0, "maximumValue": 0 }, { "statHash": 1735777505, "value": 23, "maximumValue": 0 }, { "statHash": 4244567218, "value": 24, "maximumValue": 0 } ], "primaryStat": { "statHash": 3897883278, "value": 400, "maximumValue": 0 }, "canEquip": false, "equipRequiredLevel": 40, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "progression": { "dailyProgress": 0, "weeklyProgress": 0, "currentProgress": 0, "level": 1, "step": 0, "progressToNextLevel": 0, "nextLevelAt": 5950, "progressionHash": 3306030760 }, "talentGridHash": 734858275, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 1 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 2 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 3 }, { "isActivated": false, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 4 }, { "isActivated": false, "stepIndex": 3, "state": 0, "hidden": false, "nodeHash": 5 }, { "isActivated": true, "stepIndex": 0, "state": 0, "hidden": false, "nodeHash": 6 }, { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 7 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [ { "iconPath": "/common/destiny_content/icons/e0c96fe64761ce91fe8fc2e773fd27b7.png", "perkHash": 2144441188, "isActive": false }, { "iconPath": "/common/destiny_content/icons/7218a676bed5ff28a5599abf59b3fd40.png", "perkHash": 58719660, "isActive": false }, { "iconPath": "/common/destiny_content/icons/3d6ad381d0927f80f2d94a6750694114.png", "perkHash": 3798969172, "isActive": false } ], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 1845137798, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129003703854", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 2166136261, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 }, { "itemHash": 3671454773, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "6917529129588123735", "itemLevel": 0, "stackSize": 1, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 1631415876, "cannotEquipReason": 16, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 1245368945, "nodes": [ { "isActivated": true, "stepIndex": 0, "state": 13, "hidden": true, "nodeHash": 0 } ], "useCustomDyes": true, "artRegions": {}, "isEquipment": true, "isGridComplete": false, "perks": [], "location": 2, "transferStatus": 0, "locked": false, "lockable": true, "objectives": [], "state": 0 } ], "bucketHash": 138197802 } ], "Currency": [ { "items": [ { "itemHash": 3159615086, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "0", "itemLevel": 0, "stackSize": 6208, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 0, "transferStatus": 2, "locked": false, "lockable": false, "objectives": [], "state": 0 } ], "bucketHash": 2689798308 }, { "items": [ { "itemHash": 2534352370, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "0", "itemLevel": 0, "stackSize": 137, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 0, "transferStatus": 2, "locked": false, "lockable": false, "objectives": [], "state": 0 } ], "bucketHash": 2689798304 }, { "items": [ { "itemHash": 2749350776, "bindStatus": 0, "isEquipped": false, "itemInstanceId": "0", "itemLevel": 0, "stackSize": 400, "qualityLevel": 0, "stats": [], "canEquip": false, "equipRequiredLevel": 0, "unlockFlagHashRequiredToEquip": 0, "cannotEquipReason": 0, "damageType": 0, "damageTypeHash": 0, "damageTypeNodeIndex": -1, "damageTypeStepIndex": -1, "talentGridHash": 0, "nodes": [], "useCustomDyes": false, "artRegions": {}, "isEquipment": false, "isGridComplete": false, "perks": [], "location": 0, "transferStatus": 2, "locked": false, "lockable": false, "objectives": [], "state": 0 } ], "bucketHash": 2689798311 } ] }, "currencies": [ { "itemHash": 3159615086, "value": 6208 }, { "itemHash": 2534352370, "value": 137 }, { "itemHash": 2749350776, "value": 400 } ] }, "grimoireScore": 5110, "vendorReceipts": [], "dateLastPlayed": "2017-09-07T05:32:39Z", "versions": 15 } }, "ErrorCode": 1, "ThrottleSeconds": 0, "ErrorStatus": "Success", "Message": "Ok", "MessageData": {} } ================================================ FILE: src/testing/data/linkedaccounts-2025-07-15.json ================================================ { "profiles": [ { "dateLastPlayed": "2025-07-15T06:30:55Z", "isOverridden": false, "isCrossSavePrimary": true, "platformSilver": { "platformSilver": { "TigerPsn": { "itemHash": 3147280338, "quantity": 400, "bindStatus": 0, "location": 1, "bucketHash": 2689798306, "transferStatus": 2, "lockable": false, "state": 0, "dismantlePermission": 0, "isWrapper": false }, "TigerXbox": { "itemHash": 3147280338, "quantity": 0, "bindStatus": 0, "location": 1, "bucketHash": 2689798307, "transferStatus": 2, "lockable": false, "state": 0, "dismantlePermission": 0, "isWrapper": false }, "TigerBlizzard": { "itemHash": 3147280338, "quantity": 0, "bindStatus": 0, "location": 1, "bucketHash": 2689798316, "transferStatus": 2, "lockable": false, "state": 0, "dismantlePermission": 0, "isWrapper": false }, "TigerStadia": { "itemHash": 3147280338, "quantity": 0, "bindStatus": 0, "location": 1, "bucketHash": 2689798317, "transferStatus": 2, "lockable": false, "state": 0, "dismantlePermission": 0, "isWrapper": false }, "TigerSteam": { "itemHash": 3147280338, "quantity": 0, "bindStatus": 0, "location": 1, "bucketHash": 130233231, "transferStatus": 2, "lockable": false, "state": 0, "dismantlePermission": 0, "isWrapper": false }, "BungieNext": { "itemHash": 3147280338, "quantity": 0, "bindStatus": 0, "location": 1, "bucketHash": 130233230, "transferStatus": 2, "lockable": false, "state": 0, "dismantlePermission": 0, "isWrapper": false }, "TigerEgs": { "itemHash": 3147280338, "quantity": 0, "bindStatus": 0, "location": 1, "bucketHash": 130233229, "transferStatus": 2, "lockable": false, "state": 0, "dismantlePermission": 0, "isWrapper": false } } }, "crossSaveOverride": 2, "applicableMembershipTypes": [ 1, 3, 5, 6, 2 ], "isPublic": false, "membershipType": 2, "membershipId": "4611686018433092312", "displayName": "VidBoi-BMC", "bungieGlobalDisplayName": "VidBoi", "bungieGlobalDisplayNameCode": 9226 } ], "bnetMembership": { "supplementalDisplayName": "VidBoi#9226", "iconPath": "/img/profile/avatars/cc25.jpg", "crossSaveOverride": 0, "isPublic": false, "membershipType": 254, "membershipId": "7094", "displayName": "VidBoi", "bungieGlobalDisplayName": "VidBoi", "bungieGlobalDisplayNameCode": 9226 }, "profilesWithErrors": [ { "errorCode": 1601, "infoCard": { "crossSaveOverride": 2, "applicableMembershipTypes": [], "isPublic": true, "membershipType": 1, "membershipId": "4611686018429726245", "displayName": "Vid Boi", "bungieGlobalDisplayName": "VidBoi", "bungieGlobalDisplayNameCode": 9226 } }, { "errorCode": 1601, "infoCard": { "crossSaveOverride": 2, "applicableMembershipTypes": [], "isPublic": true, "membershipType": 3, "membershipId": "4611686018509956320", "displayName": "VidBoi", "bungieGlobalDisplayName": "VidBoi", "bungieGlobalDisplayNameCode": 9226 } }, { "errorCode": 1601, "infoCard": { "crossSaveOverride": 2, "applicableMembershipTypes": [], "isPublic": true, "membershipType": 5, "membershipId": "4611686018509956086", "displayName": "vidboi#6166", "bungieGlobalDisplayName": "VidBoi", "bungieGlobalDisplayNameCode": 9226 } }, { "errorCode": 1601, "infoCard": { "crossSaveOverride": 2, "applicableMembershipTypes": [], "isPublic": true, "membershipType": 6, "membershipId": "4611686018528634783", "displayName": "VidBoi-BMC", "bungieGlobalDisplayName": "VidBoi", "bungieGlobalDisplayNameCode": 9226 } } ] } ================================================ FILE: src/testing/data/profile-2025-12-02.json ================================================ [File too large to display: 10.4 MB] ================================================ FILE: src/testing/data/vendors-2025-12-02.json ================================================ { "Response": { "vendorGroups": { "data": { "groups": [ { "vendorGroupHash": 3227191227, "vendorHashes": [ 4060517507 ] }, { "vendorGroupHash": 611624255, "vendorHashes": [ 1054786807, 1499949918, 1305220558, 1664442954 ] }, { "vendorGroupHash": 4171455936, "vendorHashes": [ 1474045886 ] }, { "vendorGroupHash": 679769104, "vendorHashes": [ 3347378076, 69482069, 1976548992, 3603221665, 2255782930, 672118013, 248695599, 765357505, 296729347, 3484140575, 4230408743, 350061650, 2572415929, 3361454721 ] }, { "vendorGroupHash": 3350514087, "vendorHashes": [ 1660659508 ] }, { "vendorGroupHash": 1767960256, "vendorHashes": [ 1021220385, 1413212512 ] }, { "vendorGroupHash": 3667761105, "vendorHashes": [ 2384113223 ] }, { "vendorGroupHash": 4179305295, "vendorHashes": [ 3442679730, 3431983428 ] }, { "vendorGroupHash": 2537374699, "vendorHashes": [ 396892126, 1576276905, 1841717884, 1616085565, 3411552308, 1816541247, 2531198101, 4254652401 ] } ] }, "privacy": 2 }, "vendors": { "data": { "537912098": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 537912098, "nextRefreshDate": "9999-12-31T23:59:59.999Z", "enabled": true }, "3751514131": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3751514131, "nextRefreshDate": "9999-12-31T23:59:59.999Z", "enabled": true }, "3033500747": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3033500747, "nextRefreshDate": "2025-12-02T19:45:00Z", "enabled": true }, "396892126": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 396892126, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3347378076": { "canPurchase": true, "progression": { "progressionHash": 198624022, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 90, "level": 1, "levelCap": 16, "stepIndex": 1, "progressToNextLevel": 40, "nextLevelAt": 75 }, "vendorLocationIndex": 0, "vendorHash": 3347378076, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1576276905": { "canPurchase": true, "progression": { "progressionHash": 1660497607, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 61705, "level": 22, "levelCap": -1, "stepIndex": 2, "progressToNextLevel": 1205, "nextLevelAt": 2750 }, "vendorLocationIndex": 0, "vendorHash": 1576276905, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "69482069": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 69482069, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1976548992": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 1976548992, "nextRefreshDate": "2025-12-02T20:00:00Z", "enabled": true }, "3603221665": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 3603221665, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2255782930": { "canPurchase": false, "progression": { "progressionHash": 784742260, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 0, "level": 0, "levelCap": 16, "stepIndex": 0, "progressToNextLevel": 0, "nextLevelAt": 50 }, "vendorLocationIndex": 0, "vendorHash": 2255782930, "nextRefreshDate": "2025-12-09T01:00:00Z", "enabled": true }, "672118013": { "canPurchase": true, "progression": { "progressionHash": 1471185389, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 0, "level": 0, "levelCap": 16, "stepIndex": 0, "progressToNextLevel": 0, "nextLevelAt": 50, "currentResetCount": 0 }, "vendorLocationIndex": 0, "vendorHash": 672118013, "nextRefreshDate": "2025-12-03T17:00:00Z", "enabled": true }, "1841717884": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 1841717884, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "248695599": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 248695599, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1616085565": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 1616085565, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3411552308": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 3411552308, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "765357505": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 765357505, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "319449446": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 319449446, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1932528548": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1932528548, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2372049275": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2372049275, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2268720627": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2268720627, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "261493107": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 261493107, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2494162583": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2494162583, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3440426498": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3440426498, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "307148624": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 307148624, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "418092119": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 418092119, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1192173495": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1192173495, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2474196287": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2474196287, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2047883483": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2047883483, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2117613039": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2117613039, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2060371059": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2060371059, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2856424422": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2856424422, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2933806284": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2933806284, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "776690932": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 776690932, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "117637110": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 117637110, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3296235050": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3296235050, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1663272602": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1663272602, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1816541247": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 1816541247, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3442679730": { "canPurchase": true, "progression": { "progressionHash": 527867935, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 7058, "level": 13, "levelCap": 16, "stepIndex": 13, "progressToNextLevel": 608, "nextLevelAt": 1075, "currentResetCount": 0 }, "vendorLocationIndex": 0, "vendorHash": 3442679730, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3431983428": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 3431983428, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2531198101": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 2531198101, "nextRefreshDate": "2025-12-03T09:00:00Z", "enabled": true }, "4254652401": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 4254652401, "nextRefreshDate": "2025-12-03T09:00:00Z", "enabled": true }, "3705882217": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3705882217, "nextRefreshDate": "9999-12-31T23:59:59.999Z", "enabled": true }, "1660659508": { "canPurchase": true, "progression": { "progressionHash": 3011295063, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 8710, "level": 5, "levelCap": 16, "stepIndex": 5, "progressToNextLevel": 1210, "nextLevelAt": 1500 }, "vendorLocationIndex": 0, "vendorHash": 1660659508, "nextRefreshDate": "2025-12-03T09:00:00Z", "enabled": true }, "3352059696": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3352059696, "nextRefreshDate": "2025-12-03T09:00:00Z", "enabled": true }, "2734167": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2734167, "nextRefreshDate": "9999-12-31T23:59:59.999Z", "enabled": true }, "2384113223": { "canPurchase": true, "progression": { "progressionHash": 4203877294, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 32635, "level": 30, "levelCap": -1, "stepIndex": 30, "progressToNextLevel": 1635, "nextLevelAt": 2000 }, "vendorLocationIndex": 0, "seasonalRank": 1, "vendorHash": 2384113223, "nextRefreshDate": "2025-12-03T09:00:00Z", "enabled": true }, "296729347": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 296729347, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3484140575": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3484140575, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3884814177": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3884814177, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2776510816": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2776510816, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "4242059374": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 4242059374, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "4230408743": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 4230408743, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "272081343": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 272081343, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3298600270": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3298600270, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1854671592": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1854671592, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2209308523": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2209308523, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1125969407": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1125969407, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "67760598": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 67760598, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2996039207": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2996039207, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "399563869": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 399563869, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1092954315": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1092954315, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "350061650": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 350061650, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2199358137": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2199358137, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3895383279": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3895383279, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2572415929": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2572415929, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3949128738": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3949128738, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2890273682": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2890273682, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2890273683": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2890273683, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628785": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628785, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628784": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628784, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628787": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628787, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628786": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628786, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628789": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628789, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628788": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628788, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628791": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628791, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628790": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628790, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628793": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628793, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444628792": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444628792, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3394295928": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3394295928, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3394295929": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3394295929, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3394295930": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3394295930, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3394295931": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3394295931, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3394295932": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3394295932, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3394295933": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3394295933, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3394295934": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3394295934, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1021220385": { "canPurchase": true, "progression": { "progressionHash": 1807348350, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 27605, "level": 32, "levelCap": -1, "stepIndex": 30, "progressToNextLevel": 1355, "nextLevelAt": 1500 }, "vendorLocationIndex": 0, "seasonalRank": 0, "vendorHash": 1021220385, "nextRefreshDate": "2025-12-03T09:00:00Z", "enabled": true }, "1413212512": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1413212512, "nextRefreshDate": "2025-12-03T09:00:00Z", "enabled": true }, "1474045886": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1474045886, "nextRefreshDate": "9999-12-31T23:59:59.999Z", "enabled": true }, "714148153": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 714148153, "nextRefreshDate": "9999-12-31T23:59:59.999Z", "enabled": true }, "811102248": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 811102248, "nextRefreshDate": "9999-12-31T23:59:59.999Z", "enabled": true }, "811102249": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 811102249, "nextRefreshDate": "9999-12-31T23:59:59.999Z", "enabled": true }, "4060517507": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 4060517507, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "153857624": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 153857624, "nextRefreshDate": "9999-12-31T23:59:59.999Z", "enabled": true }, "1054786807": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1054786807, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1499949918": { "canPurchase": false, "progression": { "progressionHash": 2136678968, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 0, "level": 0, "levelCap": -1, "stepIndex": 0, "progressToNextLevel": 0, "nextLevelAt": 1000 }, "vendorLocationIndex": 0, "vendorHash": 1499949918, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1305220558": { "canPurchase": false, "progression": { "progressionHash": 639915560, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 0, "level": 0, "levelCap": -1, "stepIndex": 0, "progressToNextLevel": 0, "nextLevelAt": 1000 }, "vendorLocationIndex": 0, "vendorHash": 1305220558, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1664442954": { "canPurchase": false, "progression": { "progressionHash": 2339655916, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 0, "level": 0, "levelCap": -1, "stepIndex": 0, "progressToNextLevel": 0, "nextLevelAt": 1000 }, "vendorLocationIndex": 0, "vendorHash": 1664442954, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3361454721": { "canPurchase": true, "vendorLocationIndex": 0, "vendorHash": 3361454721, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "4195846091": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 4195846091, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1420462601": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1420462601, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2727390699": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2727390699, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "4095855400": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 4095855400, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "511784434": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 511784434, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "4137400834": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 4137400834, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3547377374": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3547377374, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3344881326": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3344881326, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2747252909": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2747252909, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3904045300": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3904045300, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2357427271": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2357427271, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "574872862": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 574872862, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3809168174": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3809168174, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2578999354": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2578999354, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3198105354": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3198105354, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "313845375": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 313845375, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2085188184": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2085188184, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3478784353": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3478784353, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3146561831": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3146561831, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3895124535": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3895124535, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "903440291": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 903440291, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1913609779": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1913609779, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3728200412": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3728200412, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "862671345": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 862671345, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2975168334": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2975168334, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3484865102": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3484865102, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1480784244": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1480784244, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3588941765": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3588941765, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3303483917": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3303483917, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1749390897": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1749390897, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1672606033": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1672606033, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "145159682": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 145159682, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1292692259": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1292692259, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2840194832": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2840194832, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2565786257": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2565786257, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1429791273": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1429791273, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3049065213": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3049065213, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "871887901": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 871887901, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1677356040": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1677356040, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1143379815": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1143379815, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "946015990": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 946015990, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "710977256": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 710977256, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3104830720": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3104830720, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3947739156": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3947739156, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "311590836": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 311590836, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2091720363": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2091720363, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2382195382": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2382195382, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2718734409": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2718734409, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3529215660": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3529215660, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1942263816": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1942263816, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2223896103": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2223896103, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2634769701": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2634769701, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "667903501": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 667903501, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "998725389": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 998725389, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1626995479": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1626995479, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3743143700": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3743143700, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "46764309": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 46764309, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "183427272": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 183427272, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "84023067": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 84023067, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1629506227": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1629506227, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1806740915": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1806740915, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1712713373": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1712713373, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3360654976": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3360654976, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1594205003": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1594205003, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3053846308": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3053846308, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2630013030": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2630013030, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3409789310": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3409789310, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1820924542": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1820924542, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "639852168": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 639852168, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "875061823": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 875061823, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3194559894": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3194559894, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1890828307": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1890828307, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "683678992": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 683678992, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2595490586": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2595490586, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "654952868": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 654952868, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2906014866": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2906014866, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2232145065": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2232145065, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "3444362755": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 3444362755, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "502095006": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 502095006, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "4140351452": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 4140351452, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2672927612": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2672927612, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "2435958557": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 2435958557, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "1248953136": { "canPurchase": false, "vendorLocationIndex": 0, "vendorHash": 1248953136, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true }, "444807002": { "canPurchase": false, "progression": { "progressionHash": 3011295063, "dailyProgress": 0, "dailyLimit": 0, "weeklyProgress": 0, "weeklyLimit": 0, "currentProgress": 8710, "level": 5, "levelCap": 16, "stepIndex": 5, "progressToNextLevel": 1210, "nextLevelAt": 1500 }, "vendorLocationIndex": 0, "vendorHash": 444807002, "nextRefreshDate": "2025-12-09T17:00:00Z", "enabled": true } }, "privacy": 2 }, "categories": { "privacy": 2 }, "sales": { "data": { "537912098": { "saleItems": { "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 353704689, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 23, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 3853748946, "quantity": 7, "costs": [ { "itemHash": 800069450, "quantity": 11, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 3853748946, "quantity": 11, "costs": [ { "itemHash": 800069450, "quantity": 11, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 1, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 3159615086, "quantity": 19997, "costs": [ { "itemHash": 800069450, "quantity": 5, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 1, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 3159615086, "quantity": 29297, "costs": [ { "itemHash": 800069450, "quantity": 7, "hasConditionalVisibility": false } ] }, "33": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 33, "itemHash": 3282419336, "quantity": 7, "costs": [ { "itemHash": 800069450, "quantity": 11, "hasConditionalVisibility": false } ] }, "36": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 36, "itemHash": 3581456570, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 1, "hasConditionalVisibility": false } ] }, "37": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 37, "itemHash": 4032296272, "quantity": 1, "costs": [] } } }, "3751514131": { "saleItems": { "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 903043774, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 17, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 4068264807, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 23, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 4190156464, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 23, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 3325463374, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 23, "hasConditionalVisibility": false } ] }, "57": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 57, "itemHash": 1783582993, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 71, "hasConditionalVisibility": false } ] }, "81": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 81, "itemHash": 2826187530, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 71, "hasConditionalVisibility": false } ] }, "84": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 84, "itemHash": 3856705927, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 23, "hasConditionalVisibility": false } ] }, "94": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 94, "itemHash": 2742838700, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 17, "hasConditionalVisibility": false } ] }, "100": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 100, "itemHash": 1946491241, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 17, "hasConditionalVisibility": false } ] }, "106": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 106, "itemHash": 3616586446, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 17, "hasConditionalVisibility": false } ] }, "127": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 127, "itemHash": 3165547384, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 17, "hasConditionalVisibility": false } ] }, "156": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 156, "itemHash": 216983039, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 17, "hasConditionalVisibility": false } ] }, "231": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 231, "itemHash": 1354727549, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 41, "hasConditionalVisibility": false } ] }, "252": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 252, "itemHash": 4195186942, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 17, "hasConditionalVisibility": false } ] }, "262": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 262, "itemHash": 2152484073, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 17, "hasConditionalVisibility": false } ] }, "291": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 291, "itemHash": 2599338624, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 17, "hasConditionalVisibility": false } ] }, "343": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 343, "itemHash": 3691823069, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 7, "hasConditionalVisibility": false } ] } } }, "3033500747": { "saleItems": { "0": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 4103169457, "quantity": 1, "costs": [ { "itemHash": 3041849475, "quantity": 100, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 3472417126, "quantity": 1, "costs": [ { "itemHash": 3041849475, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1344404154, "quantity": 1, "costs": [ { "itemHash": 3041849475, "quantity": 100, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 1933944344, "quantity": 1, "costs": [ { "itemHash": 3041849475, "quantity": 100, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 1796959741, "quantity": 1, "costs": [ { "itemHash": 3041849475, "quantity": 100, "hasConditionalVisibility": false } ] } } }, "396892126": { "saleItems": { "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 321260461, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 539497618, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 2504333144, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1435681904, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 1666413633, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 4135938409, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 3541326821, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 3541326820, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 3541326823, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "3347378076": { "saleItems": { "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3282419336, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 3282419336, "quantity": 10, "costs": [ { "itemHash": 3159615086, "quantity": 50000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 0, "failureIndexes": [], "augments": 256, "vendorItemIndex": 13, "itemHash": 3120705093, "quantity": 1, "costs": [] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 256, "vendorItemIndex": 14, "itemHash": 3120705092, "quantity": 1, "costs": [] }, "120": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 0, "vendorItemIndex": 120, "itemHash": 301726109, "quantity": 1, "costs": [] }, "123": { "saleStatus": 8, "failureIndexes": [ 6 ], "augments": 0, "vendorItemIndex": 123, "itemHash": 1903995905, "quantity": 1, "costs": [] }, "124": { "saleStatus": 8, "failureIndexes": [ 7 ], "augments": 0, "vendorItemIndex": 124, "itemHash": 3853748946, "quantity": 2, "costs": [] }, "126": { "saleStatus": 8, "failureIndexes": [ 8 ], "augments": 0, "vendorItemIndex": 126, "itemHash": 3282419336, "quantity": 5, "costs": [] }, "128": { "saleStatus": 8, "failureIndexes": [ 9 ], "augments": 0, "vendorItemIndex": 128, "itemHash": 3853748946, "quantity": 2, "costs": [] }, "130": { "saleStatus": 8, "failureIndexes": [ 10 ], "augments": 0, "vendorItemIndex": 130, "itemHash": 3282419336, "quantity": 5, "costs": [] }, "132": { "saleStatus": 8, "failureIndexes": [ 11 ], "augments": 0, "vendorItemIndex": 132, "itemHash": 353704689, "quantity": 1, "costs": [] }, "134": { "saleStatus": 8, "failureIndexes": [ 12 ], "augments": 0, "vendorItemIndex": 134, "itemHash": 2133694745, "quantity": 1, "costs": [] }, "142": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, false, true, true, true, true ], "vendorItemIndex": 142, "itemHash": 152476114, "quantity": 1, "costs": [] } } }, "1576276905": { "saleItems": { "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 137386025, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2956080815, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 374306366, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 3968652888, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 4258364155, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 12000, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 4135938415, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 2484637938, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 2484637939, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 2484637936, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "69482069": { "saleItems": { "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 4252280581, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1616736576, "quantity": 1, "costs": [] }, "29": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 40185715, "quantity": 1, "costs": [] }, "31": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 2110538051, "quantity": 1, "costs": [] } } }, "1976548992": { "saleItems": { "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 1968553944, "quantity": 1, "costs": [] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 1702926303, "quantity": 1, "costs": [] }, "21": { "saleStatus": 0, "failureIndexes": [], "augments": 256, "vendorItemIndex": 21, "itemHash": 3460746239, "quantity": 1, "costs": [] }, "39": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 39, "itemHash": 2209347473, "quantity": 1, "costs": [] }, "45": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 2506573994, "quantity": 1, "costs": [] }, "69": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ false, false, false, true, true, true ], "vendorItemIndex": 69, "itemHash": 1390863957, "quantity": 1, "costs": [] }, "80": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ false, false, false, true, true, true ], "vendorItemIndex": 80, "itemHash": 2744299504, "quantity": 1, "costs": [] }, "87": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ false, false, true, false, true, true ], "vendorItemIndex": 87, "itemHash": 3311748604, "quantity": 1, "costs": [] } } }, "3603221665": { "saleItems": { "18": { "saleStatus": 8, "failureIndexes": [ 5 ], "augments": 0, "vendorItemIndex": 18, "itemHash": 2680217528, "quantity": 1, "costs": [] }, "19": { "saleStatus": 8, "failureIndexes": [ 6 ], "augments": 0, "vendorItemIndex": 19, "itemHash": 2004142280, "quantity": 1, "costs": [] }, "20": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 3717501117, "quantity": 1, "costs": [] }, "23": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 3643304188, "quantity": 1, "costs": [] } } }, "2255782930": { "saleItems": { "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2137558813, "quantity": 1, "costs": [] }, "23": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378135, "quantity": 3, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378134, "quantity": 3, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378133, "quantity": 3, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378132, "quantity": 3, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378131, "quantity": 3, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378130, "quantity": 3, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378129, "quantity": 3, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378128, "quantity": 3, "hasConditionalVisibility": false } ] }, "31": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378143, "quantity": 3, "hasConditionalVisibility": false } ] }, "32": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3351378142, "quantity": 3, "hasConditionalVisibility": false } ] }, "33": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 33, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013970, "quantity": 3, "hasConditionalVisibility": false } ] }, "34": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013971, "quantity": 3, "hasConditionalVisibility": false } ] }, "35": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 35, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013968, "quantity": 3, "hasConditionalVisibility": false } ] }, "36": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 36, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013969, "quantity": 3, "hasConditionalVisibility": false } ] }, "37": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 37, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013974, "quantity": 3, "hasConditionalVisibility": false } ] }, "38": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 38, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013975, "quantity": 3, "hasConditionalVisibility": false } ] }, "39": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 39, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013972, "quantity": 3, "hasConditionalVisibility": false } ] }, "40": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 40, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013973, "quantity": 3, "hasConditionalVisibility": false } ] }, "41": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 41, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013978, "quantity": 3, "hasConditionalVisibility": false } ] }, "42": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 42, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 1281013979, "quantity": 3, "hasConditionalVisibility": false } ] }, "43": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 43, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115587, "quantity": 3, "hasConditionalVisibility": false } ] }, "44": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 44, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115586, "quantity": 3, "hasConditionalVisibility": false } ] }, "45": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115585, "quantity": 3, "hasConditionalVisibility": false } ] }, "46": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 46, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115584, "quantity": 3, "hasConditionalVisibility": false } ] }, "47": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 47, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115591, "quantity": 3, "hasConditionalVisibility": false } ] }, "48": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 48, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115590, "quantity": 3, "hasConditionalVisibility": false } ] }, "49": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 49, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115589, "quantity": 3, "hasConditionalVisibility": false } ] }, "50": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 50, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115588, "quantity": 3, "hasConditionalVisibility": false } ] }, "51": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 51, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115595, "quantity": 3, "hasConditionalVisibility": false } ] }, "52": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 52, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2153115594, "quantity": 3, "hasConditionalVisibility": false } ] }, "53": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 53, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3376967254, "quantity": 3, "hasConditionalVisibility": false } ] }, "54": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 54, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3376967255, "quantity": 3, "hasConditionalVisibility": false } ] }, "55": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 55, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2607476204, "quantity": 3, "hasConditionalVisibility": false } ] }, "56": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 56, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2607476205, "quantity": 3, "hasConditionalVisibility": false } ] }, "57": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 57, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2607476206, "quantity": 3, "hasConditionalVisibility": false } ] }, "58": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 58, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 2607476207, "quantity": 3, "hasConditionalVisibility": false } ] }, "59": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 59, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 691914261, "quantity": 3, "hasConditionalVisibility": false } ] }, "60": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 60, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3168164098, "quantity": 3, "hasConditionalVisibility": false } ] }, "61": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 61, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3947596543, "quantity": 3, "hasConditionalVisibility": false } ] }, "62": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 62, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 599687980, "quantity": 3, "hasConditionalVisibility": false } ] }, "63": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 63, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3012249671, "quantity": 3, "hasConditionalVisibility": false } ] }, "64": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 64, "itemHash": 3070068572, "quantity": 1, "costs": [ { "itemHash": 3012249670, "quantity": 3, "hasConditionalVisibility": false } ] }, "67": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 67, "itemHash": 2530045086, "quantity": 1, "costs": [] } } }, "672118013": { "saleItems": { "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 1246793994, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:01:00Z" }, "28": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 1999754402, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:01:00Z" }, "33": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 33, "itemHash": 3830941962, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:01:00Z" }, "38": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 38, "itemHash": 4200122994, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:01:00Z" }, "45": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 2857348871, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:01:00Z" }, "48": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 48, "itemHash": 882778888, "quantity": 1, "costs": [] }, "49": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 49, "itemHash": 3211624072, "quantity": 1, "costs": [] }, "50": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 50, "itemHash": 4169225313, "quantity": 1, "costs": [] }, "51": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 51, "itemHash": 1866778462, "quantity": 1, "costs": [] }, "52": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 52, "itemHash": 3381450498, "quantity": 1, "costs": [] }, "53": { "saleStatus": 8, "failureIndexes": [ 5 ], "augments": 0, "vendorItemIndex": 53, "itemHash": 1762785662, "quantity": 1, "costs": [] }, "54": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 54, "itemHash": 2556406641, "quantity": 1, "costs": [] } } }, "895295461": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1000378810, "quantity": 1, "costs": [] } } }, "2190858386": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3158812152, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 97, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 2405271938, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 41, "hasConditionalVisibility": false } ] }, "49": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 49, "itemHash": 1437375562, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 41, "hasConditionalVisibility": false } ] }, "83": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 83, "itemHash": 3864952146, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 41, "hasConditionalVisibility": false } ] }, "90": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 90, "itemHash": 1978467312, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 41, "hasConditionalVisibility": false } ] }, "98": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 98, "itemHash": 597039524, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 41, "hasConditionalVisibility": false } ] }, "102": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 102, "itemHash": 327385975, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 41, "hasConditionalVisibility": false } ] }, "133": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 133, "itemHash": 1540068657, "quantity": 1, "costs": [ { "itemHash": 800069450, "quantity": 41, "hasConditionalVisibility": false } ] }, "137": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 137, "itemHash": 1617663696, "quantity": 1, "costs": [] }, "138": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 138, "itemHash": 3670668729, "quantity": 1, "costs": [] }, "142": { "saleStatus": 8, "failureIndexes": [ 10 ], "augments": 0, "vendorItemIndex": 142, "itemHash": 2125848607, "quantity": 1, "costs": [] } } }, "1841717884": { "saleItems": { "4": { "saleStatus": 1, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 685299502, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 2500, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 810623803, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 1, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 771273473, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 2473252800, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 810623803, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 2367713531, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 3282419336, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 3337739523, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1500, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "21": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 1319723906, "quantity": 1, "costs": [ { "itemHash": 1633854071, "quantity": 40, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "22": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 2661223204, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 500, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "23": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 1001399024, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 500, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "25": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 2192699203, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 500, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "30": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 973983247, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 500, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "34": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 4095766153, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ], "apiPurchasable": true } } }, "248695599": { "saleItems": { "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 2262661348, "quantity": 1, "costs": [] } } }, "1796504621": { "saleItems": { "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 12, "itemHash": 3141979346, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "100": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 100, "itemHash": 2240152949, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "101": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 101, "itemHash": 3084282676, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "112": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 112, "itemHash": 2268523867, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" } } }, "1616085565": { "saleItems": { "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 993395957, "quantity": 1, "costs": [ { "itemHash": 937378714, "quantity": 1, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 2244757358, "quantity": 1, "costs": [ { "itemHash": 937378714, "quantity": 1, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 687082423, "quantity": 1, "costs": [ { "itemHash": 937378714, "quantity": 1, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 3799864144, "quantity": 1, "costs": [ { "itemHash": 937378714, "quantity": 1, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 1343473388, "quantity": 1, "costs": [ { "itemHash": 937378714, "quantity": 1, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 3643656807, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "39": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 39, "itemHash": 3540140709, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "49": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 49, "itemHash": 709535750, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "50": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 50, "itemHash": 709535749, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "58": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 58, "itemHash": 3597986949, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "59": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 59, "itemHash": 631065801, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "60": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 60, "itemHash": 1025057518, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ], "apiPurchasable": true } } }, "3411552308": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3836775970, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, false, false, true, true, true ], "vendorItemIndex": 1, "itemHash": 202169029, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, false, false, true, true, true ], "vendorItemIndex": 2, "itemHash": 3061679979, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, false, false, true, true, true ], "vendorItemIndex": 3, "itemHash": 3091479969, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, false, false, true, true, true ], "vendorItemIndex": 4, "itemHash": 3808316204, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, false, false, true, true, true ], "vendorItemIndex": 5, "itemHash": 4003211415, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 3424456965, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 4208016531, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 1335967964, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 1647306237, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 10, "itemHash": 327090183, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 11, "itemHash": 3999201698, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 12, "itemHash": 1330233961, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 2032932188, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 3536491683, "quantity": 1, "costs": [ { "itemHash": 443031983, "quantity": 1, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 443031982, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 443031983, "quantity": 1, "costs": [ { "itemHash": 443031982, "quantity": 20, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 400298584, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "19": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 400298585, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "28": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 2305954618, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "31": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 3993430445, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "32": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 1183240368, "quantity": 1, "costs": [ { "itemHash": 443031982, "quantity": 5, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "33": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 33, "itemHash": 1183240369, "quantity": 1, "costs": [ { "itemHash": 443031982, "quantity": 5, "hasConditionalVisibility": false } ], "apiPurchasable": true } } }, "765357505": { "saleItems": { "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 3644657562, "quantity": 1, "costs": [] } } }, "319449446": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3643533859, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2190201809, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 49565461, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 3357521860, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 4161594163, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 3053034812, "quantity": 1, "costs": [] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 8, "itemHash": 1126451529, "quantity": 1, "costs": [] } } }, "1932528548": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 4260353953, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 4260353952, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "2372049275": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2722641740, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2722641741, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "2268720627": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 4220332375, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 4220332374, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "261493107": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1380268166, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1380268167, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1380268164, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2494162583": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1602994568, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1602994569, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1602994570, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "3440426498": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 0, "itemHash": 2860908221, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 1, "itemHash": 405131479, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 2, "itemHash": 696469687, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 3, "itemHash": 2999892510, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 4161594163, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 3053034812, "quantity": 1, "costs": [] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 8, "itemHash": 1126451529, "quantity": 1, "costs": [] } } }, "307148624": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2722573683, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2722573682, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2722573681, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "418092119": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2816982784, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2816982785, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "1192173495": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1139822081, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2474196287": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 20616658, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 20616659, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 20616656, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2047883483": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 187655375, "quantity": 1, "costs": [] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 187655374, "quantity": 1, "costs": [] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 187655372, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 187655373, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "2117613039": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1504293982, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2211058946, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1144817194, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 2834992845, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 4161594163, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 3053034812, "quantity": 1, "costs": [] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 8, "itemHash": 1126451529, "quantity": 1, "costs": [] } } }, "2060371059": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1656118680, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1656118681, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1656118682, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "2856424422": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2209081649, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2209081648, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "2933806284": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2299867342, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "776690932": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1237488987, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1237488986, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1237488985, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 1237488984, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "117637110": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2321824284, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2321824285, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2321824287, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "3296235050": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2272984669, "quantity": 1, "costs": [] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2272984670, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2272984668, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 2661180601, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 2272984665, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 2272984664, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 2272984667, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 2272984666, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 2272984671, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 2272984657, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 2272984656, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 2661180600, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 2661180602, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 2661180603, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 3854948621, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 3854948620, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "1663272602": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1255073825, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2809141585, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2265076177, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 1016030582, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 1514173218, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 1547656727, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 3232422679, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "1816541247": { "saleItems": { "21": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 2749722041, "quantity": 1, "costs": [] }, "22": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 2565108508, "quantity": 1, "costs": [] }, "48": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 48, "itemHash": 1218497598, "quantity": 1, "costs": [] } } }, "3442679730": { "saleItems": { "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3053023275, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "20": { "saleStatus": 8, "failureIndexes": [ 13 ], "augments": 0, "vendorItemIndex": 20, "itemHash": 2965853200, "quantity": 1, "costs": [] }, "21": { "saleStatus": 8, "failureIndexes": [ 14 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 1068954417, "quantity": 1, "costs": [] }, "23": { "saleStatus": 8, "failureIndexes": [ 15 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 2979281381, "quantity": 3, "costs": [] }, "25": { "saleStatus": 8, "failureIndexes": [ 16 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 3853748946, "quantity": 3, "costs": [] }, "27": { "saleStatus": 8, "failureIndexes": [ 17 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 4257549984, "quantity": 2, "costs": [] }, "29": { "saleStatus": 8, "failureIndexes": [ 18 ], "augments": 0, "vendorItemIndex": 29, "itemHash": 1806492036, "quantity": 1, "costs": [] }, "31": { "saleStatus": 8, "failureIndexes": [ 19 ], "augments": 0, "vendorItemIndex": 31, "itemHash": 2133694745, "quantity": 1, "costs": [] }, "34": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 1475436129, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "44": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 44, "itemHash": 3539440756, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "47": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 47, "itemHash": 3539440755, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "52": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 52, "itemHash": 3522663042, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true } } }, "3431983428": { "saleItems": { "10": { "saleStatus": 8, "failureIndexes": [ 9 ], "augments": 0, "vendorItemIndex": 10, "itemHash": 1136139771, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 2777, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "11": { "saleStatus": 8, "failureIndexes": [ 9 ], "augments": 0, "vendorItemIndex": 11, "itemHash": 1136139768, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 2777, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "14": { "saleStatus": 8, "failureIndexes": [ 9 ], "augments": 0, "vendorItemIndex": 14, "itemHash": 1136139775, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 2777, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "17": { "saleStatus": 8, "failureIndexes": [ 9 ], "augments": 0, "vendorItemIndex": 17, "itemHash": 299576121, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7177, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T09:00:00Z", "apiPurchasable": true }, "20": { "saleStatus": 8, "failureIndexes": [ 9 ], "augments": 0, "vendorItemIndex": 20, "itemHash": 299576126, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7177, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T09:00:00Z", "apiPurchasable": true }, "24": { "saleStatus": 8, "failureIndexes": [ 9 ], "augments": 0, "vendorItemIndex": 24, "itemHash": 242688675, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 11777, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T09:00:00Z", "apiPurchasable": true } } }, "2531198101": { "saleItems": { "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2177336318, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1422463224, "quantity": 1, "costs": [] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 790092644, "quantity": 1, "costs": [] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 1943575435, "quantity": 1, "costs": [] }, "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 1425038744, "quantity": 1, "costs": [] }, "12": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 3849405672, "quantity": 1, "costs": [ { "itemHash": 2993288448, "quantity": 30, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 1339800266, "quantity": 1, "costs": [ { "itemHash": 2993288448, "quantity": 30, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 101950167, "quantity": 1, "costs": [ { "itemHash": 2993288448, "quantity": 30, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 1438578734, "quantity": 1, "costs": [ { "itemHash": 2993288448, "quantity": 30, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 1301601637, "quantity": 1, "costs": [ { "itemHash": 2993288448, "quantity": 30, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 1865638436, "quantity": 1, "costs": [ { "itemHash": 2993288448, "quantity": 50, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 1190558125, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "29": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 1606160575, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "45": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 3831027417, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "59": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 59, "itemHash": 1921694150, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "60": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 60, "itemHash": 1429228592, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "63": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 63, "itemHash": 1429228595, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ], "apiPurchasable": true } } }, "4254652401": { "saleItems": { "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1714959198, "quantity": 1, "costs": [] } } }, "3705882217": { "saleItems": { "1": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1732779010, "quantity": 1, "costs": [] }, "2": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 1225104535, "quantity": 1, "costs": [] }, "3": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 4152692808, "quantity": 1, "costs": [] }, "7": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 4200938286, "quantity": 1, "costs": [] }, "8": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 1089363843, "quantity": 1, "costs": [] }, "9": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 189395988, "quantity": 1, "costs": [] }, "13": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 3680626170, "quantity": 1, "costs": [] }, "14": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 4227781007, "quantity": 1, "costs": [] }, "15": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 2038691424, "quantity": 1, "costs": [] }, "19": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 1972332120, "quantity": 1, "costs": [] }, "20": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 149775185, "quantity": 1, "costs": [] }, "21": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 2861257618, "quantity": 1, "costs": [] }, "25": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 1712777075, "quantity": 1, "costs": [] }, "26": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 2876457246, "quantity": 1, "costs": [] }, "27": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 1579990173, "quantity": 1, "costs": [] }, "31": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 31, "itemHash": 2322700704, "quantity": 1, "costs": [] }, "32": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 32, "itemHash": 4020275385, "quantity": 1, "costs": [] }, "33": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 33, "itemHash": 3332521786, "quantity": 1, "costs": [] }, "37": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 37, "itemHash": 3711485041, "quantity": 1, "costs": [] }, "38": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 38, "itemHash": 1346645752, "quantity": 1, "costs": [] }, "39": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 39, "itemHash": 1137528903, "quantity": 1, "costs": [] } } }, "1660659508": { "saleItems": { "9": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 50121332, "quantity": 1, "costs": [] }, "10": { "saleStatus": 8, "failureIndexes": [ 3 ], "augments": 0, "vendorItemIndex": 10, "itemHash": 2258146754, "quantity": 1, "costs": [] }, "11": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 1777822281, "quantity": 1, "costs": [] }, "12": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 1383257114, "quantity": 1, "costs": [] }, "20": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 0, "vendorItemIndex": 20, "itemHash": 2003833922, "quantity": 1, "costs": [] }, "23": { "saleStatus": 8, "failureIndexes": [ 6 ], "augments": 0, "vendorItemIndex": 23, "itemHash": 465243501, "quantity": 1, "costs": [] }, "24": { "saleStatus": 8, "failureIndexes": [ 7 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 1094197864, "quantity": 1, "costs": [] }, "26": { "saleStatus": 8, "failureIndexes": [ 8 ], "augments": 0, "vendorItemIndex": 26, "itemHash": 3853748946, "quantity": 3, "costs": [] }, "28": { "saleStatus": 8, "failureIndexes": [ 9 ], "augments": 0, "vendorItemIndex": 28, "itemHash": 2979281381, "quantity": 3, "costs": [] }, "30": { "saleStatus": 8, "failureIndexes": [ 10 ], "augments": 0, "vendorItemIndex": 30, "itemHash": 4257549984, "quantity": 2, "costs": [] }, "32": { "saleStatus": 8, "failureIndexes": [ 11 ], "augments": 0, "vendorItemIndex": 32, "itemHash": 439240927, "quantity": 1, "costs": [] }, "34": { "saleStatus": 8, "failureIndexes": [ 12 ], "augments": 0, "vendorItemIndex": 34, "itemHash": 2133694745, "quantity": 1, "costs": [] }, "36": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 36, "itemHash": 1861221692, "quantity": 1, "costs": [] } } }, "3352059696": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 0, "itemHash": 3830966498, "quantity": 1, "costs": [] } } }, "2734167": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 0, "itemHash": 2327927756, "quantity": 1, "costs": [] }, "1": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 1, "itemHash": 3369947366, "quantity": 1, "costs": [] }, "2": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 2, "itemHash": 3369947367, "quantity": 1, "costs": [] }, "3": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 3, "itemHash": 3369947364, "quantity": 1, "costs": [] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 4, "itemHash": 3754741131, "quantity": 1, "costs": [] }, "5": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 5, "itemHash": 3754741130, "quantity": 1, "costs": [] }, "6": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 6, "itemHash": 3754741129, "quantity": 1, "costs": [] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 7, "itemHash": 3754741128, "quantity": 1, "costs": [] }, "8": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 8, "itemHash": 3754741135, "quantity": 1, "costs": [] }, "9": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 9, "itemHash": 3369947365, "quantity": 1, "costs": [] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 10, "itemHash": 3369947362, "quantity": 1, "costs": [] }, "11": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 11, "itemHash": 3369947363, "quantity": 1, "costs": [] }, "12": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 12, "itemHash": 3754741134, "quantity": 1, "costs": [] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 13, "itemHash": 3754741133, "quantity": 1, "costs": [] }, "14": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 14, "itemHash": 3754741132, "quantity": 1, "costs": [] }, "15": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 15, "itemHash": 3754741123, "quantity": 1, "costs": [] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 16, "itemHash": 3754741122, "quantity": 1, "costs": [] } } }, "2384113223": { "saleItems": { "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1699221452, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "2": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2809748888, "quantity": 1, "costs": [] }, "5": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 2979281381, "quantity": 5, "costs": [] }, "6": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 2390366526, "quantity": 1, "costs": [] }, "9": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 1660918514, "quantity": 1, "costs": [] }, "12": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 3853748946, "quantity": 5, "costs": [] }, "13": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 297296830, "quantity": 1, "costs": [] }, "14": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 2448111149, "quantity": 1, "costs": [] }, "15": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 3736494460, "quantity": 1, "costs": [] }, "16": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 2448111150, "quantity": 1, "costs": [] }, "17": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 17, "itemHash": 2695703187, "quantity": 1, "costs": [] }, "18": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 18, "itemHash": 2297736067, "quantity": 1, "costs": [] }, "21": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 2641682052, "quantity": 1, "costs": [] }, "22": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 3316852407, "quantity": 1, "costs": [] }, "25": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 2533555104, "quantity": 1, "costs": [] }, "49": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, true, true, true, true, true ], "vendorItemIndex": 49, "itemHash": 2808722827, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "50": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, true, true, true, true, true ], "vendorItemIndex": 50, "itemHash": 2808722828, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "56": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, true, true, true, true, true ], "vendorItemIndex": 56, "itemHash": 2893357938, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "69": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, true, true, true, true, true ], "vendorItemIndex": 69, "itemHash": 609421556, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true } } }, "296729347": { "saleItems": { "69": { "saleStatus": 1, "failureIndexes": [], "augments": 0, "vendorItemIndex": 69, "itemHash": 3467984096, "quantity": 1, "costs": [] }, "94": { "saleStatus": 1, "failureIndexes": [], "augments": 0, "vendorItemIndex": 94, "itemHash": 3467984096, "quantity": 1, "costs": [] }, "108": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 108, "itemHash": 2749722041, "quantity": 1, "costs": [] }, "109": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 109, "itemHash": 2565108508, "quantity": 1, "costs": [] }, "111": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 111, "itemHash": 4209233853, "quantity": 1, "costs": [] }, "112": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 112, "itemHash": 3717489385, "quantity": 1, "costs": [] }, "113": { "saleStatus": 1, "failureIndexes": [], "augments": 0, "vendorItemIndex": 113, "itemHash": 3467984096, "quantity": 1, "costs": [] }, "117": { "saleStatus": 1, "failureIndexes": [], "augments": 0, "vendorItemIndex": 117, "itemHash": 4257549985, "quantity": 5, "costs": [] }, "120": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 120, "itemHash": 3853748946, "quantity": 10, "costs": [] } } }, "3484140575": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1488322334, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2606684288, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 309561406, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 2936205479, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 4061712111, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 1026858435, "quantity": 1, "costs": [] } } }, "3884814177": { "saleItems": { "41": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, true, true, true, true, true ], "vendorItemIndex": 41, "itemHash": 3236557427, "quantity": 1, "costs": [] }, "44": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 44, "itemHash": 3586902510, "quantity": 1, "costs": [] } } }, "2776510816": { "saleItems": { "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 692805471, "quantity": 1, "costs": [] }, "25": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 2464189896, "quantity": 1, "costs": [] }, "33": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 33, "itemHash": 1789244740, "quantity": 1, "costs": [] }, "51": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 51, "itemHash": 1218497598, "quantity": 1, "costs": [] } } }, "4242059374": { "saleItems": { "48": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 48, "itemHash": 327090183, "quantity": 1, "costs": [] }, "50": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 50, "itemHash": 3999201698, "quantity": 1, "costs": [] }, "53": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 53, "itemHash": 1330233961, "quantity": 1, "costs": [] }, "226": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 226, "itemHash": 3117358110, "quantity": 1, "costs": [] } } }, "4230408743": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 4275530304, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 844651785, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2832451056, "quantity": 1, "costs": [] } } }, "272081343": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2907129556, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1331482397, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2362471601, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 4036115577, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 1864563948, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 3413074534, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 3580904580, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 150000, "hasConditionalVisibility": false }, { "itemHash": 3702027555, "quantity": 240, "hasConditionalVisibility": false } ] } } }, "3298600270": { "saleItems": { "1": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 347366834, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 1364093401, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 3588934839, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 417164956, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 3211806999, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 150000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 3973202132, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 3512014804, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 1201830623, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 2816212794, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 2376481550, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 150000, "hasConditionalVisibility": false }, { "itemHash": 3702027555, "quantity": 240, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 3110698812, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 150000, "hasConditionalVisibility": false }, { "itemHash": 3702027555, "quantity": 240, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 3317837688, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 150000, "hasConditionalVisibility": false }, { "itemHash": 3702027555, "quantity": 240, "hasConditionalVisibility": false } ] } } }, "1854671592": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2591746970, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3524313097, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2415517654, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 4017959782, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3824106094, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 776191470, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 1665952087, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 2357297366, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 1363238943, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 1853180924, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] } } }, "2209308523": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3460576091, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3260753130, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 603721696, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 3761898871, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 1833195496, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 125000, "hasConditionalVisibility": false } ] } } }, "1125969407": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1763584999, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1234150730, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 374573733, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 219145368, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] } } }, "67760598": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 4293613902, "quantity": 1, "costs": [] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3659414143, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 1912669214, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 940371471, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3821409356, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 17096506, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] } } }, "2996039207": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3561203890, "quantity": 1, "costs": [] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 427899681, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2350354266, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 3725585710, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] } } }, "399563869": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2581676735, "quantity": 1, "costs": [] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2973900274, "quantity": 1, "costs": [ { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 100000, "hasConditionalVisibility": false } ] } } }, "1092954315": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 153979396, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 153979399, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 150000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 1161276682, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 150000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 3993415705, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 150000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 3354242550, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 654608616, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 2697058914, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 1600633250, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 4227181568, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 838556752, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 3907337522, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 847329160, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 1179141605, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 4184808992, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 2060863616, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 1644680957, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 725408022, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 17, "itemHash": 3216652511, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 18, "itemHash": 3105930175, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 616582330, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 2276328320, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 3001205424, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 542573208, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 3337727085, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 3438139008, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 3276304504, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 4176873718, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 1824586582, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 4060882456, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 4060882457, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 4060882458, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "31": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 1132740487, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "32": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 1132740486, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "33": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 33, "itemHash": 1132740485, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "34": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 34, "itemHash": 4226042919, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "35": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 35, "itemHash": 4226042918, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "36": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 36, "itemHash": 4226042917, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "40": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 40, "itemHash": 2814111722, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "41": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 41, "itemHash": 2814111723, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "42": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 42, "itemHash": 2814111720, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "43": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 43, "itemHash": 1842527156, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "44": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 44, "itemHash": 1842527157, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "45": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 1842527158, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "49": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 49, "itemHash": 807109346, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "50": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 50, "itemHash": 807109347, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] }, "51": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 51, "itemHash": 807109344, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 75000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 1, "hasConditionalVisibility": false } ] } } }, "350061650": { "saleItems": { "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 1, "itemHash": 3950721485, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 540971012, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 3, "itemHash": 171866827, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "18": { "saleStatus": 8, "failureIndexes": [ 5 ], "augments": 0, "vendorItemIndex": 18, "itemHash": 3650581589, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "30": { "saleStatus": 8, "failureIndexes": [ 5 ], "augments": 0, "vendorItemIndex": 30, "itemHash": 1489178153, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "51": { "saleStatus": 8, "failureIndexes": [ 5 ], "augments": 0, "vendorItemIndex": 51, "itemHash": 1206746476, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "149": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 149, "itemHash": 1291141296, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "150": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 150, "itemHash": 1571425240, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "151": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 151, "itemHash": 391377141, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "152": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 152, "itemHash": 1395001913, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "153": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 153, "itemHash": 1649776522, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "406": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 406, "itemHash": 3789495745, "quantity": 1, "costs": [] }, "407": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 407, "itemHash": 2906304032, "quantity": 1, "costs": [] }, "408": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 408, "itemHash": 1129121391, "quantity": 1, "costs": [] }, "409": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 409, "itemHash": 391773191, "quantity": 1, "costs": [] }, "410": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 410, "itemHash": 2051791013, "quantity": 1, "costs": [] }, "411": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 411, "itemHash": 1689409158, "quantity": 1, "costs": [] } } }, "2199358137": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 0, "itemHash": 3664001560, "quantity": 10000, "costs": [ { "itemHash": 1633854071, "quantity": 10, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 1, "itemHash": 3664001560, "quantity": 10000, "costs": [ { "itemHash": 443031982, "quantity": 10, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 2, "itemHash": 3664001560, "quantity": 10000, "costs": [ { "itemHash": 2993288448, "quantity": 25, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 4257549984, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 10, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 4257549985, "quantity": 1, "costs": [ { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 50000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 353704689, "quantity": 1, "costs": [ { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 50000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 3106913645, "quantity": 1, "costs": [ { "itemHash": 3853748946, "quantity": 10, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 2500, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 3106913644, "quantity": 1, "costs": [ { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 252746229, "quantity": 1, "costs": [ { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 1812969468, "quantity": 5, "costs": [ { "itemHash": 4257549984, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 2500, "hasConditionalVisibility": false } ] } } }, "3895383279": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 0, "itemHash": 3936659015, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2505420496, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2163145080, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 3290511811, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 509902436, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 1536870767, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 3514192102, "quantity": 1, "costs": [] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 2635900721, "quantity": 1, "costs": [] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 2231281470, "quantity": 1, "costs": [] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 942668144, "quantity": 1, "costs": [] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 3019107947, "quantity": 1, "costs": [] }, "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 1756823706, "quantity": 1, "costs": [] }, "12": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 1038078082, "quantity": 1, "costs": [] } } }, "2572415929": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1606754441, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2323220807, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2323220806, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 1263740410, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1263740411, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 1263740408, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 1263740409, "quantity": 1, "costs": [] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 1263740414, "quantity": 1, "costs": [] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 1263740415, "quantity": 1, "costs": [] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 1263740412, "quantity": 1, "costs": [] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 1263740413, "quantity": 1, "costs": [] }, "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 1263740402, "quantity": 1, "costs": [] }, "12": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 1263740403, "quantity": 1, "costs": [] }, "13": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 1246962695, "quantity": 1, "costs": [] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 1246962694, "quantity": 1, "costs": [] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 1246962693, "quantity": 1, "costs": [] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 1246962692, "quantity": 1, "costs": [] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 1246962691, "quantity": 1, "costs": [] }, "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 1246962690, "quantity": 1, "costs": [] }, "19": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 1246962689, "quantity": 1, "costs": [] }, "20": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 3973184816, "quantity": 1, "costs": [] } } }, "3949128738": { "saleItems": { "0": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 4257549984, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 353704689, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1826656070, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 300, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 2001857187, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 300, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 800069450, "quantity": 7, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 3612161799, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 500, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 6, "itemHash": 825199458, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 500, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 1968811824, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 903043774, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 300, "hasConditionalVisibility": false } ] } } }, "2890273682": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3898674642, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 664533719, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 2473068537, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 1818257532, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 1035752481, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 1650573830, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 3524313097, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 1457434304, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 448266921, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 2939609184, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 3448612595, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 3448612594, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 3562520053, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 50, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 1576402082, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 50, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 2078915253, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 136, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 825357415, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "2890273683": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1607841495, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3001813128, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 4103152482, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 2356341309, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 3004625280, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 90191153, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 4017959782, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 2512921531, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 4191932814, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 2066724468, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 1402284586, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 1402284587, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 2416805974, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 1249781195, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 1693097638, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 1362221859, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 136, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 29, "itemHash": 4140860253, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628785": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2335697060, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 2409536749, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 2655878319, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 2350826186, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 4015910035, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 3578708308, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 776191470, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 3030850318, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 1661778308, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 3263140403, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 859354675, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 2471702601, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 2471702600, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 4166195598, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 50, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 341869371, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 50, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 3922035879, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 136, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 29, "itemHash": 1269179840, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628784": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3110007705, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3045302654, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 3889245656, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 2745739715, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 2574956338, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 3971891703, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 2357297366, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 372702809, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 1901005163, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 772166226, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 609666430, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 51755992, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 51755993, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 2801311442, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 3103387299, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 4104235240, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 136, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 29, "itemHash": 3594816059, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628787": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2996356515, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 4066671384, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 2526396498, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 3675842925, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 29159728, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 3228751873, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 3460576091, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 2673955019, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 3363948245, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 3414847998, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 1929437677, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 2086800026, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 2086800027, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 575614550, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 2223907273, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 136, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 629267288, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628786": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 593470670, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3592193383, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 2344999081, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 2227552940, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 32123025, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 2388099734, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 3260753130, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 970919108, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 2884151045, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 2528315727, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 2320367075, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 2320367074, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 1955679597, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 3886820084, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 136, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 2922021463, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628789": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 656399569, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3219469610, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 1875800772, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 3604279399, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 4233075550, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 1760291195, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 603721696, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 3499897239, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 1066617987, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 3549553800, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 2944573668, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 2944573669, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 895031268, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 1333212979, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 9, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 2104928736, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628788": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 679460028, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3149804137, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 3441745979, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 1658200446, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 411041775, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 2686000536, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 3761898871, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 1289563190, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 831981838, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 1352091900, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 3761187797, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 3761187796, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 528225971, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 1476747774, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 9, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 3404725755, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628791": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3668761594, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 1545024715, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 3061239773, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 3092577752, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 141592453, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 2246957026, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 1763584999, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 1337054696, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 2073952954, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 339904227, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 682653408, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 997314999, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 997314998, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 2425962977, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 3157549001, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 9, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 2082826653, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628790": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2924224095, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 2471652988, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 1474580870, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 3479925049, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 2311637476, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 1819090317, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 1234150730, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 634549439, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 3816300537, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 2989179500, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 1934353448, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 3195867806, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 3195867807, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 467976170, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 2027044330, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 493352427, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 9, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 29, "itemHash": 2523952013, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628793": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2154754936, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 2518482829, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 3189281871, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 3843346026, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 1637920051, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 3646077812, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 374573733, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 1302968378, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 3607696740, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 823286729, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 1879832548, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 2639800745, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 2639800744, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 3851215879, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 3861400487, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 382975121, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 9, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 29, "itemHash": 1561622944, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3444628792": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 4184039789, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 4200520990, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 324936184, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 1851004515, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 1214186834, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 832039575, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 219145368, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 140806329, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 1068790731, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 781932118, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 1880238324, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 2255551160, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 2255551161, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 2498169544, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 1025446168, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 1382109741, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 136, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 29, "itemHash": 3620647249, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3394295928": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2247673945, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3908759150, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 1696869992, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 413443795, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 1189447810, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 1667089639, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 3659414143, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 1490914249, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 2178429339, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 2626902095, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 863074675, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 3683636136, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 3683636137, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 772855992, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 3195333832, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 3376699138, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 9, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 29, "itemHash": 135598935, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3394295929": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1344671844, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 1025884317, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 2967052607, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 2686403354, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 3404596515, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 1078689732, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 1912669214, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 4017272394, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 1853085236, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 3838423725, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 104264659, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 99900185, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 99900184, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 763773239, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 1261044759, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 2919888381, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 9, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 29, "itemHash": 421047309, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3394295930": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 205224043, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 979425868, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 1625261302, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 876581481, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 4003261204, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 2092539613, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 3821409356, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 3775649487, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 409337865, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 502300339, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 3174376906, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 3387248782, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 3387248783, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 2722735642, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 984897498, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 3373806360, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 9, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 29, "itemHash": 1938084298, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3394295931": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3402219638, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 1620181147, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 494781965, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 1985154504, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 2904782325, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 1153828018, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 17096506, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 3059244728, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 2504786634, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 2846974501, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 23, "itemHash": 2901750059, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 24, "itemHash": 179876903, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 179876902, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 26, "itemHash": 241371537, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 59600177, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 28, "itemHash": 1396559464, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 1, "failureIndexes": [], "augments": 33554432, "vendorItemIndex": 29, "itemHash": 3405495772, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3394295932": { "saleItems": { "1": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3179415481, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1818107307, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 2297835694, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 584746015, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 3044904, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 3044905, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 300, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 427899681, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 818393196, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 478550150, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 2834883850, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 1833819184, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 3763571494, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 3888027476, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 3255310006, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 1106076229, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 1106076228, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 1106076231, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 30, "itemHash": 1767405859, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "31": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 1767405858, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "32": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 408004171, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "33": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 33, "itemHash": 3780960906, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "34": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 2283845662, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "35": { "saleStatus": 9, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 35, "itemHash": 980381603, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3394295933": { "saleItems": { "1": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2044265402, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 3725194740, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 2960861399, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 2642766862, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 4146568779, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 4146568778, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 300, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 2350354266, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 3941269100, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 3186756437, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 3177513121, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 3039049959, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 3676474804, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 1153955185, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 887402982, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 27, "itemHash": 3656515094, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 3656515092, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 3656515093, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 30, "itemHash": 185033940, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "31": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 185033941, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "32": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 2020056988, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "33": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 33, "itemHash": 357532844, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "34": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 4049234040, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "35": { "saleStatus": 1, "failureIndexes": [], "augments": 33554432, "vendorItemIndex": 35, "itemHash": 2469258004, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "3394295934": { "saleItems": { "1": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1357528247, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 2741605465, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 21782364, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 3068107265, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 2385145062, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 200, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 2385145063, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 300, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 3725585710, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 2018366981, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 2018965396, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 2569943136, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 1764758326, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 907724400, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 427144613, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 538751803, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 250, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 1731419283, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 50, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 1731419282, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 1731419281, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 30, "itemHash": 2133303453, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] }, "31": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 2133303452, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "32": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 1999764565, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 100, "hasConditionalVisibility": false } ] }, "33": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 33, "itemHash": 270222899, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "34": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 2398921588, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 150, "hasConditionalVisibility": false } ] }, "35": { "saleStatus": 136, "failureIndexes": [ 2 ], "augments": 128, "vendorItemIndex": 35, "itemHash": 1391246437, "quantity": 1, "costs": [ { "itemHash": 4041218086, "quantity": 0, "hasConditionalVisibility": false } ] } } }, "1021220385": { "saleItems": { "1": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2990500972, "quantity": 1, "costs": [] }, "4": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 2979281381, "quantity": 5, "costs": [] }, "5": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 2589262546, "quantity": 1, "costs": [] }, "8": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 1017542806, "quantity": 1, "costs": [] }, "11": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 3853748946, "quantity": 5, "costs": [] }, "12": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 347169983, "quantity": 1, "costs": [] }, "15": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 3297318847, "quantity": 1, "costs": [] }, "16": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 16, "itemHash": 3978678046, "quantity": 1, "costs": [] }, "17": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 17, "itemHash": 749765567, "quantity": 1, "costs": [] }, "19": { "saleStatus": 9, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 19, "itemHash": 1471199156, "quantity": 1, "costs": [] }, "20": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 20, "itemHash": 540339979, "quantity": 1, "costs": [] }, "21": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 21, "itemHash": 1781477733, "quantity": 1, "costs": [] }, "22": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 22, "itemHash": 1451723779, "quantity": 1, "costs": [] }, "25": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 25, "itemHash": 4069118310, "quantity": 1, "costs": [] }, "36": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 36, "itemHash": 1149357594, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ], "apiPurchasable": true }, "50": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 50, "itemHash": 1407750585, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z", "apiPurchasable": true }, "52": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, true, true, true, true, true ], "vendorItemIndex": 52, "itemHash": 901575783, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "56": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, true, true, true, true, true ], "vendorItemIndex": 56, "itemHash": 901575779, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "73": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, true, true, true, true, true ], "vendorItemIndex": 73, "itemHash": 3846956449, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true }, "82": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "itemValueVisibility": [ true, true, true, true, true, true ], "vendorItemIndex": 82, "itemHash": 1403685365, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-03T17:00:00Z", "apiPurchasable": true } } }, "1413212512": { "saleItems": { "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 1, "itemHash": 2099896459, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 3265994285, "quantity": 1, "costs": [] } } }, "1474045886": { "saleItems": { "24": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 1035468760, "quantity": 1, "costs": [] } } }, "714148153": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 557875470, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 4106000231, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 159156336, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 1427926040, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1164259767, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 2202602948, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 2798633009, "quantity": 1, "costs": [] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 4135827408, "quantity": 1, "costs": [] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 560892136, "quantity": 1, "costs": [] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 3852326007, "quantity": 1, "costs": [] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 3588609731, "quantity": 1, "costs": [] }, "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 1756052686, "quantity": 1, "costs": [] } } }, "811102248": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 1, "itemHash": 3521754087, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 3, "itemHash": 3052325065, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 5, "itemHash": 638728344, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 7, "itemHash": 2925298470, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 9, "itemHash": 505671629, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 11, "itemHash": 4217170772, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 13, "itemHash": 3175386398, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 15, "itemHash": 3307211867, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] } } }, "811102249": { "saleItems": { "1": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 1, "itemHash": 2732521327, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 3, "itemHash": 24639057, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 5, "itemHash": 1475463424, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 7, "itemHash": 3552072238, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 9, "itemHash": 386896293, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 11, "itemHash": 3958573263, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 13, "itemHash": 478238387, "quantity": 1, "costs": [ { "itemHash": 3702027555, "quantity": 20, "hasConditionalVisibility": false } ] } } }, "4060517507": { "saleItems": { "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 7, "itemHash": 3498489831, "quantity": 1, "costs": [] }, "8": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 8, "itemHash": 912845486, "quantity": 1, "costs": [] }, "11": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 11, "itemHash": 2540822598, "quantity": 1, "costs": [] } } }, "153857624": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 0, "itemHash": 4199055647, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 1, "itemHash": 2849508185, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "2": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 2, "itemHash": 293267476, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "3": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 3, "itemHash": 3173749166, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "4": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 4, "itemHash": 3120202721, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "15": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 15, "itemHash": 361848666, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "16": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 16, "itemHash": 217940910, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "17": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 17, "itemHash": 536035447, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "18": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 18, "itemHash": 2039591531, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "19": { "saleStatus": 8, "failureIndexes": [ 0, 1 ], "augments": 0, "vendorItemIndex": 19, "itemHash": 1618320532, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "30": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 1579969198, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "31": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 1579969198, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "32": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 1579969198, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "33": { "saleStatus": 8, "failureIndexes": [ 3 ], "augments": 0, "vendorItemIndex": 33, "itemHash": 1579969199, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "34": { "saleStatus": 8, "failureIndexes": [ 4 ], "augments": 0, "vendorItemIndex": 34, "itemHash": 1579969196, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" } } }, "1054786807": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 0, "itemHash": 380589816, "quantity": 1, "costs": [] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 2954865797, "quantity": 1, "costs": [] } } }, "1499949918": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3540391529, "quantity": 1, "costs": [] } } }, "1305220558": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 4174005337, "quantity": 1, "costs": [] } } }, "1664442954": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 4109798877, "quantity": 1, "costs": [] } } }, "3361454721": { "saleItems": { "12": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 1919716545, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "15": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 1386985379, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "18": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 363485652, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "19": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 363485654, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "20": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 363485653, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "21": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 642361517, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "22": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 1444566704, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "23": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 1444566705, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "24": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 3313112374, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "25": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 3850767908, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "26": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 3850767911, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "27": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 3850767910, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "30": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 1961738944, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "31": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 1961738947, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "32": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 1961738946, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "36": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 36, "itemHash": 39786808, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "37": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 37, "itemHash": 39786811, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "38": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 38, "itemHash": 39786810, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "40": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 40, "itemHash": 39786812, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "41": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 41, "itemHash": 2170420716, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1400, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "42": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 42, "itemHash": 330998921, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "43": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 43, "itemHash": 330998920, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "45": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 1019120849, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "46": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 46, "itemHash": 637444868, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "47": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 47, "itemHash": 2348243143, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1900, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "49": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 49, "itemHash": 1194864069, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1900, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "50": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 50, "itemHash": 1296882980, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1900, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "52": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 52, "itemHash": 1566388215, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "53": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 53, "itemHash": 3900509251, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "58": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 58, "itemHash": 1534363612, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "59": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 59, "itemHash": 3417548187, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "60": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 60, "itemHash": 202527151, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "61": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 61, "itemHash": 67652813, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "62": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 62, "itemHash": 806209283, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "63": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 63, "itemHash": 3393068241, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "65": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 65, "itemHash": 883215657, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "66": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 66, "itemHash": 2867613168, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "68": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 68, "itemHash": 3759922146, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "72": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 8388608, "vendorItemIndex": 72, "itemHash": 3692478656, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "73": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 8388608, "vendorItemIndex": 73, "itemHash": 3692478657, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "74": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 8388608, "vendorItemIndex": 74, "itemHash": 3692478658, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "75": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 75, "itemHash": 3087618155, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "76": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 76, "itemHash": 1717042024, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "77": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 77, "itemHash": 2546301093, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "78": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 78, "itemHash": 991808355, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "79": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 79, "itemHash": 1649773431, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "80": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 80, "itemHash": 100834013, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "81": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 81, "itemHash": 3055714494, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "84": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 84, "itemHash": 420564048, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "85": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 85, "itemHash": 420564049, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "86": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 86, "itemHash": 420564050, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "87": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 87, "itemHash": 1601510957, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "88": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 88, "itemHash": 1479573076, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "89": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 89, "itemHash": 1479573077, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "90": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 90, "itemHash": 3886583137, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "91": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 91, "itemHash": 939226694, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "92": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 92, "itemHash": 2145815824, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 3700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "93": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 93, "itemHash": 2145815825, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "94": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 94, "itemHash": 426902517, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "95": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 95, "itemHash": 183861275, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "96": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 96, "itemHash": 3106274811, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "97": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 97, "itemHash": 3063003415, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "98": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 98, "itemHash": 1888607907, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "99": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 99, "itemHash": 3105350133, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "100": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 100, "itemHash": 3738356606, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "103": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 103, "itemHash": 3738356605, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "106": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 106, "itemHash": 3402014742, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "107": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 107, "itemHash": 2816945469, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "110": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 110, "itemHash": 514698970, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "119": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 119, "itemHash": 3023677836, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "120": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 120, "itemHash": 3023677837, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "121": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 121, "itemHash": 3086026436, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 6000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "122": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 122, "itemHash": 3086026436, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "123": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 123, "itemHash": 3949992392, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "126": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 126, "itemHash": 1900498843, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "129": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 129, "itemHash": 1597227028, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "132": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 132, "itemHash": 2036788921, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 900, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "135": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 135, "itemHash": 648959945, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "138": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 138, "itemHash": 698205583, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "141": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 141, "itemHash": 1564547288, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "144": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 144, "itemHash": 2584577814, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 900, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "147": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 147, "itemHash": 664636061, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "150": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 150, "itemHash": 1245693283, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "153": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 153, "itemHash": 2333044507, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "156": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 156, "itemHash": 3495257075, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "159": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 159, "itemHash": 3637836092, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "162": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 162, "itemHash": 4253765446, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "163": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 163, "itemHash": 1442979870, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "175": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 175, "itemHash": 2514253483, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "176": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 176, "itemHash": 1862310405, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "177": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 177, "itemHash": 203343079, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "178": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 178, "itemHash": 1123887434, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "179": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 179, "itemHash": 3031640271, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "180": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 180, "itemHash": 2768155748, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "181": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 181, "itemHash": 1520805160, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "186": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 186, "itemHash": 3806882035, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "187": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 187, "itemHash": 2950397668, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "192": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 192, "itemHash": 210405799, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "197": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 197, "itemHash": 794569992, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "198": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 198, "itemHash": 3334797172, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "203": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 203, "itemHash": 3956218404, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "204": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 204, "itemHash": 1679002088, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "209": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 209, "itemHash": 3002234975, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "210": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 210, "itemHash": 779755286, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "215": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 215, "itemHash": 1745233901, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "216": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 216, "itemHash": 1154500559, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "221": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 221, "itemHash": 3115540150, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "222": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 222, "itemHash": 599904805, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "227": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 227, "itemHash": 1324885391, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "228": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 228, "itemHash": 3492105806, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "233": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 233, "itemHash": 2906309175, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "234": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 234, "itemHash": 2548770303, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "239": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 239, "itemHash": 2413288976, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "241": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 241, "itemHash": 3951784665, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "244": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 244, "itemHash": 1516829881, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "245": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 245, "itemHash": 1029444727, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "248": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 248, "itemHash": 2549526496, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "251": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 251, "itemHash": 3927691568, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "254": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 254, "itemHash": 2579031865, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "257": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 257, "itemHash": 331838657, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "262": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 262, "itemHash": 2783100859, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "263": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 263, "itemHash": 3211076406, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "267": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 267, "itemHash": 1397707366, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "268": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 268, "itemHash": 3542571269, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "273": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 273, "itemHash": 2580207276, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "274": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 274, "itemHash": 4192295468, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "278": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 278, "itemHash": 1702676167, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "282": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 282, "itemHash": 3628276006, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "283": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 283, "itemHash": 1391284237, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "284": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 284, "itemHash": 1244411540, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "291": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 291, "itemHash": 3972613793, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "292": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 292, "itemHash": 409092784, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "297": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 297, "itemHash": 3345029000, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "300": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 300, "itemHash": 3018855040, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "303": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 303, "itemHash": 1419050585, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "306": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 306, "itemHash": 3617188871, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "309": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 309, "itemHash": 657001809, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "312": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 312, "itemHash": 1379845313, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "315": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 315, "itemHash": 1599763643, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "318": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 318, "itemHash": 794397481, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "321": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 321, "itemHash": 3137705393, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "322": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 322, "itemHash": 3137705392, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "323": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 323, "itemHash": 3137705395, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "324": { "saleStatus": 3, "failureIndexes": [], "augments": 65536, "vendorItemIndex": 324, "itemHash": 3137705394, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "325": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 325, "itemHash": 3137705397, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "326": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 326, "itemHash": 3263694990, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "327": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 327, "itemHash": 3263694991, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "328": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 328, "itemHash": 4036491260, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "329": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 329, "itemHash": 2913094034, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "330": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 330, "itemHash": 2495830694, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "331": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 331, "itemHash": 1187026582, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "332": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 332, "itemHash": 3044988165, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "333": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 333, "itemHash": 4184628733, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "334": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 334, "itemHash": 131676177, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "335": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 335, "itemHash": 67519906, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "336": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 336, "itemHash": 1750041401, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "337": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 337, "itemHash": 1750041400, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "338": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 338, "itemHash": 1750041403, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "339": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 339, "itemHash": 1750041402, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "340": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 340, "itemHash": 1750041405, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "341": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 341, "itemHash": 4025877494, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "342": { "saleStatus": 2, "failureIndexes": [], "augments": 65536, "vendorItemIndex": 342, "itemHash": 4025877495, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "343": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 343, "itemHash": 4025877492, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "344": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 344, "itemHash": 4025877493, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "345": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 345, "itemHash": 4025877490, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "346": { "saleStatus": 2, "failureIndexes": [], "augments": 65536, "vendorItemIndex": 346, "itemHash": 4025877491, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "347": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 347, "itemHash": 1971006829, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "348": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 348, "itemHash": 1971006828, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "349": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 349, "itemHash": 1971006831, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "350": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 350, "itemHash": 1971006830, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "351": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 351, "itemHash": 1971006825, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "352": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 352, "itemHash": 1971006824, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "353": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 353, "itemHash": 3983024056, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "354": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 354, "itemHash": 3493548831, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "355": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 355, "itemHash": 2921308462, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "356": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 356, "itemHash": 2077407080, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "357": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 357, "itemHash": 4291600440, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "358": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 358, "itemHash": 2716681002, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "359": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 359, "itemHash": 1879625121, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1400, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "360": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 360, "itemHash": 1437648898, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "361": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 361, "itemHash": 3919593928, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "362": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 362, "itemHash": 715484621, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "363": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 363, "itemHash": 86755843, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "364": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 364, "itemHash": 1305811795, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "365": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 365, "itemHash": 428294954, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "366": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 366, "itemHash": 2193035132, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "367": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 367, "itemHash": 2193035133, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "368": { "saleStatus": 2, "failureIndexes": [], "augments": 65536, "vendorItemIndex": 368, "itemHash": 2193035128, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "369": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 369, "itemHash": 2193035129, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "370": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 370, "itemHash": 2193035130, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "371": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 371, "itemHash": 3361254703, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "372": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 372, "itemHash": 3361254699, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "373": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 373, "itemHash": 2653404785, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "374": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 374, "itemHash": 2653404784, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "375": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 375, "itemHash": 2001163201, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "376": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 376, "itemHash": 3988979188, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "377": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 377, "itemHash": 3363660171, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "378": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 378, "itemHash": 1392418732, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "379": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 379, "itemHash": 1944917696, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "380": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 380, "itemHash": 1944917699, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "381": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 381, "itemHash": 48058300, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "382": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 382, "itemHash": 48058301, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "383": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 383, "itemHash": 48058303, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "384": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 384, "itemHash": 48058296, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "385": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 385, "itemHash": 48058298, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "386": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 386, "itemHash": 965145933, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "387": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 387, "itemHash": 965145931, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "388": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 388, "itemHash": 1576539041, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "389": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 389, "itemHash": 1576539046, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "390": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 390, "itemHash": 4269714569, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "391": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 391, "itemHash": 4152333595, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "392": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 392, "itemHash": 8364902, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "393": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 393, "itemHash": 8364903, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "394": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 394, "itemHash": 8364901, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "395": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 395, "itemHash": 8364898, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "396": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 396, "itemHash": 8364899, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "397": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 397, "itemHash": 2818858343, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "398": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 398, "itemHash": 2818858336, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "399": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 399, "itemHash": 2818858339, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "400": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 400, "itemHash": 2652312625, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "401": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 401, "itemHash": 318560917, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "402": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 402, "itemHash": 3758110292, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "403": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 403, "itemHash": 2897103720, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "404": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 404, "itemHash": 3074849953, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "405": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 405, "itemHash": 2984800371, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "406": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 406, "itemHash": 551138056, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "407": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 407, "itemHash": 1604812628, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "408": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 408, "itemHash": 3269081043, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "409": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 409, "itemHash": 1084846546, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "410": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 410, "itemHash": 2813167325, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "411": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 411, "itemHash": 2936056926, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "412": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 412, "itemHash": 4058838904, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "413": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 413, "itemHash": 1277605939, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "414": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 414, "itemHash": 1380830409, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "415": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 415, "itemHash": 1059427817, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "416": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 416, "itemHash": 1345240784, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "417": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 417, "itemHash": 4008804369, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "418": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 418, "itemHash": 1649009138, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "419": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 419, "itemHash": 2764916656, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "420": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 420, "itemHash": 2972633230, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "421": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 421, "itemHash": 1960441750, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "422": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 422, "itemHash": 2609637836, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "423": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 423, "itemHash": 3178001277, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "424": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 424, "itemHash": 1426122122, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "425": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 425, "itemHash": 1812118660, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "426": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 426, "itemHash": 1282339605, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "427": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 427, "itemHash": 569609522, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "428": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 428, "itemHash": 2779903514, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "429": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 429, "itemHash": 3145596224, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "430": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 430, "itemHash": 1948715072, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "431": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 431, "itemHash": 3803099167, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "432": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 432, "itemHash": 2539359118, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "433": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 433, "itemHash": 2244041240, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "434": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 434, "itemHash": 4225634636, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "435": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 435, "itemHash": 620653272, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "436": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 436, "itemHash": 4067547603, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "437": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 437, "itemHash": 1780436207, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "438": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 438, "itemHash": 3356471746, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "439": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 439, "itemHash": 3013531032, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "440": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 440, "itemHash": 4173887325, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "441": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 441, "itemHash": 2063193740, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "442": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 442, "itemHash": 4100080019, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "443": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 443, "itemHash": 3853428074, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "444": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 444, "itemHash": 2402948371, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "445": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 445, "itemHash": 437618171, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "446": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 446, "itemHash": 1733558474, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "447": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 447, "itemHash": 2329953306, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "448": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 448, "itemHash": 452729034, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "449": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 449, "itemHash": 2134412545, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "450": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 450, "itemHash": 670974744, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "451": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 451, "itemHash": 210156316, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "452": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 452, "itemHash": 534742271, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "453": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 453, "itemHash": 2113116241, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "454": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 454, "itemHash": 3339775, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "455": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 455, "itemHash": 138477739, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "456": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 456, "itemHash": 1503941894, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "457": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 457, "itemHash": 2610892819, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "458": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 458, "itemHash": 3575125069, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "459": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 459, "itemHash": 489304975, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "460": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 460, "itemHash": 3494457418, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "461": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 461, "itemHash": 1787190130, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "462": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 462, "itemHash": 3341687311, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "463": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 463, "itemHash": 1396812221, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "464": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 464, "itemHash": 3804935529, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "465": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 465, "itemHash": 73064000, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "466": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 466, "itemHash": 1102224360, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "467": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 467, "itemHash": 467730932, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "468": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 468, "itemHash": 4073089126, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "469": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 469, "itemHash": 4118865571, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "470": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 470, "itemHash": 3671432115, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "471": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 471, "itemHash": 2763305228, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "472": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 472, "itemHash": 4013202920, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "473": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 473, "itemHash": 2919419217, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "474": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 474, "itemHash": 3546298141, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "475": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 475, "itemHash": 172636124, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "476": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 476, "itemHash": 4289552726, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "477": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 477, "itemHash": 3802939618, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "478": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 478, "itemHash": 1083910840, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "479": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 479, "itemHash": 1080813018, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "480": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 480, "itemHash": 3184365082, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "481": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 481, "itemHash": 2972088291, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "482": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 482, "itemHash": 1871823355, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "483": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 483, "itemHash": 3287498059, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "484": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 484, "itemHash": 1138782604, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "485": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 485, "itemHash": 1163145340, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "486": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 486, "itemHash": 3789077741, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "487": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 487, "itemHash": 2173404085, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 900, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "488": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 488, "itemHash": 403878807, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "489": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 489, "itemHash": 2757144557, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "490": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 490, "itemHash": 512458769, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "491": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 491, "itemHash": 525967465, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "492": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 492, "itemHash": 1716717668, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "493": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 493, "itemHash": 2920072631, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "494": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 494, "itemHash": 1742941198, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "495": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 495, "itemHash": 3142425843, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "496": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 496, "itemHash": 3673249931, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "497": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 497, "itemHash": 844227660, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "498": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 498, "itemHash": 2392937477, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "499": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 499, "itemHash": 3066440557, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "500": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 500, "itemHash": 3363257468, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "501": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 501, "itemHash": 1075867128, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "502": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 502, "itemHash": 468010077, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "503": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 503, "itemHash": 581152885, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "504": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 504, "itemHash": 2720745558, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "505": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 505, "itemHash": 2949710074, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "506": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 506, "itemHash": 114225848, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "507": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 507, "itemHash": 2954623542, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "508": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 508, "itemHash": 1141818699, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "509": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 509, "itemHash": 2971678083, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "510": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 510, "itemHash": 1868384995, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "511": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 511, "itemHash": 2991424415, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "512": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 512, "itemHash": 3046205073, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "513": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 513, "itemHash": 1532753063, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "514": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 514, "itemHash": 1180846762, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "515": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 515, "itemHash": 4116539322, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "516": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 516, "itemHash": 2468053986, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "517": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 517, "itemHash": 2478595126, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "518": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 518, "itemHash": 1339798067, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "519": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 519, "itemHash": 3855774718, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "520": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 520, "itemHash": 3991681308, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "521": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 521, "itemHash": 2272772784, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "522": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 522, "itemHash": 1173052383, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "523": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 523, "itemHash": 899855954, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "524": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 524, "itemHash": 3753648496, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "525": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 525, "itemHash": 3756164435, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "526": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 526, "itemHash": 314669087, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "527": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 527, "itemHash": 2267660625, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "528": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 528, "itemHash": 4231724739, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "529": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 529, "itemHash": 287295296, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "530": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 530, "itemHash": 568877018, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "531": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 531, "itemHash": 4074521886, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "532": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 532, "itemHash": 3802742250, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "533": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 533, "itemHash": 3900166436, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "534": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 534, "itemHash": 396425346, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "535": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 535, "itemHash": 1061828070, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "536": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 536, "itemHash": 4258327739, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "537": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 537, "itemHash": 3170445301, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "538": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 538, "itemHash": 3170445300, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "539": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 539, "itemHash": 3844549116, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "540": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 540, "itemHash": 3000111575, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "541": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 541, "itemHash": 1535205434, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "542": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 542, "itemHash": 2634511221, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "543": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 543, "itemHash": 4133608609, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "544": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 544, "itemHash": 1865779065, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "545": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 545, "itemHash": 1473300982, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "546": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 546, "itemHash": 3009090211, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "547": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 547, "itemHash": 1871253231, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "548": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 548, "itemHash": 3426600741, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "549": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 549, "itemHash": 1570516124, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "550": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 550, "itemHash": 476682455, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "551": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 551, "itemHash": 2990167832, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "552": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 552, "itemHash": 346961818, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "553": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 553, "itemHash": 1672094182, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "554": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 554, "itemHash": 1644978187, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "555": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 555, "itemHash": 1119316141, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "556": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 556, "itemHash": 2943778709, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "557": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 557, "itemHash": 2515377971, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "558": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 558, "itemHash": 3977461550, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "559": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 559, "itemHash": 3474614844, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "560": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 560, "itemHash": 3071174732, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "561": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 561, "itemHash": 3458554860, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "562": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 562, "itemHash": 2004366751, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "563": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 563, "itemHash": 4252160794, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "564": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 564, "itemHash": 1445854248, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "565": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 565, "itemHash": 2752106987, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "566": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 566, "itemHash": 1206295112, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "567": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 567, "itemHash": 2243729317, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "568": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 568, "itemHash": 2949664689, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "569": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 569, "itemHash": 1906359096, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "570": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 570, "itemHash": 1750365155, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "571": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 571, "itemHash": 1259278657, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "572": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 572, "itemHash": 2396888157, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "573": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 573, "itemHash": 1906359097, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "574": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 574, "itemHash": 2036459008, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "575": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 575, "itemHash": 2409312095, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "576": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 576, "itemHash": 3556728490, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "577": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 577, "itemHash": 3686084858, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "578": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 578, "itemHash": 2775242452, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "579": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 579, "itemHash": 2425317590, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "580": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 580, "itemHash": 2433900295, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "581": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 581, "itemHash": 2356548547, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "582": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 582, "itemHash": 3373357627, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "583": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 583, "itemHash": 20859173, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "584": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 584, "itemHash": 2412488653, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "585": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 585, "itemHash": 2583504679, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "586": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 586, "itemHash": 1630611952, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "587": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 587, "itemHash": 2110420504, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "588": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 588, "itemHash": 1748595581, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "589": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 589, "itemHash": 3520552272, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "590": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 590, "itemHash": 2673403667, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "591": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 591, "itemHash": 2240647682, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2900, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "592": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 592, "itemHash": 2600829518, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "593": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 593, "itemHash": 546447744, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "594": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 594, "itemHash": 1901918825, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "595": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 595, "itemHash": 2409025634, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "596": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 596, "itemHash": 1254437295, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "597": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 597, "itemHash": 1472624141, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "598": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 598, "itemHash": 2983128512, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "599": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 599, "itemHash": 820060201, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "600": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 600, "itemHash": 4033567948, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "733": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 733, "itemHash": 3187955025, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "734": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 734, "itemHash": 1583786617, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "735": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 735, "itemHash": 3135401183, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "736": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 736, "itemHash": 1633390039, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "737": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 737, "itemHash": 330696438, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "738": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 738, "itemHash": 2868451638, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "742": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 742, "itemHash": 3134396012, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "743": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 743, "itemHash": 1919716545, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "746": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 746, "itemHash": 3900509251, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "749": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 749, "itemHash": 1386985379, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "752": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 752, "itemHash": 1566388215, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "788": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 788, "itemHash": 1004583504, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "789": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 8388608, "vendorItemIndex": 789, "itemHash": 3692478656, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "790": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 8388608, "vendorItemIndex": 790, "itemHash": 3692478657, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "791": { "saleStatus": 10, "failureIndexes": [ 2 ], "augments": 8388608, "vendorItemIndex": 791, "itemHash": 3692478658, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "825": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 825, "itemHash": 2016900202, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "826": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 826, "itemHash": 1019120849, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "827": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 827, "itemHash": 637444868, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "828": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 828, "itemHash": 330998921, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "829": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 829, "itemHash": 330998920, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "853": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 853, "itemHash": 3053853945, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "854": { "saleStatus": 2, "failureIndexes": [], "augments": 4194304, "vendorItemIndex": 854, "itemHash": 1194864069, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1900, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "855": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 855, "itemHash": 3850767911, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 600, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "856": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 856, "itemHash": 1961738947, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "857": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 857, "itemHash": 39786811, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 800, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "873": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 873, "itemHash": 3759922146, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 2000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "891": { "saleStatus": 3, "failureIndexes": [], "augments": 0, "vendorItemIndex": 891, "itemHash": 1717042024, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "911": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 911, "itemHash": 330998920, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 1000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "925": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 925, "itemHash": 67652813, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "965": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 965, "itemHash": 2394501953, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 3250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "978": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 978, "itemHash": 1308779349, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 1200, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1003": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1003, "itemHash": 993333539, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 2500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1016": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1016, "itemHash": 1091850349, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 450, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1029": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1029, "itemHash": 1694958154, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1042": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1042, "itemHash": 1371145734, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1056": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1056, "itemHash": 2272950849, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 3250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1069": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1069, "itemHash": 695820637, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 700, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1094": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1094, "itemHash": 2222909940, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 2850, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1107": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1107, "itemHash": 2043847343, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 2000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1120": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1120, "itemHash": 1522633035, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 2500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1133": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1133, "itemHash": 1083910840, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 1250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1146": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1146, "itemHash": 143766269, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 1500, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1164": { "saleStatus": 2, "failureIndexes": [], "augments": 128, "vendorItemIndex": 1164, "itemHash": 2022851842, "quantity": 1, "costs": [ { "itemHash": 3147280338, "quantity": 6000, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1177": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1177, "itemHash": 2326811430, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1179": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1179, "itemHash": 117774540, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1180": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1180, "itemHash": 1437560966, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1181": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1181, "itemHash": 1131767961, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1184": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1184, "itemHash": 35031989, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1185": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1185, "itemHash": 1940446964, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1186": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1186, "itemHash": 3202647208, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1187": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1187, "itemHash": 2041259773, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1188": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1188, "itemHash": 47843291, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1189": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1189, "itemHash": 3279323753, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1190": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1190, "itemHash": 3752723910, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1191": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1191, "itemHash": 3819981545, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1192": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 128, "vendorItemIndex": 1192, "itemHash": 3853536719, "quantity": 1, "costs": [], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1223": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1223, "itemHash": 3906243541, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1226": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1226, "itemHash": 3906243542, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1228": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1228, "itemHash": 2885393053, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1255": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1255, "itemHash": 4103741401, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 300, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1343": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1343, "itemHash": 4258704674, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 450, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1373": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1373, "itemHash": 3951356827, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 450, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1390": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1390, "itemHash": 2353828567, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 450, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" }, "1455": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1455, "itemHash": 3187955025, "quantity": 1, "costs": [ { "itemHash": 2817410917, "quantity": 250, "hasConditionalVisibility": false } ], "overrideNextRefreshDate": "2025-12-09T17:00:00Z" } } }, "4195846091": { "saleItems": { "0": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 0, "itemHash": 3723851669, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 25, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1503186944, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 35, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 3500791139, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 25, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 366482347, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 25, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 1275523246, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 25, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 1396813375, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 25, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 2813099905, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 25, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 3820433909, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 25, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 1142145543, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 25, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 2694422776, "quantity": 1, "costs": [ { "itemHash": 4208534110, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 1526969819, "quantity": 25, "hasConditionalVisibility": false } ] } } }, "1420462601": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 171056525, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2406982452, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 82989367, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 634867754, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 3192017872, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 1441578852, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 2617636352, "quantity": 1, "costs": [] } } }, "2727390699": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 0, "itemHash": 420124571, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 1, "itemHash": 3656304720, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 2, "itemHash": 2339629205, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 3, "itemHash": 2146501422, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 1661857196, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 5, "itemHash": 3981572928, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 831160988, "quantity": 1, "costs": [] } } }, "4095855400": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2038397552, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2386378173, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1212397474, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 3850881195, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 2134525033, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 960191341, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 4136534361, "quantity": 1, "costs": [] } } }, "511784434": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2984351204, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2984351205, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2984351206, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "4137400834": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2495523340, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2495523341, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "3547377374": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 362132289, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1051276351, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 362132291, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 4180586736, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 362132293, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 362132292, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 362132295, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 362132294, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 362132288, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 362132290, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 362132301, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 362132300, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 1051276348, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 1051276349, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 1051276350, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 4180586737, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "3344881326": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2946990961, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3969294337, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2581086849, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 2216698406, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3199702642, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 1013086087, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 2202441959, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 1841016428, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2747252909": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 852252788, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 852252789, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "3904045300": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2225231092, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2225231093, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2225231094, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2357427271": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2747500761, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2747500760, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "574872862": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3066103998, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3066103999, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0, 2 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 3066103996, "quantity": 1, "costs": [] } } }, "3809168174": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3636300854, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3636300855, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 3636300852, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "2578999354": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0, 2 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 362132289, "quantity": 1, "costs": [] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1051276351, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 362132291, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 4180586736, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 362132293, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 362132292, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 362132295, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 362132294, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 362132288, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 362132290, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 362132301, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 362132300, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 1051276348, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 1051276349, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 1051276350, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 4180586737, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "3198105354": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2946990961, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3969294337, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2581086849, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 2216698406, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3199702642, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 1013086087, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 2202441959, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 1841016428, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "313845375": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 4016776974, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 4016776975, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 4016776972, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 4016776973, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2085188184": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1128768654, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1128768655, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 1128768652, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "3478784353": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 375052469, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 375052468, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 375052471, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "3146561831": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 83039195, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 83039194, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 83039193, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "3895124535": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2979486803, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2979486802, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2979486801, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "903440291": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 362132289, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1051276351, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 362132291, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 4180586736, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 362132293, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 362132292, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 362132295, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 362132294, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 362132288, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 362132290, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 362132301, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 362132300, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 1051276348, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 1051276349, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 1051276350, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 4180586737, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "1913609779": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2946990961, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3969294337, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2581086849, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 2216698406, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3199702642, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 1013086087, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 2202441959, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 1841016428, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "3728200412": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1470370539, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1470370538, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "862671345": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3686638443, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3686638442, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 3686638441, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2975168334": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2274196886, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2274196887, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "3484865102": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3171366294, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 256339607, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2709708596, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 230819033, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 790306059, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 3743476479, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 3097891051, "quantity": 1, "costs": [] } } }, "1480784244": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 0, "itemHash": 3457551080, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 1, "itemHash": 3647385875, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 2, "itemHash": 1450051422, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 3, "itemHash": 3676537005, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 1113974055, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 5, "itemHash": 3955863115, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 4033378567, "quantity": 1, "costs": [] } } }, "3588941765": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1656549672, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1656549673, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 2 ], "augments": 0, "vendorItemIndex": 2, "itemHash": 1656549674, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 1656549675, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "3303483917": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 489583096, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 489583097, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 489583098, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "1749390897": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0, 2 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1727069366, "quantity": 1, "costs": [] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1727069361, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 3277705907, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 1727069375, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 1727069367, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 1727069365, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 1727069364, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 1727069363, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 1727069362, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 1727069360, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 1727069374, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 3277705906, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 3277705904, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 3277705905, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 3478354817, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 3478354816, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "1672606033": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2994412667, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2481624867, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2909720723, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 1713935764, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 146194908, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 4198689901, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 1582574009, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "145159682": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2708585277, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2708585276, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2708585279, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "1292692259": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1698387814, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1698387815, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1698387812, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2840194832": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 119041299, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 119041298, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "2565786257": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 4194622036, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 4194622037, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 4194622038, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "1429791273": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 426473316, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 426473317, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "3049065213": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1727069366, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1727069361, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 3277705907, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 1727069375, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 1727069367, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 1727069365, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 1727069364, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 1727069363, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 1727069362, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 1727069360, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 1727069374, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 3277705906, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 3277705904, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 3277705905, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 3478354817, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 3478354816, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "871887901": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2994412667, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2481624867, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2909720723, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 1713935764, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 146194908, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 4198689901, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 1582574009, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "1677356040": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2716335211, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2716335210, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "1143379815": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 95544330, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 95544331, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 95544328, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 95544329, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "946015990": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3769507633, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3769507632, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "710977256": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1293395731, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1293395730, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1293395729, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 1293395728, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "3104830720": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 25156515, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 25156514, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "3947739156": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1727069366, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1727069361, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 3277705907, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 1727069375, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 1727069367, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 1727069365, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 1727069364, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 1727069363, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 1727069362, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 1727069360, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 1727069374, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 3277705906, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 3277705904, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 3277705905, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 3478354817, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 3478354816, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "311590836": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2994412667, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2481624867, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 2909720723, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 1713935764, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 146194908, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 4198689901, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 1582574009, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2091720363": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1232050830, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1232050831, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2382195382": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 4154539169, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 4154539168, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 4154539171, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "2718734409": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1081893460, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1081893461, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "3529215660": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1910728990, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1915726487, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1599177802, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 1791617935, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 118249117, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 725136991, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 2246548375, "quantity": 1, "costs": [] } } }, "1942263816": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 0, "itemHash": 3590881562, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 1, "itemHash": 1850421481, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 2, "itemHash": 1711682374, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 3, "itemHash": 1076058865, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 1954799527, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 5, "itemHash": 3241048449, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 2027656169, "quantity": 1, "costs": [] } } }, "2223896103": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2111743561, "quantity": 1, "costs": [] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1779294368, "quantity": 1, "costs": [] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 52771157, "quantity": 1, "costs": [] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 3324054408, "quantity": 1, "costs": [] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 4, "itemHash": 412547550, "quantity": 1, "costs": [] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 4001038552, "quantity": 1, "costs": [] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 128, "vendorItemIndex": 6, "itemHash": 190429600, "quantity": 1, "costs": [] } } }, "2634769701": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2031919265, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2031919264, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1563930741, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 3866705246, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "667903501": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3260056808, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3260056809, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "998725389": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3469412970, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3469412971, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 3469412968, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 3469412969, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3469412974, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 3469412975, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 537774540, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 537774541, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 537774542, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 537774543, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 2483898429, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 2483898428, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 2483898431, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 2483898430, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 2368990400, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 2368990401, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "1626995479": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1399217, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1399216, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 1399219, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "3743143700": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2028772231, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "46764309": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 469281040, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 469281041, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 469281042, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "183427272": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2021620139, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "84023067": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2934767477, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 2934767476, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 1920417385, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 4184589900, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "1629506227": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3933525366, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3933525367, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "1806740915": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3469412970, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3469412971, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 3469412968, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 3469412969, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3469412974, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 3469412975, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 537774540, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 537774541, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 537774542, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 537774543, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 2483898429, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 2483898428, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 2483898431, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 2483898430, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 2368990400, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 2368990401, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "1712713373": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1399217, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1399216, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 1399219, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "3360654976": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1341767667, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "1594205003": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3531427694, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3531427695, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 3531427692, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "3053846308": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 2625980631, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "2630013030": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 668903196, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 668903197, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2642597904, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 2651551055, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 5000, "hasConditionalVisibility": false } ] } } }, "3409789310": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3492047641, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3492047640, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] } } }, "1820924542": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 3469412970, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 3469412971, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 3469412968, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 3, "itemHash": 3469412969, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 4, "itemHash": 3469412974, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 5, "itemHash": 3469412975, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 6, "itemHash": 537774540, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 7, "itemHash": 537774541, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 8, "itemHash": 537774542, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 9, "itemHash": 537774543, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 10, "itemHash": 2483898429, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 11, "itemHash": 2483898428, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 12, "itemHash": 2483898431, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 13, "itemHash": 2483898430, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 14, "itemHash": 2368990400, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 15, "itemHash": 2368990401, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 10000, "hasConditionalVisibility": false } ] } } }, "639852168": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 0, "itemHash": 1399217, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 1, "itemHash": 1399216, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 128, "vendorItemIndex": 2, "itemHash": 1399219, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "875061823": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2543177538, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "3194559894": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 890263313, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 890263312, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 890263315, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] } } }, "1890828307": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3683904166, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 7500, "hasConditionalVisibility": false } ] } } }, "683678992": { "saleItems": { "0": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2003638177, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1730148720, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2871721702, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 3346371707, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 46196714, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 2145353828, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 1, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 4130251279, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 2091225008, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 2843072129, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 17, "itemHash": 789415529, "quantity": 1, "costs": [ { "itemHash": 3036656991, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] } } }, "2595490586": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3705995974, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3237668309, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2060830238, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 1125545953, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1428449496, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 3034681814, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 3054751781, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 3881082126, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 1064943697, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 220051784, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 2997491826, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "31": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 2889504953, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "32": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 792320746, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "33": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 33, "itemHash": 3204561957, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "34": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 3716635620, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "45": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 2733974969, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "46": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 46, "itemHash": 2165423482, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "47": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 47, "itemHash": 1900451399, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "48": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 48, "itemHash": 2748982312, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "49": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 49, "itemHash": 1669660339, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "60": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 60, "itemHash": 4130251279, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "61": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 61, "itemHash": 2091225008, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "62": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 62, "itemHash": 2843072129, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "63": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 63, "itemHash": 1087404526, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "64": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 64, "itemHash": 789415529, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "75": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 75, "itemHash": 1516075651, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "76": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 76, "itemHash": 2898525497, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "77": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 77, "itemHash": 3534797310, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "78": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 78, "itemHash": 3776837386, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "79": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 79, "itemHash": 154412394, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "80": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 80, "itemHash": 285956507, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "81": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 81, "itemHash": 264944861, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "82": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 82, "itemHash": 2028217633, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "83": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 83, "itemHash": 3228993066, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "84": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 84, "itemHash": 3960749708, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "85": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 85, "itemHash": 2154456873, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "86": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 86, "itemHash": 2852649264, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "87": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 87, "itemHash": 325881855, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "88": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 88, "itemHash": 2003638177, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "89": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 89, "itemHash": 1730148720, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "90": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 90, "itemHash": 2871721702, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "91": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 91, "itemHash": 3346371707, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "92": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 92, "itemHash": 46196714, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "93": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 93, "itemHash": 2145353828, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] } } }, "654952868": { "saleItems": { "0": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 173152368, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3217523599, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 3940601453, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 3776165335, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 791810866, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 3555286847, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 1, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 6, "itemHash": 2849111819, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 7, "itemHash": 2262455308, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 3484903008, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 9, "itemHash": 380543246, "quantity": 1, "costs": [ { "itemHash": 451915085, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] } } }, "2906014866": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1024892034, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3556063855, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2718271926, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 1552702991, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1529835198, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 3460423082, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 3930115709, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 2187992932, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 1961759511, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 953703634, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 479072281, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 1998829404, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 2642969363, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 173152368, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 3217523599, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 3940601453, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 3776165335, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 791810866, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 3555286847, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 3639816879, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 1896335568, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 207548065, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 3368700430, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 1762083721, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "34": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 941282909, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "35": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 35, "itemHash": 1413646678, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "36": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 36, "itemHash": 2967958507, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "37": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 37, "itemHash": 3293679620, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "38": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 38, "itemHash": 3119153287, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "59": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 59, "itemHash": 415227049, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "60": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 60, "itemHash": 2432853034, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "61": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 61, "itemHash": 767948183, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "62": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 62, "itemHash": 3870549112, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "63": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 63, "itemHash": 3924951683, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "74": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 74, "itemHash": 4198182657, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "75": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 75, "itemHash": 3729506850, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "76": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 76, "itemHash": 1588913887, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "77": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 77, "itemHash": 2292621312, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "78": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 78, "itemHash": 3749148603, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "89": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 89, "itemHash": 2307892413, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "90": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 90, "itemHash": 420543798, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "91": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 91, "itemHash": 3599713355, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "92": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 92, "itemHash": 2950694244, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "93": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 93, "itemHash": 683632999, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "104": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 104, "itemHash": 3876012135, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "105": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 105, "itemHash": 2669950408, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "106": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 106, "itemHash": 2772236585, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "107": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 107, "itemHash": 3796222038, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "108": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 108, "itemHash": 709164353, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "109": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 109, "itemHash": 2859670382, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "110": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 110, "itemHash": 3809684989, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "111": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 111, "itemHash": 2677528310, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "112": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 112, "itemHash": 3812440761, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "113": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 113, "itemHash": 405311520, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "124": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 124, "itemHash": 2262455308, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "125": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 125, "itemHash": 2849111819, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "126": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 126, "itemHash": 3484903008, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "127": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 127, "itemHash": 3299723351, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "128": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 128, "itemHash": 380543246, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] } } }, "2232145065": { "saleItems": { "0": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2363653, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 3076072398, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1842623742, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 356432935, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 827599943, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 1140943142, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 1, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 6, "itemHash": 2495124174, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 5, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 7, "itemHash": 593699562, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 5, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 8, "itemHash": 2398130369, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 5, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 9, "itemHash": 4001764690, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 5, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 10, "itemHash": 2730887956, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 5, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 11, "itemHash": 782991879, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 5, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 366482347, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 5, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 1568976227, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 5, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 14, "itemHash": 3828502736, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 5, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 366482347, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3643918802, "quantity": 1, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 10, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 23, "itemHash": 3828502736, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3643918802, "quantity": 1, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 27, "itemHash": 4023039160, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 50000, "hasConditionalVisibility": false }, { "itemHash": 3643918802, "quantity": 10, "hasConditionalVisibility": false } ] }, "32": { "saleStatus": 10, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 32, "itemHash": 3189110091, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 50000, "hasConditionalVisibility": false }, { "itemHash": 3643918802, "quantity": 10, "hasConditionalVisibility": false } ] }, "41": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 41, "itemHash": 3562567620, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "42": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 42, "itemHash": 819773640, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "43": { "saleStatus": 2, "failureIndexes": [], "augments": 0, "vendorItemIndex": 43, "itemHash": 487137411, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] }, "44": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 44, "itemHash": 2057542406, "quantity": 1, "costs": [ { "itemHash": 554159122, "quantity": 3, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false } ] } } }, "3444362755": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3649173192, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 609243450, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 284985776, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 764975752, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 816003836, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 4066300387, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 2164131193, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 2029585884, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 91766129, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 2631606272, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 1488099192, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 205680223, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 4280633975, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 2363653, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 3076072398, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 1842623742, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 356432935, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 827599943, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 1140943142, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "56": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 56, "itemHash": 2724714408, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "57": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 57, "itemHash": 378242631, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "58": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 58, "itemHash": 1923654025, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "59": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 59, "itemHash": 4141497761, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "60": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 60, "itemHash": 2947742646, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "71": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 71, "itemHash": 1549471512, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "72": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 72, "itemHash": 3384059383, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "73": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 73, "itemHash": 3074765785, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "74": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 74, "itemHash": 2042885489, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "75": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 75, "itemHash": 4096757830, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "86": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 86, "itemHash": 2467867662, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "87": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 87, "itemHash": 2691087957, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "88": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 88, "itemHash": 1896884883, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "89": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 89, "itemHash": 1149271199, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "90": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 90, "itemHash": 1869654860, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "101": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 101, "itemHash": 2182452253, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "102": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 102, "itemHash": 1232437646, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "103": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 103, "itemHash": 253950230, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "104": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 104, "itemHash": 3815376960, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "105": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 105, "itemHash": 4195360985, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "116": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 116, "itemHash": 487137411, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "117": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 117, "itemHash": 3562567620, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "118": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 118, "itemHash": 819773640, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "119": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 119, "itemHash": 2057542406, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "120": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 120, "itemHash": 1166254911, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] } } }, "502095006": { "saleItems": { "0": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 0, "itemHash": 4028427880, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 1, "itemHash": 850834881, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 2, "itemHash": 1816364190, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 3, "itemHash": 3200434396, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 4, "itemHash": 1551464092, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 5, "itemHash": 3500791139, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 6, "itemHash": 2175841482, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 7, "itemHash": 4155923794, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 20000, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 8, "itemHash": 3223924017, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 20000, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 9, "itemHash": 40290223, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 20000, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 10, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 10, "itemHash": 3960914123, "quantity": 1, "costs": [ { "itemHash": 2068842367, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 20000, "hasConditionalVisibility": false } ] } } }, "4140351452": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 2854025240, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2059601679, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1629007932, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 338446411, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1643656410, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 183582177, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 1224060546, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 3939284543, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 1240056416, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 3287081371, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 4236667838, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "31": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 3891912205, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "32": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 3382832422, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "33": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 33, "itemHash": 3771606505, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "34": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 891829968, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "45": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 3281352910, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "46": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 46, "itemHash": 2742898269, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "47": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 47, "itemHash": 3498657110, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "48": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 48, "itemHash": 2445219609, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "49": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 49, "itemHash": 1569427840, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "60": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 60, "itemHash": 3223924017, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "61": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 61, "itemHash": 4155923794, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "62": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 62, "itemHash": 40290223, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "63": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 63, "itemHash": 3114052816, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "64": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 64, "itemHash": 3960914123, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] } } }, "2672927612": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 1290536011, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 2413746134, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 2617594621, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 1578915183, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 1169027282, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 2183966710, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 1732065014, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 3675599216, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 508533863, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 2461407681, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 1103701935, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 3122430471, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 2720725271, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 3459488441, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 2754445092, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 654848542, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 2354898110, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 279354191, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 1408322960, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 872190708, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 3489910822, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 3588527091, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 2640011174, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 683374858, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 1715740932, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 1418030349, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 878104499, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 25000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 3742527360, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 3144750103, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 4079614900, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 2551249699, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "31": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 966848994, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "42": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 42, "itemHash": 1726065449, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "43": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 43, "itemHash": 3743691434, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "44": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 44, "itemHash": 2902329623, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "45": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 1709963256, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "46": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 46, "itemHash": 940822787, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "57": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 57, "itemHash": 152550357, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "58": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 58, "itemHash": 4224297358, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "59": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 59, "itemHash": 3215932179, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "60": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 60, "itemHash": 3188702156, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "61": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 61, "itemHash": 2905700895, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "72": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 72, "itemHash": 3157594934, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "73": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 73, "itemHash": 2410737925, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "74": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 74, "itemHash": 1901196910, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "75": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 75, "itemHash": 3063795377, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "76": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 76, "itemHash": 1193316520, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "87": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 87, "itemHash": 1059479825, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "88": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 88, "itemHash": 2733913394, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "89": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 89, "itemHash": 2233602319, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "90": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 90, "itemHash": 2159167152, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "91": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 91, "itemHash": 3731724843, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "102": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 102, "itemHash": 1809794768, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "103": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 103, "itemHash": 717710567, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "104": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 104, "itemHash": 720153764, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "105": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 105, "itemHash": 667818643, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "106": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 106, "itemHash": 2271876370, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "117": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 117, "itemHash": 179758314, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "118": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 118, "itemHash": 4025410673, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "119": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 119, "itemHash": 1137434578, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "120": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 120, "itemHash": 2826830765, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "121": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 121, "itemHash": 2851870940, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "132": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 132, "itemHash": 1734076444, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "133": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 133, "itemHash": 85648155, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "134": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 134, "itemHash": 2349487312, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "135": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 135, "itemHash": 66527815, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "136": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 136, "itemHash": 2111030142, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] } } }, "2435958557": { "saleItems": { "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 809709120, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 12, "itemHash": 1444163150, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 13, "itemHash": 1828328977, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 1034058285, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 737428704, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 3403264960, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 3744426107, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 3887291648, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 3298006530, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 4039633424, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 2423294139, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 1585868599, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 588963177, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 2281936901, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 1373116678, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 11400735, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 4273639393, "quantity": 1, "costs": [ { "itemHash": 3159615086, "quantity": 15000, "hasConditionalVisibility": false }, { "itemHash": 3853748946, "quantity": 3, "hasConditionalVisibility": false } ] } } }, "1248953136": { "saleItems": { "0": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 0, "itemHash": 4035598116, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 768898035, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 2, "itemHash": 1625737280, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 8, "failureIndexes": [ 0 ], "augments": 0, "vendorItemIndex": 3, "itemHash": 2281196142, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 2202933645, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 5, "itemHash": 2310030674, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 6, "itemHash": 3772721368, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 7, "itemHash": 3244195962, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "8": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 8, "itemHash": 3796515603, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "9": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 9, "itemHash": 323254371, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "10": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 10, "itemHash": 328238764, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "11": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 11, "itemHash": 2987270856, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "12": { "saleStatus": 8, "failureIndexes": [ 3 ], "augments": 0, "vendorItemIndex": 12, "itemHash": 2478441249, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "13": { "saleStatus": 8, "failureIndexes": [ 3 ], "augments": 0, "vendorItemIndex": 13, "itemHash": 151059116, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 30000, "hasConditionalVisibility": false }, { "itemHash": 4257549984, "quantity": 10, "hasConditionalVisibility": false } ] }, "14": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 14, "itemHash": 3969198262, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "15": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 15, "itemHash": 3139852187, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 16, "itemHash": 3844627809, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 17, "itemHash": 930044848, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 18, "itemHash": 2968238583, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 19, "itemHash": 1242518736, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 20, "itemHash": 3688579389, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "21": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 21, "itemHash": 1096864740, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "22": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 22, "itemHash": 1892687578, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "23": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 23, "itemHash": 814527257, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "24": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 24, "itemHash": 3570710433, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "25": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 25, "itemHash": 1844990602, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "26": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 26, "itemHash": 1059520422, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "27": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 27, "itemHash": 1436391764, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "28": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 28, "itemHash": 3406420059, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "29": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 29, "itemHash": 2279050881, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "30": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 30, "itemHash": 917501872, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "31": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 31, "itemHash": 2000707632, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "32": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 32, "itemHash": 2016003826, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "33": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 33, "itemHash": 1282098120, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "34": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 34, "itemHash": 3539478895, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "35": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 35, "itemHash": 2439112524, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "36": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 36, "itemHash": 4175277987, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "37": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 37, "itemHash": 733560210, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "38": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 38, "itemHash": 642983430, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "39": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 39, "itemHash": 1786438829, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "40": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 40, "itemHash": 859789177, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "41": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 41, "itemHash": 3827474196, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "42": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 42, "itemHash": 2998475016, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "43": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 43, "itemHash": 1351987111, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "44": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 44, "itemHash": 691707870, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "45": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 45, "itemHash": 1538369423, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "46": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 46, "itemHash": 949449009, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "47": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 47, "itemHash": 3462471786, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "48": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 48, "itemHash": 3013156849, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "49": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 49, "itemHash": 1874604139, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "50": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 50, "itemHash": 2015733300, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "51": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 51, "itemHash": 4223536259, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "52": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 52, "itemHash": 308335642, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "53": { "saleStatus": 8, "failureIndexes": [ 8 ], "augments": 0, "vendorItemIndex": 53, "itemHash": 852885104, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "54": { "saleStatus": 8, "failureIndexes": [ 8 ], "augments": 0, "vendorItemIndex": 54, "itemHash": 2880450203, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "55": { "saleStatus": 8, "failureIndexes": [ 8 ], "augments": 0, "vendorItemIndex": 55, "itemHash": 2851422678, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "56": { "saleStatus": 8, "failureIndexes": [ 8 ], "augments": 0, "vendorItemIndex": 56, "itemHash": 1951308312, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "58": { "saleStatus": 8, "failureIndexes": [ 8 ], "augments": 0, "vendorItemIndex": 58, "itemHash": 3548628854, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "59": { "saleStatus": 8, "failureIndexes": [ 8 ], "augments": 0, "vendorItemIndex": 59, "itemHash": 1781607626, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "60": { "saleStatus": 8, "failureIndexes": [ 8 ], "augments": 0, "vendorItemIndex": 60, "itemHash": 3538991101, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 60000, "hasConditionalVisibility": false }, { "itemHash": 4257549985, "quantity": 2, "hasConditionalVisibility": false } ] }, "155": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 155, "itemHash": 3969198262, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "156": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 156, "itemHash": 3139852187, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "157": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 157, "itemHash": 3844627809, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "158": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 158, "itemHash": 930044848, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "159": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 159, "itemHash": 2968238583, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "160": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 160, "itemHash": 1242518736, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "161": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 161, "itemHash": 3688579389, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "162": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 162, "itemHash": 1096864740, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "163": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 163, "itemHash": 1892687578, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "164": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 164, "itemHash": 814527257, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "165": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 165, "itemHash": 3570710433, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "166": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 166, "itemHash": 1844990602, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "167": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 167, "itemHash": 1059520422, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "168": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 168, "itemHash": 1436391764, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "169": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 169, "itemHash": 3406420059, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "170": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 170, "itemHash": 2279050881, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "171": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 171, "itemHash": 917501872, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "172": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 172, "itemHash": 2000707632, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "173": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 173, "itemHash": 2016003826, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "174": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 174, "itemHash": 1282098120, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "175": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 175, "itemHash": 3539478895, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "176": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 176, "itemHash": 2439112524, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "177": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 177, "itemHash": 4175277987, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "178": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 178, "itemHash": 733560210, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "179": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 179, "itemHash": 642983430, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "180": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 180, "itemHash": 1786438829, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "181": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 181, "itemHash": 859789177, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "182": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 182, "itemHash": 3827474196, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "183": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 183, "itemHash": 2998475016, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "184": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 184, "itemHash": 1351987111, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "185": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 185, "itemHash": 691707870, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "186": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 186, "itemHash": 1538369423, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "187": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 187, "itemHash": 949449009, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "188": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 188, "itemHash": 3462471786, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "189": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 189, "itemHash": 3013156849, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "190": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 190, "itemHash": 1874604139, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "191": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 191, "itemHash": 2015733300, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "192": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 192, "itemHash": 4223536259, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "193": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 193, "itemHash": 308335642, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "194": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 194, "itemHash": 852885104, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "195": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 195, "itemHash": 2880450203, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "196": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 196, "itemHash": 2851422678, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "197": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 197, "itemHash": 1951308312, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] }, "199": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 199, "itemHash": 3548628854, "quantity": 1, "costs": [ { "itemHash": 903043774, "quantity": 1, "hasConditionalVisibility": false }, { "itemHash": 3467984096, "quantity": 1, "hasConditionalVisibility": false } ] } } }, "444807002": { "saleItems": { "0": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 0, "itemHash": 3254804681, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 4, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "1": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 1, "itemHash": 1757424586, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 4, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "2": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 2, "itemHash": 1937611370, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 4, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "3": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 3, "itemHash": 3537285454, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 4, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "4": { "saleStatus": 0, "failureIndexes": [], "augments": 0, "vendorItemIndex": 4, "itemHash": 4061367591, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 4, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "5": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 5, "itemHash": 1221110111, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 4, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "6": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 6, "itemHash": 1965867309, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 4, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "7": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 7, "itemHash": 839115995, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 4, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 3000, "hasConditionalVisibility": false } ] }, "16": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 16, "itemHash": 3446623301, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 2, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 2000, "hasConditionalVisibility": false } ] }, "17": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 17, "itemHash": 1258973771, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 2, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 2000, "hasConditionalVisibility": false } ] }, "18": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 18, "itemHash": 2607072210, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 2, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 1000, "hasConditionalVisibility": false } ] }, "19": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 19, "itemHash": 2069829085, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 2, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 2000, "hasConditionalVisibility": false } ] }, "20": { "saleStatus": 8, "failureIndexes": [ 1 ], "augments": 0, "vendorItemIndex": 20, "itemHash": 2609207620, "quantity": 1, "costs": [ { "itemHash": 1094197864, "quantity": 2, "hasConditionalVisibility": false }, { "itemHash": 3159615086, "quantity": 2000, "hasConditionalVisibility": false } ] } } } }, "privacy": 2 }, "itemComponents": {}, "currencyLookups": { "data": { "itemQuantities": { "771273473": 5, "1633854071": 145, "685299502": 5, "2473252800": 4, "4257549984": 16, "4257549985": 29, "2367713531": 4, "3702027555": 5, "3467984096": 5, "2979281381": 50, "800069450": 183, "353704689": 24, "2289944966": 1, "3282419336": 208, "3325717727": 1, "443031982": 280, "1165306707": 3, "1289622079": 278, "3855200273": 100, "1471199156": 5, "810623803": 2, "443031983": 15, "592227263": 200, "937378714": 13, "950899352": 2251, "616392721": 5, "107323930": 1, "1293574817": 14, "1379061299": 4, "1485756901": 3637, "2993288448": 176, "3592324052": 223, "3853748946": 3498, "4046539562": 603, "4089840981": 1, "4114204995": 20, "4116837065": 5, "2133927665": 1, "2512446424": 50, "3788525515": 20, "3828673156": 1, "3622573279": 1, "777473377": 1, "3159615086": 500000, "2718300701": 300, "2817410917": 7178, "1643912408": 14, "3195333832": 1, "984897498": 1, "1929721158": 1, "678034719": 1, "59600177": 1, "1425631486": 1, "2088009808": 1, "1812118660": 1, "1094197864": 14, "4019412287": 5, "4238733045": 11, "1498161294": 11, "3692215122": 4, "971785346": 4, "2669877396": 16, "3034981848": 8, "2244763540": 18, "3369334256": 8, "412436772": 15, "857379656": 3, "3650458128": 1, "412313744": 30, "3388913371": 1, "2174713383": 24, "2610515000": 2, "2217640605": 2, "2838279629": 1, "1769847435": 1, "46524085": 1, "4138174248": 1, "3849810018": 1, "2130065553": 1, "2910326942": 1, "2558925366": 1, "2357297366": 1, "2119346509": 1, "1674692344": 1, "367772693": 1, "2973900274": 1, "48643186": 1, "3725585710": 1, "2351747818": 1, "1193318082": 1, "3851176026": 1, "3926153598": 1, "2111625436": 1, "1877183765": 1, "999767358": 1, "2773056939": 1, "2891906302": 1, "2292070913": 2, "586128500": 1, "893751566": 1, "3810243376": 1, "3423279826": 1, "3479737253": 1, "3202902670": 1, "3160437036": 1, "3129990424": 1, "4116381015": 1, "3218422834": 1, "503854896": 1, "3309120116": 1, "988607392": 1, "1444894250": 1, "807905267": 2, "1589715538": 1, "2270345041": 1, "328467570": 1, "175883909": 1, "4150538093": 2, "3437155610": 1, "3389206211": 1, "2809120022": 1, "3705925878": 1, "2313814566": 1, "3733702440": 1, "1163767710": 1, "1558857471": 1, "1302968378": 1, "3398078482": 1, "4238874520": 1, "1013853356": 1, "2504786634": 1, "2664281431": 1, "3981634627": 1, "952431286": 1, "2525637070": 1, "406589950": 1, "3317837688": 1, "3944833756": 1, "258623689": 1, "855351524": 1, "2949538030": 1, "2924794318": 1, "460688465": 1, "460688464": 1, "317465074": 1, "472776702": 1, "1289092817": 1, "460688467": 1, "2328211300": 1, "2240888816": 1, "2453351420": 1, "873720784": 1, "3785442599": 1, "4282591831": 1, "432848324": 1, "2540008660": 1, "1162929425": 1, "1985773548": 1, "1409726988": 1, "3992231371": 1, "3919847958": 1, "3936625542": 1, "2919429251": 1, "3903070396": 1, "1409726984": 1, "4012203003": 1, "825357415": 1, "152583919": 1, "4140860253": 1, "1269179840": 1, "3594816059": 1, "629267288": 1, "2922021463": 1, "3620647249": 1, "1391246437": 1, "3183180185": 1, "850726831": 1, "1968811824": 1, "903043774": 1, "2765534779": 1, "1652224118": 1, "890476372": 3, "4164299630": 1, "3858293505": 1, "3777269965": 1, "890476375": 7, "1472484362": 8, "3805576189": 1, "3644638448": 1, "74310575": 1, "2462004150": 1, "3995422990": 1, "3083700040": 1, "3586070587": 1, "3890873294": 1, "3140210033": 1, "4284902390": 1, "110539500": 1, "2991727273": 1, "1113229786": 1, "814073777": 1, "1248338443": 1, "2864963414": 1, "615393850": 1, "1051536457": 1, "3934247790": 1, "1831806500": 1, "3758175010": 1, "578566109": 1, "2038129348": 1, "2305891963": 1, "3216117824": 1, "4205508063": 1, "1505040830": 1, "2165646103": 1, "2231267742": 1, "3078568019": 1, "2220926673": 1, "4052488163": 1, "261046310": 1, "3660810735": 1, "1492378994": 1, "242688677": 1, "4044330011": 1, "2850552049": 1, "3980449645": 1, "4045313562": 1 }, "materialRequirementSetStates": { "935601344": { "materialRequirementSetHash": 935601344, "materialRequirementStates": [ { "itemHash": 3702027555, "count": 0, "stackSize": 5 }, { "itemHash": 3159615086, "count": 11000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 20, "stackSize": 3498 } ] }, "935601347": { "materialRequirementSetHash": 935601347, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 }, { "itemHash": 4257549984, "count": 3, "stackSize": 16 }, { "itemHash": 353704689, "count": 1, "stackSize": 24 } ] }, "935601346": { "materialRequirementSetHash": 935601346, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 }, { "itemHash": 4257549984, "count": 3, "stackSize": 16 }, { "itemHash": 353704689, "count": 1, "stackSize": 24 } ] }, "3145932713": { "materialRequirementSetHash": 3145932713, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 } ] }, "1458072636": { "materialRequirementSetHash": 1458072636, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 10000, "stackSize": 500000 } ] }, "95440906": { "materialRequirementSetHash": 95440906, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 11000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 17, "stackSize": 3498 } ] }, "564529998": { "materialRequirementSetHash": 564529998, "materialRequirementStates": [] }, "1854466728": { "materialRequirementSetHash": 1854466728, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 10000, "stackSize": 500000 } ] }, "4122362198": { "materialRequirementSetHash": 4122362198, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 11000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 17, "stackSize": 3498 } ] }, "2511021010": { "materialRequirementSetHash": 2511021010, "materialRequirementStates": [] }, "1914315298": { "materialRequirementSetHash": 1914315298, "materialRequirementStates": [ { "itemHash": 353704689, "count": 0, "stackSize": 24 }, { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "2100501659": { "materialRequirementSetHash": 2100501659, "materialRequirementStates": [ { "itemHash": 2228452164, "count": 1, "stackSize": 0 }, { "itemHash": 3702027555, "count": 0, "stackSize": 5 } ] }, "2327783007": { "materialRequirementSetHash": 2327783007, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 } ] }, "4025389633": { "materialRequirementSetHash": 4025389633, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 } ] }, "4034325565": { "materialRequirementSetHash": 4034325565, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 10000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "1743630547": { "materialRequirementSetHash": 1743630547, "materialRequirementStates": [ { "itemHash": 353704689, "count": 1, "stackSize": 24 }, { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 } ] }, "1408389375": { "materialRequirementSetHash": 1408389375, "materialRequirementStates": [ { "itemHash": 353704689, "count": 1, "stackSize": 24 }, { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 } ] }, "1154194199": { "materialRequirementSetHash": 1154194199, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "1154194198": { "materialRequirementSetHash": 1154194198, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "1154194197": { "materialRequirementSetHash": 1154194197, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 7500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 4, "stackSize": 3498 } ] }, "1154194196": { "materialRequirementSetHash": 1154194196, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 } ] }, "1154194195": { "materialRequirementSetHash": 1154194195, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 25000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 7, "stackSize": 3498 } ] }, "1154194194": { "materialRequirementSetHash": 1154194194, "materialRequirementStates": [ { "itemHash": 843838860, "count": 1, "stackSize": 0 } ] }, "1154194193": { "materialRequirementSetHash": 1154194193, "materialRequirementStates": [ { "itemHash": 843838860, "count": 1, "stackSize": 0 } ] }, "1154194192": { "materialRequirementSetHash": 1154194192, "materialRequirementStates": [ { "itemHash": 843838860, "count": 1, "stackSize": 0 } ] }, "1636699990": { "materialRequirementSetHash": 1636699990, "materialRequirementStates": [ { "itemHash": 843838860, "count": 1, "stackSize": 0 } ] }, "2228923964": { "materialRequirementSetHash": 2228923964, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "945498333": { "materialRequirementSetHash": 945498333, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3867293167, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "945498332": { "materialRequirementSetHash": 945498332, "materialRequirementStates": [ { "itemHash": 3867293163, "count": 1, "stackSize": 0 }, { "itemHash": 3338329097, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "945498335": { "materialRequirementSetHash": 945498335, "materialRequirementStates": [ { "itemHash": 3867293163, "count": 1, "stackSize": 0 }, { "itemHash": 3338329096, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "945498334": { "materialRequirementSetHash": 945498334, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3338329100, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "945498329": { "materialRequirementSetHash": 945498329, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3338329098, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "945498328": { "materialRequirementSetHash": 945498328, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3338329099, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "945498331": { "materialRequirementSetHash": 945498331, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3338329100, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "945498330": { "materialRequirementSetHash": 945498330, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3867293153, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "945498325": { "materialRequirementSetHash": 945498325, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3867293152, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "945498324": { "materialRequirementSetHash": 945498324, "materialRequirementStates": [ { "itemHash": 3867293163, "count": 1, "stackSize": 0 }, { "itemHash": 3867293166, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725988": { "materialRequirementSetHash": 2474725988, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3867293166, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725989": { "materialRequirementSetHash": 2474725989, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3338329096, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725990": { "materialRequirementSetHash": 2474725990, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3338329097, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725991": { "materialRequirementSetHash": 2474725991, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3867293153, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725984": { "materialRequirementSetHash": 2474725984, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3867293152, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725985": { "materialRequirementSetHash": 2474725985, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3338329103, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725986": { "materialRequirementSetHash": 2474725986, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3867293167, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725987": { "materialRequirementSetHash": 2474725987, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3338329098, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725996": { "materialRequirementSetHash": 2474725996, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3338329089, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2474725997": { "materialRequirementSetHash": 2474725997, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3338329102, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058877": { "materialRequirementSetHash": 2525058877, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3338329101, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058876": { "materialRequirementSetHash": 2525058876, "materialRequirementStates": [ { "itemHash": 3867293163, "count": 1, "stackSize": 0 }, { "itemHash": 3338329088, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058879": { "materialRequirementSetHash": 2525058879, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3867293152, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058878": { "materialRequirementSetHash": 2525058878, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3338329100, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058873": { "materialRequirementSetHash": 2525058873, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3338329102, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058872": { "materialRequirementSetHash": 2525058872, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3338329088, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058875": { "materialRequirementSetHash": 2525058875, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3338329089, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058874": { "materialRequirementSetHash": 2525058874, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3388661953, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058869": { "materialRequirementSetHash": 2525058869, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3867293166, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2525058868": { "materialRequirementSetHash": 2525058868, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3338329099, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2508281162": { "materialRequirementSetHash": 2508281162, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3388661953, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2508281163": { "materialRequirementSetHash": 2508281163, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3388661953, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "2508281160": { "materialRequirementSetHash": 2508281160, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3338329102, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "3673998563": { "materialRequirementSetHash": 3673998563, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3867293167, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "3673998562": { "materialRequirementSetHash": 3673998562, "materialRequirementStates": [ { "itemHash": 3867293163, "count": 1, "stackSize": 0 }, { "itemHash": 3338329097, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "3673998561": { "materialRequirementSetHash": 3673998561, "materialRequirementStates": [ { "itemHash": 3867293163, "count": 1, "stackSize": 0 }, { "itemHash": 3338329096, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "3673998560": { "materialRequirementSetHash": 3673998560, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3338329100, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "3673998567": { "materialRequirementSetHash": 3673998567, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3338329098, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "3673998566": { "materialRequirementSetHash": 3673998566, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3338329099, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "3673998565": { "materialRequirementSetHash": 3673998565, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3338329100, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "3673998564": { "materialRequirementSetHash": 3673998564, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3867293153, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "3673998571": { "materialRequirementSetHash": 3673998571, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3867293152, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "3673998570": { "materialRequirementSetHash": 3673998570, "materialRequirementStates": [ { "itemHash": 3867293163, "count": 1, "stackSize": 0 }, { "itemHash": 3867293166, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354230": { "materialRequirementSetHash": 2649354230, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3867293166, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354231": { "materialRequirementSetHash": 2649354231, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3338329096, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354228": { "materialRequirementSetHash": 2649354228, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3338329097, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354229": { "materialRequirementSetHash": 2649354229, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3867293153, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354226": { "materialRequirementSetHash": 2649354226, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3867293152, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354227": { "materialRequirementSetHash": 2649354227, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3338329103, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354224": { "materialRequirementSetHash": 2649354224, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3867293167, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354225": { "materialRequirementSetHash": 2649354225, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3338329098, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354238": { "materialRequirementSetHash": 2649354238, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3338329089, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2649354239": { "materialRequirementSetHash": 2649354239, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3338329102, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576515": { "materialRequirementSetHash": 2632576515, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3338329101, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576514": { "materialRequirementSetHash": 2632576514, "materialRequirementStates": [ { "itemHash": 3867293163, "count": 1, "stackSize": 0 }, { "itemHash": 3338329088, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576513": { "materialRequirementSetHash": 2632576513, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3867293152, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576512": { "materialRequirementSetHash": 2632576512, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3338329100, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576519": { "materialRequirementSetHash": 2632576519, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3338329102, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576518": { "materialRequirementSetHash": 2632576518, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3338329088, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576517": { "materialRequirementSetHash": 2632576517, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3338329089, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576516": { "materialRequirementSetHash": 2632576516, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3388661953, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576523": { "materialRequirementSetHash": 2632576523, "materialRequirementStates": [ { "itemHash": 3867293165, "count": 1, "stackSize": 0 }, { "itemHash": 3867293166, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2632576522": { "materialRequirementSetHash": 2632576522, "materialRequirementStates": [ { "itemHash": 3867293162, "count": 1, "stackSize": 0 }, { "itemHash": 3338329099, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2615798928": { "materialRequirementSetHash": 2615798928, "materialRequirementStates": [ { "itemHash": 3867293161, "count": 1, "stackSize": 0 }, { "itemHash": 3388661953, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2615798929": { "materialRequirementSetHash": 2615798929, "materialRequirementStates": [ { "itemHash": 3867293160, "count": 1, "stackSize": 0 }, { "itemHash": 3388661953, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "2615798930": { "materialRequirementSetHash": 2615798930, "materialRequirementStates": [ { "itemHash": 3867293164, "count": 1, "stackSize": 0 }, { "itemHash": 3338329102, "count": 1, "stackSize": 0 }, { "itemHash": 2162879194, "count": 10, "stackSize": 0 } ] }, "1139677991": { "materialRequirementSetHash": 1139677991, "materialRequirementStates": [ { "itemHash": 2162879194, "count": 15, "stackSize": 0 } ] }, "3533315173": { "materialRequirementSetHash": 3533315173, "materialRequirementStates": [ { "itemHash": 2835570795, "count": 2100, "stackSize": 0 } ] }, "3705371007": { "materialRequirementSetHash": 3705371007, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "2895945826": { "materialRequirementSetHash": 2895945826, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "4076440615": { "materialRequirementSetHash": 4076440615, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "489534282": { "materialRequirementSetHash": 489534282, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "690387059": { "materialRequirementSetHash": 690387059, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "1681842564": { "materialRequirementSetHash": 1681842564, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "4111161157": { "materialRequirementSetHash": 4111161157, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "2732484384": { "materialRequirementSetHash": 2732484384, "materialRequirementStates": [] }, "2133881383": { "materialRequirementSetHash": 2133881383, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 50, "stackSize": 500000 } ] }, "2575562186": { "materialRequirementSetHash": 2575562186, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "604222368": { "materialRequirementSetHash": 604222368, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "745689032": { "materialRequirementSetHash": 745689032, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "3852497618": { "materialRequirementSetHash": 3852497618, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "1656458293": { "materialRequirementSetHash": 1656458293, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "1575099945": { "materialRequirementSetHash": 1575099945, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "3688316323": { "materialRequirementSetHash": 3688316323, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 400, "stackSize": 500000 } ] }, "1633988883": { "materialRequirementSetHash": 1633988883, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 } ] }, "2899539364": { "materialRequirementSetHash": 2899539364, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "2446543652": { "materialRequirementSetHash": 2446543652, "materialRequirementStates": [ { "itemHash": 1505278293, "count": 1, "stackSize": 0 } ] }, "1585604029": { "materialRequirementSetHash": 1585604029, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 } ] }, "593856806": { "materialRequirementSetHash": 593856806, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "251037925": { "materialRequirementSetHash": 251037925, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "2214836462": { "materialRequirementSetHash": 2214836462, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "2844144231": { "materialRequirementSetHash": 2844144231, "materialRequirementStates": [ { "itemHash": 3853748946, "count": 5, "stackSize": 3498 } ] }, "3772980459": { "materialRequirementSetHash": 3772980459, "materialRequirementStates": [ { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "4144131749": { "materialRequirementSetHash": 4144131749, "materialRequirementStates": [ { "itemHash": 3853748946, "count": 10, "stackSize": 3498 } ] }, "2884799297": { "materialRequirementSetHash": 2884799297, "materialRequirementStates": [ { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "2704230363": { "materialRequirementSetHash": 2704230363, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "4080982664": { "materialRequirementSetHash": 4080982664, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "2833495167": { "materialRequirementSetHash": 2833495167, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 750, "stackSize": 500000 } ] }, "2833495166": { "materialRequirementSetHash": 2833495166, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "3497929822": { "materialRequirementSetHash": 3497929822, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 10000, "stackSize": 500000 } ] }, "3497929823": { "materialRequirementSetHash": 3497929823, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 50000, "stackSize": 500000 } ] }, "139151347": { "materialRequirementSetHash": 139151347, "materialRequirementStates": [ { "itemHash": 2817410917, "count": 10, "stackSize": 7178 } ] }, "139151346": { "materialRequirementSetHash": 139151346, "materialRequirementStates": [ { "itemHash": 2817410917, "count": 25, "stackSize": 7178 } ] }, "1132830599": { "materialRequirementSetHash": 1132830599, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 50, "stackSize": 500000 } ] }, "1100702562": { "materialRequirementSetHash": 1100702562, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 50, "stackSize": 500000 } ] }, "2332444919": { "materialRequirementSetHash": 2332444919, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 50, "stackSize": 500000 } ] }, "3384842521": { "materialRequirementSetHash": 3384842521, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 50, "stackSize": 500000 } ] }, "444819116": { "materialRequirementSetHash": 444819116, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 50, "stackSize": 500000 } ] }, "3964552737": { "materialRequirementSetHash": 3964552737, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "3020732508": { "materialRequirementSetHash": 3020732508, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "650867077": { "materialRequirementSetHash": 650867077, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "1328393472": { "materialRequirementSetHash": 1328393472, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 } ] }, "1708432873": { "materialRequirementSetHash": 1708432873, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 750, "stackSize": 500000 } ] }, "4110426674": { "materialRequirementSetHash": 4110426674, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "357059935": { "materialRequirementSetHash": 357059935, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "455529194": { "materialRequirementSetHash": 455529194, "materialRequirementStates": [ { "itemHash": 583548063, "count": 2, "stackSize": 0 } ] }, "455529195": { "materialRequirementSetHash": 455529195, "materialRequirementStates": [ { "itemHash": 583548063, "count": 3, "stackSize": 0 } ] }, "1168053688": { "materialRequirementSetHash": 1168053688, "materialRequirementStates": [ { "itemHash": 3853748946, "count": 15, "stackSize": 3498 }, { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 } ] }, "2507128937": { "materialRequirementSetHash": 2507128937, "materialRequirementStates": [] }, "1920773870": { "materialRequirementSetHash": 1920773870, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "797281477": { "materialRequirementSetHash": 797281477, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 } ] }, "3880411699": { "materialRequirementSetHash": 3880411699, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 777, "stackSize": 500000 } ] }, "4021893553": { "materialRequirementSetHash": 4021893553, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1500, "stackSize": 500000 } ] }, "983547049": { "materialRequirementSetHash": 983547049, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "1524569442": { "materialRequirementSetHash": 1524569442, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 777, "stackSize": 500000 } ] }, "856029439": { "materialRequirementSetHash": 856029439, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "3207656126": { "materialRequirementSetHash": 3207656126, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "2019714558": { "materialRequirementSetHash": 2019714558, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 }, { "itemHash": 2817410917, "count": 40, "stackSize": 7178 } ] }, "3933714686": { "materialRequirementSetHash": 3933714686, "materialRequirementStates": [] }, "976736818": { "materialRequirementSetHash": 976736818, "materialRequirementStates": [] }, "3947675841": { "materialRequirementSetHash": 3947675841, "materialRequirementStates": [ { "itemHash": 2817410917, "count": 100, "stackSize": 7178 } ] }, "1514656542": { "materialRequirementSetHash": 1514656542, "materialRequirementStates": [ { "itemHash": 2817410917, "count": 175, "stackSize": 7178 } ] }, "2164656339": { "materialRequirementSetHash": 2164656339, "materialRequirementStates": [ { "itemHash": 2817410917, "count": 875, "stackSize": 7178 } ] }, "2192786535": { "materialRequirementSetHash": 2192786535, "materialRequirementStates": [ { "itemHash": 2817410917, "count": 150, "stackSize": 7178 } ] }, "200721500": { "materialRequirementSetHash": 200721500, "materialRequirementStates": [ { "itemHash": 2817410917, "count": 450, "stackSize": 7178 } ] }, "2548257894": { "materialRequirementSetHash": 2548257894, "materialRequirementStates": [] }, "52071242": { "materialRequirementSetHash": 52071242, "materialRequirementStates": [] }, "3196647503": { "materialRequirementSetHash": 3196647503, "materialRequirementStates": [] }, "3200061738": { "materialRequirementSetHash": 3200061738, "materialRequirementStates": [ { "itemHash": 2718300701, "count": 1, "stackSize": 300 } ] }, "3906126011": { "materialRequirementSetHash": 3906126011, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "266623546": { "materialRequirementSetHash": 266623546, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "2196926153": { "materialRequirementSetHash": 2196926153, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "1670230647": { "materialRequirementSetHash": 1670230647, "materialRequirementStates": [ { "itemHash": 2718300701, "count": 0, "stackSize": 300 }, { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 0, "stackSize": 3498 } ] }, "2007311294": { "materialRequirementSetHash": 2007311294, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 0, "stackSize": 50 }, { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 2718300701, "count": 1, "stackSize": 300 }, { "itemHash": 3853748946, "count": 0, "stackSize": 3498 } ] }, "4154879933": { "materialRequirementSetHash": 4154879933, "materialRequirementStates": [ { "itemHash": 1305274547, "count": 0, "stackSize": 0 } ] }, "4211982558": { "materialRequirementSetHash": 4211982558, "materialRequirementStates": [ { "itemHash": 950899352, "count": 0, "stackSize": 2251 } ] }, "2988464829": { "materialRequirementSetHash": 2988464829, "materialRequirementStates": [ { "itemHash": 2014411539, "count": 0, "stackSize": 0 } ] }, "3674878801": { "materialRequirementSetHash": 3674878801, "materialRequirementStates": [ { "itemHash": 31293053, "count": 0, "stackSize": 0 } ] }, "3369639570": { "materialRequirementSetHash": 3369639570, "materialRequirementStates": [ { "itemHash": 49145143, "count": 0, "stackSize": 0 } ] }, "3103910193": { "materialRequirementSetHash": 3103910193, "materialRequirementStates": [ { "itemHash": 3487922223, "count": 0, "stackSize": 0 } ] }, "3748578940": { "materialRequirementSetHash": 3748578940, "materialRequirementStates": [ { "itemHash": 1177810185, "count": 0, "stackSize": 0 } ] }, "3867545234": { "materialRequirementSetHash": 3867545234, "materialRequirementStates": [ { "itemHash": 592227263, "count": 0, "stackSize": 200 } ] }, "108789637": { "materialRequirementSetHash": 108789637, "materialRequirementStates": [ { "itemHash": 3592324052, "count": 0, "stackSize": 223 } ] }, "3617345041": { "materialRequirementSetHash": 3617345041, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "2881877881": { "materialRequirementSetHash": 2881877881, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 0, "stackSize": 3498 } ] }, "2881877882": { "materialRequirementSetHash": 2881877882, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 0, "stackSize": 3498 } ] }, "2881877883": { "materialRequirementSetHash": 2881877883, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 0, "stackSize": 3498 } ] }, "2881877884": { "materialRequirementSetHash": 2881877884, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "2881877885": { "materialRequirementSetHash": 2881877885, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "2881877886": { "materialRequirementSetHash": 2881877886, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "2881877887": { "materialRequirementSetHash": 2881877887, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "2881877872": { "materialRequirementSetHash": 2881877872, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "2881877873": { "materialRequirementSetHash": 2881877873, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "3785659211": { "materialRequirementSetHash": 3785659211, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 } ] }, "99690599": { "materialRequirementSetHash": 99690599, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 10, "stackSize": 3498 } ] }, "99690596": { "materialRequirementSetHash": 99690596, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 0, "stackSize": 3498 } ] }, "99690597": { "materialRequirementSetHash": 99690597, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "99690594": { "materialRequirementSetHash": 99690594, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "99690595": { "materialRequirementSetHash": 99690595, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 } ] }, "1096424763": { "materialRequirementSetHash": 1096424763, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 8, "stackSize": 3498 } ] }, "130662630": { "materialRequirementSetHash": 130662630, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 8, "stackSize": 3498 } ] }, "1463078728": { "materialRequirementSetHash": 1463078728, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "3549770946": { "materialRequirementSetHash": 3549770946, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 } ] }, "3549770945": { "materialRequirementSetHash": 3549770945, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "3549770944": { "materialRequirementSetHash": 3549770944, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "3549770951": { "materialRequirementSetHash": 3549770951, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "3549770950": { "materialRequirementSetHash": 3549770950, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "3549770949": { "materialRequirementSetHash": 3549770949, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "3549770948": { "materialRequirementSetHash": 3549770948, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "3549770955": { "materialRequirementSetHash": 3549770955, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 4257549984, "count": 1, "stackSize": 16 } ] }, "3549770954": { "materialRequirementSetHash": 3549770954, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 } ] }, "3588361558": { "materialRequirementSetHash": 3588361558, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 4000, "stackSize": 500000 }, { "itemHash": 4257549985, "count": 1, "stackSize": 29 } ] }, "2324226263": { "materialRequirementSetHash": 2324226263, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 } ] }, "2324226260": { "materialRequirementSetHash": 2324226260, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "2324226261": { "materialRequirementSetHash": 2324226261, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "2324226258": { "materialRequirementSetHash": 2324226258, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "2324226259": { "materialRequirementSetHash": 2324226259, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 } ] }, "2324226256": { "materialRequirementSetHash": 2324226256, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "2324226257": { "materialRequirementSetHash": 2324226257, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "2324226270": { "materialRequirementSetHash": 2324226270, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 4000, "stackSize": 500000 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 } ] }, "2324226271": { "materialRequirementSetHash": 2324226271, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 4000, "stackSize": 500000 }, { "itemHash": 4257549984, "count": 3, "stackSize": 16 } ] }, "3967414853": { "materialRequirementSetHash": 3967414853, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 }, { "itemHash": 4257549985, "count": 3, "stackSize": 29 } ] }, "4116823703": { "materialRequirementSetHash": 4116823703, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "4116823702": { "materialRequirementSetHash": 4116823702, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 1000, "stackSize": 500000 } ] }, "4116823697": { "materialRequirementSetHash": 4116823697, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 1500, "stackSize": 500000 } ] }, "4116823696": { "materialRequirementSetHash": 4116823696, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 } ] }, "4116823699": { "materialRequirementSetHash": 4116823699, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 3500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "4116823698": { "materialRequirementSetHash": 4116823698, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 6000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "4116823709": { "materialRequirementSetHash": 4116823709, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 9000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 1, "stackSize": 16 } ] }, "4116823708": { "materialRequirementSetHash": 4116823708, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 12000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 3, "stackSize": 16 } ] }, "3705602764": { "materialRequirementSetHash": 3705602764, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 10000, "stackSize": 500000 } ] }, "2680836216": { "materialRequirementSetHash": 2680836216, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "2680836217": { "materialRequirementSetHash": 2680836217, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 1500, "stackSize": 500000 } ] }, "2680836222": { "materialRequirementSetHash": 2680836222, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 2500, "stackSize": 500000 } ] }, "2680836223": { "materialRequirementSetHash": 2680836223, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 } ] }, "2680836220": { "materialRequirementSetHash": 2680836220, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 8000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "2680836221": { "materialRequirementSetHash": 2680836221, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 11000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 } ] }, "2680836210": { "materialRequirementSetHash": 2680836210, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 } ] }, "2680836211": { "materialRequirementSetHash": 2680836211, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 19000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 5, "stackSize": 16 } ] }, "108835217": { "materialRequirementSetHash": 108835217, "materialRequirementStates": [ { "itemHash": 2979281381, "count": 1, "stackSize": 50 }, { "itemHash": 3159615086, "count": 20000, "stackSize": 500000 } ] }, "1488841134": { "materialRequirementSetHash": 1488841134, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 777, "stackSize": 500000 } ] }, "245139275": { "materialRequirementSetHash": 245139275, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1500, "stackSize": 500000 } ] }, "4032750400": { "materialRequirementSetHash": 4032750400, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2000, "stackSize": 500000 } ] }, "2082271808": { "materialRequirementSetHash": 2082271808, "materialRequirementStates": [] }, "1228020973": { "materialRequirementSetHash": 1228020973, "materialRequirementStates": [] }, "723759326": { "materialRequirementSetHash": 723759326, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 777, "stackSize": 500000 } ] }, "1921496923": { "materialRequirementSetHash": 1921496923, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 777, "stackSize": 500000 } ] }, "3557732400": { "materialRequirementSetHash": 3557732400, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 777, "stackSize": 500000 } ] }, "2611197965": { "materialRequirementSetHash": 2611197965, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 777, "stackSize": 500000 } ] }, "2413613994": { "materialRequirementSetHash": 2413613994, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 777, "stackSize": 500000 } ] }, "3493515811": { "materialRequirementSetHash": 3493515811, "materialRequirementStates": [] }, "585769096": { "materialRequirementSetHash": 585769096, "materialRequirementStates": [] }, "2463837317": { "materialRequirementSetHash": 2463837317, "materialRequirementStates": [] }, "1564811296": { "materialRequirementSetHash": 1564811296, "materialRequirementStates": [] }, "2672958770": { "materialRequirementSetHash": 2672958770, "materialRequirementStates": [ { "itemHash": 6249081, "count": 1, "stackSize": 0 } ] }, "2672958771": { "materialRequirementSetHash": 2672958771, "materialRequirementStates": [ { "itemHash": 6249080, "count": 1, "stackSize": 0 } ] }, "2672958768": { "materialRequirementSetHash": 2672958768, "materialRequirementStates": [ { "itemHash": 6249083, "count": 1, "stackSize": 0 } ] }, "1492125331": { "materialRequirementSetHash": 1492125331, "materialRequirementStates": [ { "itemHash": 3020799946, "count": 1, "stackSize": 0 } ] }, "1175267805": { "materialRequirementSetHash": 1175267805, "materialRequirementStates": [ { "itemHash": 4037641632, "count": 1, "stackSize": 0 } ] }, "2260833676": { "materialRequirementSetHash": 2260833676, "materialRequirementStates": [ { "itemHash": 2209399863, "count": 1, "stackSize": 0 } ] }, "2260833677": { "materialRequirementSetHash": 2260833677, "materialRequirementStates": [ { "itemHash": 2209399862, "count": 1, "stackSize": 0 } ] }, "1054787202": { "materialRequirementSetHash": 1054787202, "materialRequirementStates": [ { "itemHash": 513968009, "count": 1, "stackSize": 0 } ] }, "418401439": { "materialRequirementSetHash": 418401439, "materialRequirementStates": [ { "itemHash": 4159830752, "count": 1, "stackSize": 0 } ] }, "733742519": { "materialRequirementSetHash": 733742519, "materialRequirementStates": [ { "itemHash": 225483518, "count": 1, "stackSize": 0 } ] }, "733742518": { "materialRequirementSetHash": 733742518, "materialRequirementStates": [ { "itemHash": 225483519, "count": 1, "stackSize": 0 } ] }, "3776923878": { "materialRequirementSetHash": 3776923878, "materialRequirementStates": [ { "itemHash": 3160697021, "count": 1, "stackSize": 0 } ] }, "2311284646": { "materialRequirementSetHash": 2311284646, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 }, { "itemHash": 4257549984, "count": 3, "stackSize": 16 }, { "itemHash": 353704689, "count": 2, "stackSize": 24 } ] }, "317133567": { "materialRequirementSetHash": 317133567, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 } ] }, "317133564": { "materialRequirementSetHash": 317133564, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 7000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 6, "stackSize": 3498 } ] }, "317133565": { "materialRequirementSetHash": 317133565, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 15000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 7, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 1, "stackSize": 16 } ] }, "317133562": { "materialRequirementSetHash": 317133562, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 20000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 8, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 3, "stackSize": 16 } ] }, "317133563": { "materialRequirementSetHash": 317133563, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 25000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 9, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 5, "stackSize": 16 }, { "itemHash": 4257549985, "count": 1, "stackSize": 29 } ] }, "4051449874": { "materialRequirementSetHash": 4051449874, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "4051449873": { "materialRequirementSetHash": 4051449873, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "4051449872": { "materialRequirementSetHash": 4051449872, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 1, "stackSize": 16 } ] }, "4051449879": { "materialRequirementSetHash": 4051449879, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 7000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 4, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 }, { "itemHash": 4257549985, "count": 1, "stackSize": 29 } ] }, "4051449878": { "materialRequirementSetHash": 4051449878, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 8500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 }, { "itemHash": 4257549985, "count": 2, "stackSize": 29 } ] }, "3469165782": { "materialRequirementSetHash": 3469165782, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "3469165783": { "materialRequirementSetHash": 3469165783, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 300, "stackSize": 500000 } ] }, "3469165780": { "materialRequirementSetHash": 3469165780, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "3469165781": { "materialRequirementSetHash": 3469165781, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1200, "stackSize": 500000 } ] }, "3469165778": { "materialRequirementSetHash": 3469165778, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "3469165779": { "materialRequirementSetHash": 3469165779, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 4500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "2663623407": { "materialRequirementSetHash": 2663623407, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "2663623406": { "materialRequirementSetHash": 2663623406, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1200, "stackSize": 500000 } ] }, "2663623405": { "materialRequirementSetHash": 2663623405, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2800, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "2663623404": { "materialRequirementSetHash": 2663623404, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 4000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "2663623403": { "materialRequirementSetHash": 2663623403, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "2663623402": { "materialRequirementSetHash": 2663623402, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 7000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 1, "stackSize": 16 } ] }, "1150390892": { "materialRequirementSetHash": 1150390892, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "1150390893": { "materialRequirementSetHash": 1150390893, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "1150390894": { "materialRequirementSetHash": 1150390894, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "1150390895": { "materialRequirementSetHash": 1150390895, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 5000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "1150390888": { "materialRequirementSetHash": 1150390888, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 7000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 4, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 1, "stackSize": 16 } ] }, "1150390889": { "materialRequirementSetHash": 1150390889, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 8500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 }, { "itemHash": 4257549985, "count": 1, "stackSize": 29 } ] }, "187527613": { "materialRequirementSetHash": 187527613, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "187527612": { "materialRequirementSetHash": 187527612, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "187527615": { "materialRequirementSetHash": 187527615, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3300, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 1, "stackSize": 16 } ] }, "187527614": { "materialRequirementSetHash": 187527614, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 6500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 4, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 1, "stackSize": 16 } ] }, "187527609": { "materialRequirementSetHash": 187527609, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 8200, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 }, { "itemHash": 4257549985, "count": 1, "stackSize": 29 } ] }, "187527608": { "materialRequirementSetHash": 187527608, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 10000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 7, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 3, "stackSize": 16 }, { "itemHash": 4257549985, "count": 1, "stackSize": 29 } ] }, "1989672882": { "materialRequirementSetHash": 1989672882, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 10, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 9, "stackSize": 16 }, { "itemHash": 4257549985, "count": 2, "stackSize": 29 } ] }, "1989672883": { "materialRequirementSetHash": 1989672883, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2400, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 1, "stackSize": 16 } ] }, "1989672880": { "materialRequirementSetHash": 1989672880, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 3500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 5, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 } ] }, "1989672881": { "materialRequirementSetHash": 1989672881, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 7500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 6, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 2, "stackSize": 16 } ] }, "1989672886": { "materialRequirementSetHash": 1989672886, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 9100, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 7, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 3, "stackSize": 16 }, { "itemHash": 4257549985, "count": 1, "stackSize": 29 } ] }, "1989672887": { "materialRequirementSetHash": 1989672887, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 12500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 9, "stackSize": 3498 }, { "itemHash": 4257549984, "count": 5, "stackSize": 16 }, { "itemHash": 4257549985, "count": 2, "stackSize": 29 } ] }, "1546074058": { "materialRequirementSetHash": 1546074058, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 250, "stackSize": 500000 } ] }, "1546074057": { "materialRequirementSetHash": 1546074057, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 300, "stackSize": 500000 } ] }, "1546074056": { "materialRequirementSetHash": 1546074056, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 500, "stackSize": 500000 } ] }, "1546074063": { "materialRequirementSetHash": 1546074063, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 700, "stackSize": 500000 } ] }, "1546074062": { "materialRequirementSetHash": 1546074062, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 900, "stackSize": 500000 } ] }, "1546074061": { "materialRequirementSetHash": 1546074061, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1100, "stackSize": 500000 } ] }, "1546074060": { "materialRequirementSetHash": 1546074060, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1300, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "1546074051": { "materialRequirementSetHash": 1546074051, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1500, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 1, "stackSize": 3498 } ] }, "1546074050": { "materialRequirementSetHash": 1546074050, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 1700, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 2, "stackSize": 3498 } ] }, "3686585038": { "materialRequirementSetHash": 3686585038, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 2000, "stackSize": 500000 }, { "itemHash": 3853748946, "count": 3, "stackSize": 3498 } ] }, "2492892279": { "materialRequirementSetHash": 2492892279, "materialRequirementStates": [ { "itemHash": 1583786617, "count": 1, "stackSize": 0 } ] }, "1220635782": { "materialRequirementSetHash": 1220635782, "materialRequirementStates": [ { "itemHash": 4019412287, "count": 1, "stackSize": 5 } ] }, "981201632": { "materialRequirementSetHash": 981201632, "materialRequirementStates": [ { "itemHash": 4238733045, "count": 1, "stackSize": 11 } ] }, "1158946875": { "materialRequirementSetHash": 1158946875, "materialRequirementStates": [ { "itemHash": 1498161294, "count": 1, "stackSize": 11 } ] }, "1657777515": { "materialRequirementSetHash": 1657777515, "materialRequirementStates": [] }, "1912681418": { "materialRequirementSetHash": 1912681418, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 0, "stackSize": 500000 } ] }, "934031711": { "materialRequirementSetHash": 934031711, "materialRequirementStates": [ { "itemHash": 3159615086, "count": 10000, "stackSize": 500000 }, { "itemHash": 3467984096, "count": 1, "stackSize": 5 } ] } } }, "privacy": 2 }, "stringVariables": { "privacy": 2 } }, "ErrorCode": 1, "ThrottleSeconds": 0, "ErrorStatus": "Success", "Message": "Ok", "MessageData": {} } ================================================ FILE: src/testing/global.d.ts ================================================ // Specifying the types for these modules skips huge type inference from JSON // steps. Sadly these can't use a wildcard because TypeScript will not match // wildcards across `-` characters. declare module 'testing/data/profile-2025-12-02.json' { import { DestinyProfileResponse, ServerResponse } from 'bungie-api-ts/destiny2'; const value: ServerResponse; export default value; } declare module 'testing/data/vendors-2025-12-02.json' { import { DestinyVendorsResponse, ServerResponse } from 'bungie-api-ts/destiny2'; const value: ServerResponse; export default value; } declare module 'testing/data/d1profiles-2022-10-24.json' { import { D1GetAccountResponse } from 'app/destiny1/d1-manifest-types'; const value: D1GetAccountResponse; export default value; } ================================================ FILE: src/testing/jest-setup.cjs ================================================ // setupJest.js or similar file // Set environment variables for tests process.env.NODE_ENV = 'test'; process.env.LOCAL_MANIFEST = 'true'; // eslint-disable-next-line @typescript-eslint/no-unsafe-call require('jest-fetch-mock').enableMocks(); const crypto = require('crypto'); const util = require('util'); Object.defineProperty(globalThis, 'crypto', { value: { getRandomValues: (arr) => crypto.randomBytes(arr.length), randomUUID: () => crypto.randomUUID(), }, }); Object.assign(global, { TextDecoder: util.TextDecoder, TextEncoder: util.TextEncoder }); ================================================ FILE: src/testing/precache-manifest.test.ts ================================================ /* eslint-disable no-console */ import fs from 'node:fs/promises'; import path from 'node:path'; import { getTestManifestJson } from './test-utils'; beforeAll(() => { delete process.env.LOCAL_MANIFEST; delete process.env.NODE_ENV; }); test('precache manifest', async () => { const [_manifest, filename] = await getTestManifestJson(); console.log('Loaded manifest to', filename); // This is for our CI workers - it prevents the cache from accumulating manifests if (process.env.CLEAN_MANIFEST_CACHE) { const cacheDir = path.resolve(__dirname, '..', '..', 'manifest-cache'); const files = (await fs.readdir(cacheDir)).filter((f) => f !== path.basename(filename)); console.log( 'Cleaning manifest cache of files', files, files.map((f) => path.join(cacheDir, f)), ); await Promise.all(files.map((f) => fs.unlink(path.join(cacheDir, f)))); } }); ================================================ FILE: src/testing/test-item-utils.ts ================================================ import { DimItem } from 'app/inventory/item-types'; import { BucketHashes } from 'data/d2/generated-enums'; /** a general mod, 3 energy */ export const classStatModHash = 4204488676; // InventoryItem "Class Mod" /** raid mod, dsc mod */ export const enhancedOperatorAugmentModHash = 817361141; // InventoryItem "Enhanced Operator Augment" /** class item mod, 3 energy */ export const reaperModHash = 40751621; // InventoryItem "Reaper" /** legs mod, 4 energy */ export const stacksOnStacksModHash = 3994043492; // InventoryItem "Stacks on Stacks" /** legs mod, 3 energy */ export const elementalChargeModHash = 3712696020; // InventoryItem "Elemental Charge" /** 1st class item mod with mutual exclusion behavior, 1 energy */ export const empoweringFinishModHash = 84503918; // InventoryItem "Empowered Finish" /** 2nd class item mod with mutual exclusion behavior, 1 energy */ export const bulwarkFinishModHash = 4004774874; // InventoryItem "Bulwark Finisher" function isArmor2Item(item: DimItem) { return item.energy && item.bucket.inArmor && !item.equippingLabel && item.rarity === 'Legendary'; } export function isArmor2Helmet(item: DimItem) { return isArmor2Item(item) && item.bucket.hash === BucketHashes.Helmet; } export function isArmor2Arms(item: DimItem) { return isArmor2Item(item) && item.bucket.hash === BucketHashes.Gauntlets; } export function isArmor2Chest(item: DimItem) { return isArmor2Item(item) && item.bucket.hash === BucketHashes.ChestArmor; } export function isArmor2Legs(item: DimItem) { return isArmor2Item(item) && item.bucket.hash === BucketHashes.LegArmor; } export function isArmor2ClassItem(item: DimItem) { return isArmor2Item(item) && item.bucket.hash === BucketHashes.ClassArmor; } ================================================ FILE: src/testing/test-utils.ts ================================================ import { getBuckets } from 'app/destiny2/d2-buckets'; import { allTables, buildDefinitionsFromManifest } from 'app/destiny2/d2-definitions'; import { buildStores } from 'app/inventory/store/d2-store-factory'; import { downloadManifestComponents } from 'app/manifest/manifest-service-json'; import { humanBytes } from 'app/storage/human-bytes'; import { delay } from 'app/utils/promises'; import { AllDestinyManifestComponents, DestinyManifest } from 'bungie-api-ts/destiny2'; import { F_OK } from 'constants'; import { maxBy, once } from 'es-toolkit'; import i18next from 'i18next'; import fetchMock from 'jest-fetch-mock'; import de from 'locale/de.json'; import en from 'locale/en.json'; import es from 'locale/es.json'; import esMX from 'locale/esMX.json'; import fr from 'locale/fr.json'; import it from 'locale/it.json'; import ja from 'locale/ja.json'; import ko from 'locale/ko.json'; import pl from 'locale/pl.json'; import ptBR from 'locale/ptBR.json'; import ru from 'locale/ru.json'; import zhCHS from 'locale/zhCHS.json'; import zhCHT from 'locale/zhCHT.json'; import fs from 'node:fs/promises'; import path from 'node:path'; import profile from 'testing/data/profile-2025-12-02.json'; import vendors from 'testing/data/vendors-2025-12-02.json'; import { getManifest as d2GetManifest } from '../app/bungie-api/destiny2-api'; /** * Get the current manifest as JSON. Downloads the manifest if not cached. */ // TODO: maybe use the fake indexeddb and just insert it from a file on startup?? // fake indexeddb + mock server (msw or jest-mock-fetch) to simulate the API?? export async function getTestManifestJson() { fetchMock.dontMock(); try { // download and parse manifest const cacheDir = path.resolve(__dirname, '..', '..', 'manifest-cache'); // In this mode we assume the last-written file in the manifest-cache directory is the one we want if (process.env.LOCAL_MANIFEST) { const result = await getLocalManifest(cacheDir); if (result) { return result; } } let manifest: DestinyManifest; for (let i = 0; ; i++) { try { manifest = await d2GetManifest(); break; } catch (e) { if (i === 4) { // Fall back on local manifest when Bungie.net is down const result = await getLocalManifest(cacheDir); if (result) { return result; } throw e; } await delay(1000); } } const enManifestUrl = manifest.jsonWorldContentPaths.en; const filename = path.resolve(cacheDir, path.basename(enManifestUrl)); const fileExists = await fs .access(filename, F_OK) .then(() => true) .catch(() => false); if (fileExists) { return [ JSON.parse(await fs.readFile(filename, 'utf-8')) as AllDestinyManifestComponents, filename, ] as const; } await fs.mkdir(cacheDir, { recursive: true }); const manifestDb = await downloadManifestComponents( manifest.jsonWorldComponentContentPaths.en, allTables, ); await fs.writeFile(filename, JSON.stringify(manifestDb), 'utf-8'); return [manifestDb, filename] as const; } finally { fetchMock.dontMock(); } } // This gets the local manifest by finding the most recently modified json file in the cache directory. async function getLocalManifest(cacheDir: string) { const files = (await fs.readdir(cacheDir)).filter((f) => path.extname(f) === '.json'); if (files.length) { const mtimes = await Promise.all( files.map(async (f) => ({ filename: f, mtime: (await fs.stat(path.join(cacheDir, f))).mtime.getTime(), })), ); const filename = path.resolve(cacheDir, maxBy(mtimes, (f) => f.mtime)!.filename); return [ JSON.parse(await fs.readFile(filename, 'utf-8')) as AllDestinyManifestComponents, filename, ] as const; } } export const getTestDefinitions = once(async () => { const [manifestJson] = await getTestManifestJson(); return buildDefinitionsFromManifest(manifestJson); }); export const testAccount = { displayName: 'VidBoi-BMC', originalPlatformType: 2, membershipId: '4611686018433092312', platformLabel: 'PlayStation', destinyVersion: 2, platforms: [1, 3, 5, 2], lastPlayed: '2021-05-08T03:34:26.000Z', }; export const getTestProfile = () => profile.Response; export const fetchTestProfile = async () => profile.Response; export const getTestVendors = () => vendors.Response; export const getTestStores = once(async () => { const manifest = await getTestDefinitions(); const stores = buildStores({ defs: manifest, buckets: getBuckets(manifest), profileResponse: getTestProfile(), customStats: [], }); return stores; }); /** * Set up i18n so the `t` function will work, for en and ja locales. * * Use `i18next.changeLanguage('en');` to set language before tests. */ export function setupi18n() { i18next.init({ lng: 'en', debug: true, lowerCaseLng: true, interpolation: { escapeValue: false, format(val: string, format) { switch (format) { case 'pct': return `${Math.min(100, Math.floor(100 * parseFloat(val)))}%`; case 'humanBytes': return humanBytes(parseInt(val, 10)); case 'number': return parseInt(val, 10).toLocaleString(); default: return val; } }, }, resources: { en: { translation: en, }, ja: { translation: ja, }, de: { translation: de, }, es: { translation: es, }, 'es-mx': { translation: esMX, }, fr: { translation: fr, }, it: { translation: it, }, ko: { translation: ko, }, pl: { translation: pl, }, 'pt-br': { translation: ptBR, }, ru: { translation: ru, }, 'zh-chs': { translation: zhCHS, }, 'zh-cht': { translation: zhCHT, }, }, }); } ================================================ FILE: src/testing/utils/i18next.ts ================================================ import i18next from 'i18next'; /** * Get the text from the resource for a given key and locale. * Interpolate text with count. */ export function getCopyWithCount(key: string, locale: string, count: number) { const suffix = getSuffix(locale, count); const copy = getCopyFromResource(key + suffix, locale); return i18next.services.interpolator.interpolate(copy, { count }, locale, { escapeValue: false, }); } /** * Gets one value by given key. */ function getCopyFromResource(key: string, locale: string) { return i18next.getResource(locale, 'translation', key) as string; } /** * Get the plural suffix for a given locale and count. * e.b. _zero, _one, _few, _many, _other */ function getSuffix(locale: string, count: number) { const resolver = i18next.services.pluralResolver as { getSuffix: (locale: string, count: number) => string; }; return resolver.getSuffix(locale, count); } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "strict": true, "esModuleInterop": true, "outDir": "./dist/", "sourceMap": true, "module": "ESNext", "target": "ESNext", "jsx": "preserve", "moduleResolution": "bundler", "noUnusedLocals": true, "noUnusedParameters": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "allowJs": true, "rootDir": "./src", "types": [ "node", "jest", "dom-chromium-installation-events", "dom-screen-wake-lock", "webpack-env" ], "paths": { "app/*": ["./src/app/*"], "data/*": ["./src/data/*"], "images/*": ["./src/images/*"], "destiny-icons/*": ["./destiny-icons/*"], "locale/*": ["./src/locale/*"], "testing/*": ["./src/testing/*"], "docs/*": ["./docs/*"], "*": ["./*"] } }, "include": ["./src/**/*", "config/feature-flags.ts"] }